108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { RefObject } from "react";
|
|
import { Heading2, Heading3, Code, Quote, Image, Link, Minus, Bold, FileCode, Grid3X3, Video, Play } from "lucide-react";
|
|
|
|
interface ShortcutPanelProps {
|
|
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
}
|
|
|
|
export default function ShortcutPanel({ textareaRef, value, onChange }: ShortcutPanelProps) {
|
|
const insert = (before: string, after: string, placeholder: string) => {
|
|
const el = textareaRef.current;
|
|
if (!el) {
|
|
onChange(value + before + placeholder + after);
|
|
return;
|
|
}
|
|
const start = el.selectionStart;
|
|
const end = el.selectionEnd;
|
|
const selected = value.slice(start, end) || placeholder;
|
|
const newVal = value.slice(0, start) + before + selected + after + value.slice(end);
|
|
onChange(newVal);
|
|
setTimeout(() => {
|
|
const pos = start + before.length + selected.length + after.length;
|
|
el.setSelectionRange(start + before.length, pos);
|
|
el.focus();
|
|
}, 0);
|
|
};
|
|
|
|
const insertBlock = (template: string) => {
|
|
const el = textareaRef.current;
|
|
if (!el) {
|
|
onChange(value + template);
|
|
return;
|
|
}
|
|
const start = el.selectionStart;
|
|
const newVal = value.slice(0, start) + template + value.slice(start);
|
|
onChange(newVal);
|
|
setTimeout(() => {
|
|
el.selectionStart = start + template.length;
|
|
el.selectionEnd = start + template.length;
|
|
el.focus();
|
|
}, 0);
|
|
};
|
|
|
|
const gridTemplate = `{{< grid cols="3" >}}
|
|
![描述]()
|
|
![描述]()
|
|
![描述]()
|
|
{{< /grid >}}
|
|
`;
|
|
|
|
const videoTemplate = `{{< video src="/video/example.mp4" caption="视频说明" >}}
|
|
`;
|
|
|
|
const bilibiliTemplate = `{{< bilibili BV1xx411c7mD >}}
|
|
`;
|
|
|
|
const youtubeTemplate = `{{< youtube dQw4w9WgXcQ >}}
|
|
`;
|
|
|
|
const shortcuts = [
|
|
{ label: "H2", icon: Heading2, action: () => insert("## ", "\n", "二级标题") },
|
|
{ label: "H3", icon: Heading3, action: () => insert("### ", "\n", "三级标题") },
|
|
{ label: "粗体", icon: Bold, action: () => insert("**", "**", "粗体文本") },
|
|
{ label: "代码块", icon: FileCode, action: () => insert("```\n", "\n```\n", "代码") },
|
|
{ label: "行内代码", icon: Code, action: () => insert("`", "`", "code") },
|
|
{ label: "引用", icon: Quote, action: () => insert("> ", "\n", "引用内容") },
|
|
{ label: "图片", icon: Image, action: () => insert("![", "]()", "描述") },
|
|
{ label: "图片网格", icon: Grid3X3, action: () => insertBlock(gridTemplate) },
|
|
{ label: "本地视频", icon: Video, action: () => insertBlock(videoTemplate) },
|
|
{ label: "B站视频", icon: Play, action: () => insertBlock(bilibiliTemplate) },
|
|
{ label: "YouTube", icon: Play, action: () => insertBlock(youtubeTemplate) },
|
|
{ label: "链接", icon: Link, action: () => insert("[", "]()", "链接文本") },
|
|
{ label: "分割线", icon: Minus, action: () => insert("\n---\n", "", "") },
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">快捷插入</h3>
|
|
<div className="bg-card border border-line rounded-lg p-3 space-y-1">
|
|
{shortcuts.map((s) => (
|
|
<button
|
|
key={s.label}
|
|
type="button"
|
|
onClick={s.action}
|
|
className="w-full flex items-center gap-2.5 px-2.5 py-2 text-[12px] text-text/70 hover:text-accent hover:bg-accent/4 rounded transition-colors"
|
|
title={s.label}
|
|
>
|
|
<s.icon size={14} className="shrink-0" />
|
|
<span>{s.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="mt-4 p-3 bg-surface border border-line/50 rounded-lg">
|
|
<h4 className="text-[10px] font-medium text-muted uppercase tracking-wider mb-2">说明</h4>
|
|
<ul className="text-[11px] text-muted/70 space-y-1 leading-relaxed">
|
|
<li>选中文字后点按钮可包裹</li>
|
|
<li>未选中则插入模板 + 占位文字</li>
|
|
<li>视频需替换 src 中的路径</li>
|
|
<li>B站/YouTube 替换 BV号/ID</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|