Files
blweb/src/components/admin/MarkdownToolbar.jsx
Cho-P4 c5994759fb Initial commit: anime-minimalist personal blog
- React 18 + Vite + React Router v6
- Glass morphism cards with rounded corners
- Dark/light theme toggle
- Article CRUD with localStorage persistence
- Search, admin panel (PIN: 2501), sakura petal canvas animation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:10:59 +08:00

174 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react';
import styles from './MarkdownToolbar.module.css';
const TOOLS = [
{ label: 'B', title: '加粗', wrap: (s) => `<strong>${s}</strong>` },
{ label: 'I', title: '斜体', wrap: (s) => `<em>${s}</em>` },
{ label: 'H2', title: '二级标题', wrap: (s) => `<h2>${s}</h2>` },
{ label: '❝', title: '引用', wrap: (s) => `<blockquote>${s}</blockquote>` },
{ label: '</>', title: '代码', wrap: (s) => `<code>${s}</code>` },
];
export default function MarkdownToolbar({ textareaRef }) {
const [imgPopover, setImgPopover] = useState(false);
const [imgUrl, setImgUrl] = useState('');
const [imgAlt, setImgAlt] = useState('');
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null);
const insertAt = (html) => {
const ta = textareaRef.current;
if (!ta) return;
const pos = ta.selectionEnd;
const before = ta.value.slice(0, pos);
const after = ta.value.slice(pos);
const next = before + '\n' + html + '\n' + after;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, next);
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.focus();
};
const apply = (wrapFn) => {
const ta = textareaRef.current;
if (!ta) return;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const selected = ta.value.slice(start, end) || '内容';
const wrapped = wrapFn(selected);
const before = ta.value.slice(0, start);
const after = ta.value.slice(end);
const next = before + wrapped + after;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, next);
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.focus();
ta.setSelectionRange(start + wrapped.length, start + wrapped.length);
};
const insertImgUrl = () => {
if (!imgUrl.trim()) return;
const alt = imgAlt.trim() || '图片';
insertAt(`<img src="${imgUrl.trim()}" alt="${alt}" />`);
setImgUrl('');
setImgAlt('');
setImgPopover(false);
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
if (file.size > 2 * 1024 * 1024) {
alert('图片文件不能超过 2MBbase64 会使文章体积变大)');
return;
}
setUploading(true);
const reader = new FileReader();
reader.onload = (ev) => {
const src = ev.target.result;
const alt = file.name.replace(/\.[^.]+$/, '') || '图片';
insertAt(`<img src="${src}" alt="${alt}" />`);
setUploading(false);
setImgPopover(false);
e.target.value = '';
};
reader.readAsDataURL(file);
};
return (
<div className={styles.toolbarWrap}>
<div className={styles.toolbar}>
{TOOLS.map(t => (
<button
key={t.label}
type="button"
className={styles.tool}
title={t.title}
onMouseDown={(e) => { e.preventDefault(); apply(t.wrap); }}
>
{t.label}
</button>
))}
<div className={styles.separator} />
<button
type="button"
className={styles.tool}
title="插入图片"
onMouseDown={(e) => { e.preventDefault(); setImgPopover(v => !v); }}
>
🖼
</button>
</div>
{imgPopover && (
<div className={`${styles.popover} glass-card`}>
<p className={styles.popoverTitle}>插入图片</p>
<div className={styles.popoverSection}>
<label className={styles.popoverLabel}>图片 URL</label>
<div className={styles.urlRow}>
<input
className={styles.popoverInput}
value={imgUrl}
onChange={e => setImgUrl(e.target.value)}
onKeyDown={e => e.key === 'Enter' && insertImgUrl()}
placeholder="https://example.com/image.jpg"
autoFocus
/>
<button
type="button"
className="btn btn--primary"
onClick={insertImgUrl}
disabled={!imgUrl.trim()}
>
插入
</button>
</div>
<input
className={styles.popoverInput}
value={imgAlt}
onChange={e => setImgAlt(e.target.value)}
placeholder="图片描述alt可选"
style={{ marginTop: 6 }}
/>
</div>
<div className={styles.divider}>
<span></span>
</div>
<div className={styles.popoverSection}>
<label className={styles.popoverLabel}>上传本地图片 2MB base64</label>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
type="button"
className="btn"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
style={{ width: '100%', justifyContent: 'center' }}
>
{uploading ? '处理中…' : '选择图片文件'}
</button>
</div>
<button
type="button"
className={styles.popoverClose}
onClick={() => setImgPopover(false)}
>
取消
</button>
</div>
)}
</div>
);
}