- LandingPage: 全新水墨晕染算法背景,循环墨迹动画 - 登录/注册页: 禅意黑白极简风格 - HomePage: 三栏布局优化,标题颜色语义化 - 纪念日组件: 分类逻辑优化,颜色语义统一 - 提醒组件: 分组标题颜色优化,逾期提示更醒目 - 修复农历日期边界问题(29/30天月份) - 添加 lunar-javascript 类型声明 - 清理未使用的导入和代码
222 lines
7.2 KiB
TypeScript
222 lines
7.2 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Paper,
|
|
Textarea,
|
|
Group,
|
|
Text,
|
|
Stack,
|
|
Button,
|
|
Box,
|
|
} from '@mantine/core';
|
|
import { useDebouncedValue } from '@mantine/hooks';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import rehypeSanitize from 'rehype-sanitize';
|
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
|
import { useAppStore } from '../../stores';
|
|
|
|
interface NoteEditorProps {
|
|
onSave?: () => void;
|
|
}
|
|
|
|
type ViewMode = 'edit' | 'preview';
|
|
|
|
export function NoteEditor({ onSave }: NoteEditorProps) {
|
|
const notes = useAppStore((state) => state.notes);
|
|
const updateNotesContent = useAppStore((state) => state.updateNotesContent);
|
|
const saveNotes = useAppStore((state) => state.saveNotes);
|
|
const fetchNotes = useAppStore((state) => state.fetchNotes);
|
|
|
|
const [content, setContent] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
const [viewMode] = useState<ViewMode>('edit');
|
|
|
|
// Initialize content from notes
|
|
useEffect(() => {
|
|
if (notes) {
|
|
setContent(notes.content);
|
|
}
|
|
}, [notes]);
|
|
|
|
// Fetch notes on mount
|
|
useEffect(() => {
|
|
fetchNotes();
|
|
}, [fetchNotes]);
|
|
|
|
// Auto-save with 3 second debounce
|
|
const [debouncedContent] = useDebouncedValue(content, 3000);
|
|
|
|
useEffect(() => {
|
|
if (debouncedContent !== undefined && notes && debouncedContent !== content) {
|
|
handleSave(debouncedContent);
|
|
}
|
|
}, [debouncedContent]);
|
|
|
|
const handleSave = useCallback(
|
|
async (value: string) => {
|
|
if (!notes) return;
|
|
|
|
setSaving(true);
|
|
try {
|
|
updateNotesContent(value);
|
|
await saveNotes(value);
|
|
setLastSaved(new Date());
|
|
onSave?.();
|
|
} catch (error) {
|
|
console.error('Failed to save note:', error);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[notes, updateNotesContent, saveNotes, onSave]
|
|
);
|
|
|
|
const formatLastSaved = () => {
|
|
if (!lastSaved) return '未保存';
|
|
const now = new Date();
|
|
const diff = now.getTime() - lastSaved.getTime();
|
|
if (diff < 1000) return '刚刚保存';
|
|
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`;
|
|
return lastSaved.toLocaleTimeString('zh-CN');
|
|
};
|
|
|
|
const handleManualSave = () => {
|
|
if (content && notes) {
|
|
handleSave(content);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Paper p="md" withBorder radius={4} h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Stack gap="sm" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
{/* Header */}
|
|
<Group justify="space-between" style={{ flexShrink: 0 }}>
|
|
<Group gap="sm">
|
|
<Text fw={400} size="sm" style={{ letterSpacing: '0.1em', color: '#1a1a1a' }}>
|
|
便签
|
|
</Text>
|
|
</Group>
|
|
<Group gap="sm">
|
|
<Button
|
|
size="xs"
|
|
variant="outline"
|
|
leftSection={<IconDeviceFloppy size={12} />}
|
|
onClick={handleManualSave}
|
|
loading={saving}
|
|
style={{
|
|
borderColor: '#ccc',
|
|
color: '#1a1a1a',
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
保存
|
|
</Button>
|
|
<Text size="xs" c={saving ? '#666' : lastSaved ? '#999' : '#bbb'}>
|
|
{saving ? '保存中...' : formatLastSaved()}
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
|
|
{/* Editor/Preview Area */}
|
|
<Box style={{ flex: 1, minHeight: 0, display: 'flex' }}>
|
|
{viewMode === 'edit' && (
|
|
<Textarea
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
placeholder="在这里记录你的想法... 支持 Markdown 语法: - # 一级标题 - **粗体** - *斜体* - [链接](url)"
|
|
autosize
|
|
minRows={8}
|
|
styles={{
|
|
input: {
|
|
border: 'none',
|
|
resize: 'none',
|
|
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
fontSize: '13px',
|
|
lineHeight: '1.6',
|
|
background: 'transparent',
|
|
'&:focus': { outline: 'none' },
|
|
},
|
|
}}
|
|
style={{ flex: 1, width: '100%' }}
|
|
/>
|
|
)}
|
|
|
|
{viewMode === 'preview' && (
|
|
<Box
|
|
style={{
|
|
flex: 1,
|
|
overflowY: 'auto',
|
|
padding: '8px',
|
|
fontSize: '13px',
|
|
lineHeight: '1.6',
|
|
}}
|
|
>
|
|
{content ? (
|
|
<Box
|
|
style={{
|
|
'& h1, & h2, & h3': {
|
|
marginTop: '1em',
|
|
marginBottom: '0.5em',
|
|
fontWeight: 500,
|
|
},
|
|
'& h1': { fontSize: '1.25em', borderBottom: '1px solid rgba(0,0,0,0.08)', paddingBottom: '0.3em' },
|
|
'& h2': { fontSize: '1.1em', borderBottom: '1px solid rgba(0,0,0,0.06)', paddingBottom: '0.3em' },
|
|
'& p': { marginBottom: '0.8em', color: '#333' },
|
|
'& ul, & ol': { paddingLeft: '1.5em', marginBottom: '0.8em' },
|
|
'& li': { marginBottom: '0.3em', color: '#333' },
|
|
'& blockquote': {
|
|
borderLeft: '3px solid rgba(0,0,0,0.1)',
|
|
paddingLeft: '1em',
|
|
marginLeft: 0,
|
|
color: '#888',
|
|
},
|
|
'& code': {
|
|
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
padding: '0.2em 0.4em',
|
|
borderRadius: '2px',
|
|
fontSize: '0.9em',
|
|
},
|
|
'& pre': {
|
|
backgroundColor: 'rgba(0,0,0,0.03)',
|
|
padding: '1em',
|
|
borderRadius: '2px',
|
|
overflow: 'auto',
|
|
},
|
|
'& pre code': {
|
|
backgroundColor: 'transparent',
|
|
padding: 0,
|
|
},
|
|
'& a': {
|
|
color: '#1a1a1a',
|
|
textDecoration: 'underline',
|
|
textUnderlineOffset: 2,
|
|
},
|
|
'& hr': {
|
|
border: 'none',
|
|
borderTop: '1px solid rgba(0,0,0,0.06)',
|
|
margin: '1em 0',
|
|
},
|
|
} as any}
|
|
>
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</Box>
|
|
) : (
|
|
<Text c="#999" ta="center" py="xl" size="sm" style={{ letterSpacing: '0.05em' }}>
|
|
暂无内容
|
|
<br />
|
|
<Text size="xs" c="#bbb" mt={4}>
|
|
使用 Markdown 格式记录你的想法
|
|
</Text>
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|