0.5 缩减重构
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
SuggestionMenuController,
|
||||
getDefaultReactSlashMenuItems,
|
||||
@@ -9,34 +9,34 @@ import {
|
||||
} from "@blocknote/react";
|
||||
import { filterSuggestionItems, type BlockNoteEditor } from "@blocknote/core";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
FileImage,
|
||||
FilePlus2,
|
||||
FileVideo,
|
||||
ListTree,
|
||||
Music,
|
||||
Paperclip,
|
||||
PilcrowSquare,
|
||||
Play,
|
||||
Spline,
|
||||
Sparkles,
|
||||
SquareCheckBig,
|
||||
Table,
|
||||
} from "lucide-react";
|
||||
import type { CustomBlockSchema } from "../schema";
|
||||
import { useImagePicker } from "@/components/media/image-picker-context";
|
||||
import type { MediaKind, MediaSelection } from "@/types/media";
|
||||
import { createOnlineTable } from "@/lib/online-table";
|
||||
|
||||
type Props = {
|
||||
editor: BlockNoteEditor<CustomBlockSchema>;
|
||||
currentDocumentId: string;
|
||||
};
|
||||
|
||||
import {
|
||||
FileImage,
|
||||
FilePlus2,
|
||||
FileVideo,
|
||||
ListTree,
|
||||
Music,
|
||||
Paperclip,
|
||||
PilcrowSquare,
|
||||
Play,
|
||||
Spline,
|
||||
Sparkles,
|
||||
SquareCheckBig,
|
||||
Table,
|
||||
} from "lucide-react";
|
||||
import type { CustomBlockSchema } from "../schema";
|
||||
import { useImagePicker } from "@/components/media/image-picker-context";
|
||||
import type { MediaKind, MediaSelection } from "@/types/media";
|
||||
import { createOnlineTable } from "@/lib/online-table";
|
||||
|
||||
type Props = {
|
||||
editor: BlockNoteEditor<CustomBlockSchema>;
|
||||
currentDocumentId: string;
|
||||
};
|
||||
|
||||
const matchKeywords = (query: string, aliases: string[]) => {
|
||||
const lower = query.trim().toLowerCase();
|
||||
if (!lower) return true;
|
||||
return aliases.some((alias) => alias.toLowerCase().includes(lower));
|
||||
const lower = query.trim().toLowerCase();
|
||||
if (!lower) return true;
|
||||
return aliases.some((alias) => alias.toLowerCase().includes(lower));
|
||||
};
|
||||
|
||||
function insertOrUpdateBlockForSlashMenuCompat(
|
||||
@@ -100,95 +100,95 @@ function insertOrUpdateBlockForSlashMenuCompat(
|
||||
|
||||
editor.insertBlocks([partialBlock as never], referenceBlock, "after");
|
||||
}
|
||||
|
||||
const GROUP_TRANSLATIONS: Record<string, string> = {
|
||||
"Headings": "标题",
|
||||
"Subheadings": "副标题",
|
||||
"Basic blocks": "基础块",
|
||||
"Advanced": "高级",
|
||||
"Media": "媒体",
|
||||
"Others": "其他",
|
||||
};
|
||||
|
||||
const DEFAULT_ITEM_TRANSLATIONS: Record<
|
||||
string,
|
||||
{ title?: string; subtext?: string; group?: string; aliases?: string[] }
|
||||
> = {
|
||||
"Paragraph": { title: "正文", group: "基础块", aliases: ["zw", "paragraph", "body"] },
|
||||
"Heading 1": { title: "主标题", group: "标题", aliases: ["biaoti", "bt1"] },
|
||||
"Heading 2": { title: "大标题", group: "标题", aliases: ["biaoti", "bt2"] },
|
||||
"Heading 3": { title: "中标题", group: "标题", aliases: ["biaoti", "bt3"] },
|
||||
"Heading 4": { title: "小标题", group: "标题", aliases: ["biaoti", "bt4"] },
|
||||
"Heading 5": { title: "极小标题", group: "标题", aliases: ["biaoti", "bt5"] },
|
||||
"Heading 6": { title: "最小标题", group: "标题", aliases: ["biaoti", "bt6"] },
|
||||
"Toggle Heading 1": { title: "可折叠主标题", group: "标题", aliases: ["toggle", "zd1"] },
|
||||
"Toggle Heading 2": { title: "可折叠大标题", group: "标题", aliases: ["toggle", "zd2"] },
|
||||
"Toggle Heading 3": { title: "可折叠中标题", group: "标题", aliases: ["toggle", "zd3"] },
|
||||
"Quote": { title: "引用", group: "基础块", aliases: ["quote", "引用"] },
|
||||
"Toggle List": { title: "折叠列表", group: "基础块", aliases: ["toggle list", "zd"] },
|
||||
"Numbered List": { title: "数字列表", group: "基础块", aliases: ["ordered", "ol"] },
|
||||
"Bullet List": { title: "符号列表", group: "基础块", aliases: ["ul", "list"] },
|
||||
"Check List": { title: "任务列表", group: "基础块", aliases: ["todo", "checkbox"] },
|
||||
"Code Block": { title: "代码块", group: "基础块", aliases: ["code", "pre"] },
|
||||
"Table": { title: "表格", group: "高级", aliases: ["table", "biaoge"] },
|
||||
"Image": { title: "插入图片", group: "媒体", subtext: "上传或引用图片资源", aliases: ["tupian", "image", "tp"] },
|
||||
"Video": { title: "插入视频", group: "媒体", subtext: "上传或引用视频", aliases: ["shipin", "video", "sp"] },
|
||||
"Audio": { title: "插入音频", group: "媒体", subtext: "上传或引用音频", aliases: ["yinpin", "audio", "yp"] },
|
||||
"File": { title: "插入文件", group: "媒体", subtext: "上传附件并生成卡片", aliases: ["wenjian", "file", "wj"] },
|
||||
"Emoji": { title: "插入表情", group: "其他", aliases: ["emoji", "biaoqing"] },
|
||||
"Divider": { title: "分割线", group: "基础块", aliases: ["divider", "hr"] },
|
||||
"Page Break": { title: "分页符", group: "基础块", aliases: ["page", "break"] },
|
||||
};
|
||||
|
||||
const MEDIA_ICONS: Record<MediaKind, JSX.Element> = {
|
||||
image: <FileImage className="h-4 w-4 text-[#2563eb]" />,
|
||||
video: <FileVideo className="h-4 w-4 text-[#f97316]" />,
|
||||
audio: <Music className="h-4 w-4 text-[#10b981]" />,
|
||||
file: <Paperclip className="h-4 w-4 text-[#0f172a]" />,
|
||||
};
|
||||
|
||||
const HEADING_PRESETS = [
|
||||
{
|
||||
level: 1,
|
||||
title: "主标题",
|
||||
subtext: "适合页面名称/顶层章节",
|
||||
aliases: ["biaoti1", "h1", "level1"],
|
||||
},
|
||||
{
|
||||
level: 2,
|
||||
title: "大标题",
|
||||
subtext: "用于章节逻辑层",
|
||||
aliases: ["biaoti2", "h2", "level2"],
|
||||
},
|
||||
{
|
||||
level: 3,
|
||||
title: "中标题",
|
||||
subtext: "用于小节和段落",
|
||||
aliases: ["biaoti3", "h3", "level3"],
|
||||
},
|
||||
{
|
||||
level: 4,
|
||||
title: "小标题",
|
||||
subtext: "更细的结构说明",
|
||||
aliases: ["biaoti4", "h4", "level4"],
|
||||
},
|
||||
{
|
||||
level: 5,
|
||||
title: "极小标题",
|
||||
subtext: "适合脚注/补充说明",
|
||||
aliases: ["biaoti5", "h5", "level5"],
|
||||
},
|
||||
];
|
||||
|
||||
const isHeadingDefaultItem = (item: DefaultReactSuggestionItem) => {
|
||||
const maybeKey = (item as { key?: string }).key ?? "";
|
||||
if (maybeKey && (maybeKey === "heading" || maybeKey.startsWith("heading_") || maybeKey.startsWith("toggle_heading"))) {
|
||||
return true;
|
||||
}
|
||||
const title = item.title ?? "";
|
||||
return title.includes("标题");
|
||||
};
|
||||
|
||||
|
||||
const GROUP_TRANSLATIONS: Record<string, string> = {
|
||||
"Headings": "标题",
|
||||
"Subheadings": "副标题",
|
||||
"Basic blocks": "基础块",
|
||||
"Advanced": "高级",
|
||||
"Media": "媒体",
|
||||
"Others": "其他",
|
||||
};
|
||||
|
||||
const DEFAULT_ITEM_TRANSLATIONS: Record<
|
||||
string,
|
||||
{ title?: string; subtext?: string; group?: string; aliases?: string[] }
|
||||
> = {
|
||||
"Paragraph": { title: "正文", group: "基础块", aliases: ["zw", "paragraph", "body"] },
|
||||
"Heading 1": { title: "主标题", group: "标题", aliases: ["biaoti", "bt1"] },
|
||||
"Heading 2": { title: "大标题", group: "标题", aliases: ["biaoti", "bt2"] },
|
||||
"Heading 3": { title: "中标题", group: "标题", aliases: ["biaoti", "bt3"] },
|
||||
"Heading 4": { title: "小标题", group: "标题", aliases: ["biaoti", "bt4"] },
|
||||
"Heading 5": { title: "极小标题", group: "标题", aliases: ["biaoti", "bt5"] },
|
||||
"Heading 6": { title: "最小标题", group: "标题", aliases: ["biaoti", "bt6"] },
|
||||
"Toggle Heading 1": { title: "可折叠主标题", group: "标题", aliases: ["toggle", "zd1"] },
|
||||
"Toggle Heading 2": { title: "可折叠大标题", group: "标题", aliases: ["toggle", "zd2"] },
|
||||
"Toggle Heading 3": { title: "可折叠中标题", group: "标题", aliases: ["toggle", "zd3"] },
|
||||
"Quote": { title: "引用", group: "基础块", aliases: ["quote", "引用"] },
|
||||
"Toggle List": { title: "折叠列表", group: "基础块", aliases: ["toggle list", "zd"] },
|
||||
"Numbered List": { title: "数字列表", group: "基础块", aliases: ["ordered", "ol"] },
|
||||
"Bullet List": { title: "符号列表", group: "基础块", aliases: ["ul", "list"] },
|
||||
"Check List": { title: "任务列表", group: "基础块", aliases: ["todo", "checkbox"] },
|
||||
"Code Block": { title: "代码块", group: "基础块", aliases: ["code", "pre"] },
|
||||
"Table": { title: "表格", group: "高级", aliases: ["table", "biaoge"] },
|
||||
"Image": { title: "插入图片", group: "媒体", subtext: "上传或引用图片资源", aliases: ["tupian", "image", "tp"] },
|
||||
"Video": { title: "插入视频", group: "媒体", subtext: "上传或引用视频", aliases: ["shipin", "video", "sp"] },
|
||||
"Audio": { title: "插入音频", group: "媒体", subtext: "上传或引用音频", aliases: ["yinpin", "audio", "yp"] },
|
||||
"File": { title: "插入文件", group: "媒体", subtext: "上传附件并生成卡片", aliases: ["wenjian", "file", "wj"] },
|
||||
"Emoji": { title: "插入表情", group: "其他", aliases: ["emoji", "biaoqing"] },
|
||||
"Divider": { title: "分割线", group: "基础块", aliases: ["divider", "hr"] },
|
||||
"Page Break": { title: "分页符", group: "基础块", aliases: ["page", "break"] },
|
||||
};
|
||||
|
||||
const MEDIA_ICONS: Record<MediaKind, JSX.Element> = {
|
||||
image: <FileImage className="h-4 w-4 text-[#2563eb]" />,
|
||||
video: <FileVideo className="h-4 w-4 text-[#f97316]" />,
|
||||
audio: <Music className="h-4 w-4 text-[#10b981]" />,
|
||||
file: <Paperclip className="h-4 w-4 text-[#0f172a]" />,
|
||||
};
|
||||
|
||||
const HEADING_PRESETS = [
|
||||
{
|
||||
level: 1,
|
||||
title: "主标题",
|
||||
subtext: "适合页面名称/顶层章节",
|
||||
aliases: ["biaoti1", "h1", "level1"],
|
||||
},
|
||||
{
|
||||
level: 2,
|
||||
title: "大标题",
|
||||
subtext: "用于章节逻辑层",
|
||||
aliases: ["biaoti2", "h2", "level2"],
|
||||
},
|
||||
{
|
||||
level: 3,
|
||||
title: "中标题",
|
||||
subtext: "用于小节和段落",
|
||||
aliases: ["biaoti3", "h3", "level3"],
|
||||
},
|
||||
{
|
||||
level: 4,
|
||||
title: "小标题",
|
||||
subtext: "更细的结构说明",
|
||||
aliases: ["biaoti4", "h4", "level4"],
|
||||
},
|
||||
{
|
||||
level: 5,
|
||||
title: "极小标题",
|
||||
subtext: "适合脚注/补充说明",
|
||||
aliases: ["biaoti5", "h5", "level5"],
|
||||
},
|
||||
];
|
||||
|
||||
const isHeadingDefaultItem = (item: DefaultReactSuggestionItem) => {
|
||||
const maybeKey = (item as { key?: string }).key ?? "";
|
||||
if (maybeKey && (maybeKey === "heading" || maybeKey.startsWith("heading_") || maybeKey.startsWith("toggle_heading"))) {
|
||||
return true;
|
||||
}
|
||||
const title = item.title ?? "";
|
||||
return title.includes("标题");
|
||||
};
|
||||
|
||||
export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
const defaultItems = useMemo(() => getDefaultReactSlashMenuItems(editor), [editor]);
|
||||
const router = useRouter();
|
||||
@@ -220,7 +220,7 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const createMindmapItem: DefaultReactSuggestionItem = {
|
||||
title: "思维导图",
|
||||
group: "高级",
|
||||
@@ -235,7 +235,7 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const createPageItem: DefaultReactSuggestionItem = {
|
||||
title: "嵌入页面",
|
||||
group: "嵌入",
|
||||
@@ -268,12 +268,12 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
router.refresh();
|
||||
},
|
||||
};
|
||||
|
||||
const headingItems: DefaultReactSuggestionItem[] = HEADING_PRESETS.map((preset) => ({
|
||||
title: preset.title,
|
||||
group: "标题",
|
||||
subtext: preset.subtext,
|
||||
aliases: preset.aliases,
|
||||
|
||||
const headingItems: DefaultReactSuggestionItem[] = HEADING_PRESETS.map((preset) => ({
|
||||
title: preset.title,
|
||||
group: "标题",
|
||||
subtext: preset.subtext,
|
||||
aliases: preset.aliases,
|
||||
icon: <PilcrowSquare className="h-4 w-4 text-[#0f172a]" />,
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
||||
@@ -283,10 +283,10 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const foldHeading: DefaultReactSuggestionItem = {
|
||||
title: "折叠标题",
|
||||
group: "标题",
|
||||
|
||||
const foldHeading: DefaultReactSuggestionItem = {
|
||||
title: "折叠标题",
|
||||
group: "标题",
|
||||
aliases: ["toggle", "zd", "fold"],
|
||||
icon: <ListTree className="h-4 w-4 text-[#0f172a]" />,
|
||||
onItemClick: () => {
|
||||
@@ -297,12 +297,12 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const advancedTodo: DefaultReactSuggestionItem = {
|
||||
title: "高级待办",
|
||||
group: "待办",
|
||||
subtext: "四态状态 · Alt 直接取消",
|
||||
aliases: ["gjdblb", "todopro", "gaoji", "todo+"],
|
||||
|
||||
const advancedTodo: DefaultReactSuggestionItem = {
|
||||
title: "高级待办",
|
||||
group: "待办",
|
||||
subtext: "四态状态 · Alt 直接取消",
|
||||
aliases: ["gjdblb", "todopro", "gaoji", "todo+"],
|
||||
icon: <SquareCheckBig className="h-4 w-4 text-[#2563eb]" />,
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
||||
@@ -312,12 +312,12 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const progressMeter: DefaultReactSuggestionItem = {
|
||||
title: "进度条",
|
||||
group: "进度",
|
||||
subtext: "自动读取下方待办完成度",
|
||||
aliases: ["jdt", "progress", "jindu"],
|
||||
|
||||
const progressMeter: DefaultReactSuggestionItem = {
|
||||
title: "进度条",
|
||||
group: "进度",
|
||||
subtext: "自动读取下方待办完成度",
|
||||
aliases: ["jdt", "progress", "jindu"],
|
||||
icon: <Sparkles className="h-4 w-4 text-[#f59e0b]" />,
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
||||
@@ -327,10 +327,10 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const foldAdvancedTodo: DefaultReactSuggestionItem = {
|
||||
title: "折叠高级待办",
|
||||
group: "待办",
|
||||
|
||||
const foldAdvancedTodo: DefaultReactSuggestionItem = {
|
||||
title: "折叠高级待办",
|
||||
group: "待办",
|
||||
aliases: ["zdgjdb", "foldtodo"],
|
||||
icon: <Play className="h-4 w-4 text-[#9f1239]" />,
|
||||
onItemClick: () => {
|
||||
@@ -341,18 +341,18 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const customItems = [
|
||||
...headingItems,
|
||||
foldHeading,
|
||||
createPageItem,
|
||||
createTableItem,
|
||||
createMindmapItem,
|
||||
advancedTodo,
|
||||
foldAdvancedTodo,
|
||||
progressMeter,
|
||||
].filter((item) => matchKeywords(query, item.aliases ?? []));
|
||||
|
||||
|
||||
const customItems = [
|
||||
...headingItems,
|
||||
foldHeading,
|
||||
createPageItem,
|
||||
createTableItem,
|
||||
createMindmapItem,
|
||||
advancedTodo,
|
||||
foldAdvancedTodo,
|
||||
progressMeter,
|
||||
].filter((item) => matchKeywords(query, item.aliases ?? []));
|
||||
|
||||
const insertMediaSelection = (selection: MediaSelection) => {
|
||||
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
||||
type: "media",
|
||||
@@ -369,7 +369,7 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
content: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleMediaPick = (mediaType: MediaKind) => {
|
||||
openPicker({
|
||||
mediaType,
|
||||
@@ -379,41 +379,41 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
||||
...selection,
|
||||
assetType: selection.assetType ?? mediaType,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const localizedDefaults = defaultItems.map((item) => {
|
||||
const translation = DEFAULT_ITEM_TRANSLATIONS[item.title];
|
||||
const next: DefaultReactSuggestionItem = { ...item };
|
||||
if (translation?.title) next.title = translation.title;
|
||||
if (translation?.subtext) next.subtext = translation.subtext;
|
||||
if (translation?.aliases) next.aliases = translation.aliases;
|
||||
if (translation?.group) {
|
||||
next.group = translation.group;
|
||||
} else if (item.group && GROUP_TRANSLATIONS[item.group]) {
|
||||
next.group = GROUP_TRANSLATIONS[item.group];
|
||||
}
|
||||
|
||||
if (["Image", "Video", "Audio", "File"].includes(item.title)) {
|
||||
const mediaType = item.title.toLowerCase() as MediaKind;
|
||||
next.icon = MEDIA_ICONS[mediaType];
|
||||
next.group = translation?.group ?? "媒体";
|
||||
next.subtext = translation?.subtext ?? next.subtext;
|
||||
next.aliases = translation?.aliases ?? next.aliases;
|
||||
next.onItemClick = () => handleMediaPick(mediaType);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const sanitizedDefaults = localizedDefaults.filter(
|
||||
(item) => !isHeadingDefaultItem(item) && item.title !== "表格",
|
||||
);
|
||||
const merged = [...customItems, ...sanitizedDefaults];
|
||||
return filterSuggestionItems(merged, query);
|
||||
},
|
||||
[currentDocumentId, defaultItems, editor, openPicker, router],
|
||||
);
|
||||
|
||||
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const localizedDefaults = defaultItems.map((item) => {
|
||||
const translation = DEFAULT_ITEM_TRANSLATIONS[item.title];
|
||||
const next: DefaultReactSuggestionItem = { ...item };
|
||||
if (translation?.title) next.title = translation.title;
|
||||
if (translation?.subtext) next.subtext = translation.subtext;
|
||||
if (translation?.aliases) next.aliases = translation.aliases;
|
||||
if (translation?.group) {
|
||||
next.group = translation.group;
|
||||
} else if (item.group && GROUP_TRANSLATIONS[item.group]) {
|
||||
next.group = GROUP_TRANSLATIONS[item.group];
|
||||
}
|
||||
|
||||
if (["Image", "Video", "Audio", "File"].includes(item.title)) {
|
||||
const mediaType = item.title.toLowerCase() as MediaKind;
|
||||
next.icon = MEDIA_ICONS[mediaType];
|
||||
next.group = translation?.group ?? "媒体";
|
||||
next.subtext = translation?.subtext ?? next.subtext;
|
||||
next.aliases = translation?.aliases ?? next.aliases;
|
||||
next.onItemClick = () => handleMediaPick(mediaType);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const sanitizedDefaults = localizedDefaults.filter(
|
||||
(item) => !isHeadingDefaultItem(item) && item.title !== "表格",
|
||||
);
|
||||
const merged = [...customItems, ...sanitizedDefaults];
|
||||
return filterSuggestionItems(merged, query);
|
||||
},
|
||||
[currentDocumentId, defaultItems, editor, openPicker, router],
|
||||
);
|
||||
|
||||
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user