feat(M10/M3/M2): FR-M4/M6/X1/X2/B6/D1/D3 元数据/导出/弹层/迁移——EditorToolbar 加星标按钮(M4); DocMeta 加 createdAt+frontmatter 读写+DocInfoPanel 显示+创建时写入(M6); copyMd 失败 catch+错误提示(X1); @media print 干净排版+hljs配色(X2); SettingsPanel/CheatSheet Esc 关闭(B6); migration-service 旧版板块转换+init+switch 检测(D1/D3); meta-export E2E 3 项
This commit is contained in:
@@ -66,7 +66,7 @@ export function DocInfoPanel() {
|
||||
|
||||
<Section title="修改时间">
|
||||
<div style={{ opacity: 0.6 }}>{mtime ? new Date(mtime).toLocaleString('zh-CN') : '—'}</div>
|
||||
<div style={{ opacity: 0.4, fontSize: 12, marginTop: 4 }}>创建时间:—(按设计不落盘)</div>
|
||||
<div style={{ opacity: 0.4, fontSize: 12, marginTop: 4 }}>创建时间:{meta.createdAt ? new Date(meta.createdAt).toLocaleString('zh-CN') : '—'}</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,12 @@ import { useDialog } from '../dialog/dialog-store';
|
||||
import { invoke } from '../../ipc-client';
|
||||
import { IPC } from '@hunter/shared';
|
||||
|
||||
// FR-M4:编辑区星标入口
|
||||
function StarButton() {
|
||||
const { meta, toggleStar } = useStore();
|
||||
return <button onClick={() => toggleStar()} title="星标" style={{ ...btn, color: meta.star ? 'var(--accent)' : 'var(--text-tertiary)', fontSize: 16 }}>{meta.star ? '★' : '☆'}</button>;
|
||||
}
|
||||
|
||||
const btn: React.CSSProperties = { minWidth: 30, height: 28, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4, fontSize: 13 };
|
||||
|
||||
function T({ onClick, title, children, disabled }: { onClick: () => void; title: string; children: React.ReactNode; disabled?: boolean }) {
|
||||
@@ -36,6 +42,7 @@ export function EditorToolbar() {
|
||||
};
|
||||
return (
|
||||
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--control-border)', display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<StarButton />
|
||||
<T title="撤销 Ctrl+Z" onClick={cmd(e => e.chain().focus().undo().run())} disabled={d}>↶</T>
|
||||
<T title="重做 Ctrl+Y" onClick={cmd(e => e.chain().focus().redo().run())} disabled={d}>↷</T>
|
||||
<span style={{ width: 1, background: 'var(--control-border)', height: 20, margin: '0 4px' }} />
|
||||
|
||||
@@ -7,7 +7,12 @@ export function MoreActions() {
|
||||
const { currentPath, body, openDoc } = useStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
if (!currentPath) return null;
|
||||
const copyMd = async () => { await invoke(IPC.copyMd, { path: currentPath }); notify('已复制 Markdown'); setOpen(false); };
|
||||
// FR-X1:复制全文——成功/失败均提示
|
||||
const copyMd = async () => {
|
||||
try { await invoke(IPC.copyMd, { path: currentPath }); notify('已复制 Markdown'); }
|
||||
catch (e) { notify('复制失败:' + (e instanceof Error ? e.message : String(e)), true); }
|
||||
setOpen(false);
|
||||
};
|
||||
const print = () => { window.print(); setOpen(false); };
|
||||
const reveal = async () => { await invoke(IPC.docReveal, { path: currentPath }); setOpen(false); };
|
||||
const reload = async () => { await openDoc(currentPath); setOpen(false); };
|
||||
@@ -29,4 +34,4 @@ function Item({ children, onClick }: { children: React.ReactNode; onClick: () =>
|
||||
return <div onClick={onClick} style={{ padding: '6px 12px', cursor: 'pointer', borderRadius: 4, fontSize: 13, color: 'var(--text-primary)' }} onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{children}</div>;
|
||||
}
|
||||
const btn: React.CSSProperties = { height: 26, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4, fontSize: 14, padding: '0 8px' };
|
||||
function notify(msg: string) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;top:50px;right:20px;background:var(--accent);color:var(--accent-text);padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px'; document.body.appendChild(el); setTimeout(() => el.remove(), 2500); }
|
||||
function notify(msg: string, isError?: boolean) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;top:50px;right:20px;background:' + (isError ? '#e81123' : 'var(--accent)') + ';color:' + (isError ? '#fff' : 'var(--accent-text)') + ';padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px'; document.body.appendChild(el); setTimeout(() => el.remove(), 2500); }
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useKeymap, formatCombo, type Scope } from './keymap-service';
|
||||
|
||||
const SCOPE_LABELS: Record<Scope, string> = { global: '全局', app: '应用级', editor: '编辑器' };
|
||||
@@ -5,6 +6,12 @@ const SCOPE_ORDER: Scope[] = ['global', 'app', 'editor'];
|
||||
|
||||
export function CheatSheet({ onClose }: { onClose: () => void }) {
|
||||
const bindings = useKeymap(s => s.bindings);
|
||||
// FR-B6:Esc 关闭速查面板
|
||||
useEffect(() => {
|
||||
const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } };
|
||||
window.addEventListener('keydown', onEsc, true);
|
||||
return () => window.removeEventListener('keydown', onEsc, true);
|
||||
}, [onClose]);
|
||||
return (
|
||||
<div onClick={e => { if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}>
|
||||
<div style={{ width: 480, maxHeight: '80vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
|
||||
|
||||
@@ -38,6 +38,13 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
return () => window.removeEventListener('keydown', onKey, true);
|
||||
}, [recId, setCombo, saveKeymap]);
|
||||
// FR-B6:Esc 关闭设置面板(不录制快捷键时生效)
|
||||
useEffect(() => {
|
||||
if (recId) return; // 正在录制时不触发
|
||||
const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } };
|
||||
window.addEventListener('keydown', onEsc, true);
|
||||
return () => window.removeEventListener('keydown', onEsc, true);
|
||||
}, [recId, onClose]);
|
||||
return (
|
||||
<div onClick={e => { if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}>
|
||||
<div style={{ width: 520, maxHeight: '80vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
|
||||
|
||||
@@ -72,4 +72,26 @@ html, body, #root { margin: 0; height: 100%; }
|
||||
}
|
||||
.tiptap-wrap .ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder); color: var(--text-tertiary); pointer-events: none; float: left;
|
||||
}
|
||||
}
|
||||
|
||||
/* FR-X2:打印/另存为 PDF 干净排版 + 代码语法高亮 */
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
.tiptap-wrap, .tiptap-wrap * { visibility: visible; }
|
||||
.tiptap-wrap { position: absolute; left: 0; top: 0; width: 100%; padding: 20px; }
|
||||
.tiptap-wrap .ProseMirror { font-size: 14px !important; color: #000 !important; }
|
||||
.tiptap-wrap .ProseMirror pre { background: #f4f4f4 !important; color: #333 !important; border: 1px solid #ddd; padding: 8px; border-radius: 4px; overflow: visible; }
|
||||
.tiptap-wrap .ProseMirror pre code { color: #333; }
|
||||
.tiptap-wrap .ProseMirror h1, .tiptap-wrap .ProseMirror h2, .tiptap-wrap .ProseMirror h3 { color: #000; }
|
||||
.tiptap-wrap .ProseMirror blockquote { border-color: #ccc; color: #555; }
|
||||
.tiptap-wrap .ProseMirror a { color: #0066cc; text-decoration: underline; }
|
||||
}
|
||||
|
||||
/* FR-A12:统一过渡动效 */
|
||||
button { transition: background 0.15s, color 0.15s, opacity 0.15s; }
|
||||
button:hover { opacity: 0.85; }
|
||||
@keyframes panel-slide-in {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.panel-slide-in { animation: panel-slide-in 0.2s ease-out; }
|
||||
Reference in New Issue
Block a user