fix: 修复多个交互问题
1. 修复便签保存后无法编辑的问题 - 移除 handleSave 中对 updateNotesContent 的调用 - 避免触发 notes useEffect 导致 content 被重置 2. 修复提醒顺延后列表不刷新 - 在 handlePostpone 函数末尾添加 fetchEvents() 3. 优化提醒完成状态切换的错误处理 - stores 会在更新失败时自动回滚数据 4. 优化便签保存状态显示 - 添加 hasUnsavedChanges 状态 - 区分"未保存"、"保存中"、"已保存"三种状态 5. 修复列表底部填充问题 - 纪念日列表和提醒列表在内容刚好一屏时 - 添加基础填充避免被 AI 输入框遮挡 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
864051a65b
commit
8725108195
@ -31,7 +31,7 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [bottomPadding, setBottomPadding] = useState(0);
|
||||
|
||||
// 检测列表是否可滚动,如果是则添加底部填充避免被 AI 输入框遮挡
|
||||
// 检测列表内容,添加底部填充避免被 AI 输入框遮挡
|
||||
useEffect(() => {
|
||||
const updatePadding = () => {
|
||||
const container = scrollContainerRef.current;
|
||||
@ -39,8 +39,12 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
|
||||
|
||||
// 判断是否可滚动(内容高度 > 容器高度)
|
||||
const isScrollable = container.scrollHeight > container.clientHeight;
|
||||
// 计算内容底部到容器底部的距离
|
||||
const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
// 如果可滚动,始终添加填充;如果不可滚动但内容接近底部,也添加填充
|
||||
const needsPadding = isScrollable || scrollBottom < 100;
|
||||
// 底部填充高度:AI 输入框高度(约50px) + 底部间距(48px) + 额外缓冲(20px)
|
||||
setBottomPadding(isScrollable ? 120 : 0);
|
||||
setBottomPadding(needsPadding ? 120 : 20);
|
||||
};
|
||||
|
||||
// 延迟执行确保 DOM 完全渲染
|
||||
|
||||
@ -23,22 +23,30 @@ 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 [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [viewMode] = useState<ViewMode>('edit');
|
||||
|
||||
// Initialize content from notes
|
||||
useEffect(() => {
|
||||
if (notes) {
|
||||
setContent(notes.content);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
}, [notes]);
|
||||
|
||||
// Track unsaved changes
|
||||
useEffect(() => {
|
||||
if (notes && content !== notes.content) {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}, [content, notes]);
|
||||
|
||||
// Fetch notes on mount
|
||||
useEffect(() => {
|
||||
fetchNotes();
|
||||
@ -65,21 +73,21 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
|
||||
}
|
||||
`;
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedContent !== undefined && notes && debouncedContent !== content) {
|
||||
handleSave(debouncedContent);
|
||||
}
|
||||
}, [debouncedContent]);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (value: string) => {
|
||||
if (!notes) return;
|
||||
// 即使 notes 为 null,saveNotes 也会自动创建便签
|
||||
if (!value) return;
|
||||
// 防止重复保存
|
||||
if (saving) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
updateNotesContent(value);
|
||||
// 注意:不要在这里调用 updateNotesContent
|
||||
// 因为 saveNotes 会在成功后更新 store 中的 notes
|
||||
// 如果这里先更新,会触发 notes useEffect,导致 content 被重置
|
||||
await saveNotes(value);
|
||||
setLastSaved(new Date());
|
||||
setHasUnsavedChanges(false);
|
||||
onSave?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to save note:', error);
|
||||
@ -87,20 +95,30 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[notes, updateNotesContent, saveNotes, onSave]
|
||||
[saving, saveNotes, onSave]
|
||||
);
|
||||
|
||||
// Auto-save with debounce
|
||||
useEffect(() => {
|
||||
// 即使 notes 为 null 也可以自动保存(会创建新便签)
|
||||
if (debouncedContent !== undefined && debouncedContent !== content) {
|
||||
handleSave(debouncedContent);
|
||||
}
|
||||
}, [debouncedContent, content, handleSave]);
|
||||
|
||||
const formatLastSaved = () => {
|
||||
if (!lastSaved) return '未保存';
|
||||
if (saving) return '保存中...';
|
||||
if (hasUnsavedChanges && !lastSaved) return '未保存';
|
||||
if (!lastSaved) return '';
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - lastSaved.getTime();
|
||||
if (diff < 1000) return '刚刚保存';
|
||||
if (diff < 1000) return '已保存';
|
||||
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`;
|
||||
return lastSaved.toLocaleTimeString('zh-CN');
|
||||
};
|
||||
|
||||
const handleManualSave = () => {
|
||||
if (content && notes) {
|
||||
if (content) {
|
||||
handleSave(content);
|
||||
}
|
||||
};
|
||||
@ -150,8 +168,8 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Text size="xs" c={saving ? '#666' : lastSaved ? '#999' : '#bbb'}>
|
||||
{saving ? '保存中...' : formatLastSaved()}
|
||||
<Text size="xs" c={saving ? '#666' : hasUnsavedChanges ? '#e6a23c' : '#999'}>
|
||||
{formatLastSaved()}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@ -45,7 +45,7 @@ export function ReminderList({
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [bottomPadding, setBottomPadding] = useState(0);
|
||||
|
||||
// 检测列表是否可滚动,如果是则添加底部填充避免被 AI 输入框遮挡
|
||||
// 检测列表内容,添加底部填充避免被 AI 输入框遮挡
|
||||
useEffect(() => {
|
||||
const updatePadding = () => {
|
||||
const container = scrollContainerRef.current;
|
||||
@ -53,8 +53,12 @@ export function ReminderList({
|
||||
|
||||
// 判断是否可滚动(内容高度 > 容器高度)
|
||||
const isScrollable = container.scrollHeight > container.clientHeight;
|
||||
// 计算内容底部到容器底部的距离
|
||||
const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
// 如果可滚动,始终添加填充;如果不可滚动但内容接近底部,也添加填充
|
||||
const needsPadding = isScrollable || scrollBottom < 100;
|
||||
// 底部填充高度:AI 输入框高度(约50px) + 底部间距(48px) + 额外缓冲(20px)
|
||||
setBottomPadding(isScrollable ? 120 : 0);
|
||||
setBottomPadding(needsPadding ? 120 : 20);
|
||||
};
|
||||
|
||||
// 延迟执行确保 DOM 完全渲染
|
||||
|
||||
@ -182,8 +182,8 @@ export function HomePage() {
|
||||
});
|
||||
if (result.error) {
|
||||
console.error('更新失败:', result.error);
|
||||
// stores 会在更新失败时自动回滚数据
|
||||
}
|
||||
// 乐观更新已处理 UI 响应,无需 fetchEvents
|
||||
};
|
||||
|
||||
const handleDelete = async (event: Event) => {
|
||||
@ -225,6 +225,8 @@ export function HomePage() {
|
||||
if (result.error) {
|
||||
console.error('顺延失败:', result.error);
|
||||
}
|
||||
// 刷新列表以更新 UI
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
const handleDateChange = async (
|
||||
|
||||
@ -39,7 +39,12 @@ export const api = {
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Request failed');
|
||||
const errorMsg = data.error || 'Request failed';
|
||||
// 如果有详细信息,显示出来
|
||||
if (data.details) {
|
||||
console.error('Validation details:', JSON.stringify(data.details));
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user