diff --git a/code/backend/api/urls.py b/code/backend/api/urls.py index 1079ca1..4daa37f 100644 --- a/code/backend/api/urls.py +++ b/code/backend/api/urls.py @@ -10,5 +10,7 @@ urlpatterns = [ path('lobsters//memory/', views.lobster_memory, name='lobster-memory'), path('lobsters//memory/dates/', views.lobster_memory_dates, name='lobster-memory-dates'), path('lobsters//memory//', views.lobster_memory_detail, name='lobster-memory-detail'), + path('lobsters//diary/dates/', views.lobster_diary_dates, name='lobster-diary-dates'), + path('lobsters//diary//', views.lobster_diary_detail, name='lobster-diary-detail'), path('tools/', views.tools_list, name='tools-list'), ] diff --git a/code/backend/api/views.py b/code/backend/api/views.py index db0a35a..c9ab96a 100644 --- a/code/backend/api/views.py +++ b/code/backend/api/views.py @@ -113,3 +113,41 @@ def lobster_memory_detail(request, lobster_id, date): 'date': date, 'content': content }) + +@api_view(['GET']) +def lobster_diary_dates(request, lobster_id): + """获取龙虾有日记(成才之路)的日期列表""" + # 日记文件目录 + diary_dir = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路') + + # 获取所有日记文件 + dates = [] + if diary_dir.exists(): + for file in diary_dir.glob('*.md'): + # 提取日期 (YYYY-MM-DD.md 或 YYYY-MM-DD-*.md) + match = re.match(r'(\d{4}-\d{2}-\d{2})(?:-.*)?\.md', file.name) + if match: + dates.append(match.group(1)) + + dates = list(set(dates)) # 去重 + dates.sort(reverse=True) + return Response({'dates': dates}) + +@api_view(['GET']) +def lobster_diary_detail(request, lobster_id, date): + """获取指定日期的日记内容(成才之路)""" + # 优先查找故事版,其次技术版,再其次普通版 + diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-故事版.md') + if not diary_file.exists(): + diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-技术版.md') + if not diary_file.exists(): + diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}.md') + + if not diary_file.exists(): + return Response({'error': '该日期没有日记'}, status=404) + + content = diary_file.read_text(encoding='utf-8') + return Response({ + 'date': date, + 'content': content + }) diff --git a/code/frontend/src/components/MemoryModal/index.js b/code/frontend/src/components/MemoryModal/index.js index 586e135..66d8085 100644 --- a/code/frontend/src/components/MemoryModal/index.js +++ b/code/frontend/src/components/MemoryModal/index.js @@ -3,35 +3,72 @@ import React, { useState, useEffect } from 'react'; const API_BASE = 'http://localhost:8000/api'; function MemoryModal({ lobsterId, lobsterName, onClose }) { + const [activeTab, setActiveTab] = useState('diary'); // 'memory' 或 'diary' const [dates, setDates] = useState([]); + const [diaryDates, setDiaryDates] = useState([]); const [selectedDate, setSelectedDate] = useState(''); const [content, setContent] = useState(''); const [currentMonth, setCurrentMonth] = useState(new Date()); + const [loading, setLoading] = useState(false); - // 加载有日记的日期 + // 加载记忆和日记的日期 useEffect(() => { - fetch(`${API_BASE}/lobsters/${lobsterId}/memory/dates/`) - .then(r => r.json()) - .then(data => { - setDates(data.dates); - if (data.dates.length > 0) { + loadDates(); + }, [lobsterId, activeTab]); + + const loadDates = async () => { + setLoading(true); + try { + if (activeTab === 'memory') { + // 加载记忆日期(每日记忆文件) + const response = await fetch(`${API_BASE}/lobsters/${lobsterId}/memory/dates/`); + const data = await response.json(); + setDates(data.dates || []); + if (data.dates && data.dates.length > 0) { setSelectedDate(data.dates[0]); } - }); - }, [lobsterId]); + } else { + // 加载日记日期(成才之路) + const response = await fetch(`${API_BASE}/lobsters/${lobsterId}/diary/dates/`); + const data = await response.json(); + setDiaryDates(data.dates || []); + if (data.dates && data.dates.length > 0) { + setSelectedDate(data.dates[0]); + } + } + } catch (error) { + console.error('加载日期失败:', error); + } finally { + setLoading(false); + } + }; - // 加载选中日期的日记 + // 加载选中日期的内容 useEffect(() => { if (selectedDate) { - fetch(`${API_BASE}/lobsters/${lobsterId}/memory/${selectedDate}/`) - .then(r => r.json()) - .then(data => { - if (data.content) { - setContent(data.content); - } - }); + loadContent(selectedDate); } - }, [selectedDate, lobsterId]); + }, [selectedDate, activeTab, lobsterId]); + + const loadContent = async (date) => { + setLoading(true); + try { + if (activeTab === 'memory') { + const response = await fetch(`${API_BASE}/lobsters/${lobsterId}/memory/${date}/`); + const data = await response.json(); + setContent(data.content || ''); + } else { + const response = await fetch(`${API_BASE}/lobsters/${lobsterId}/diary/${date}/`); + const data = await response.json(); + setContent(data.content || ''); + } + } catch (error) { + console.error('加载内容失败:', error); + setContent(''); + } finally { + setLoading(false); + } + }; // 渲染日历 const renderCalendar = () => { @@ -43,6 +80,7 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { const startWeekday = firstDay.getDay(); const days = []; + const hasContentDates = activeTab === 'memory' ? dates : diaryDates; // 空白格子 for (let i = 0; i < startWeekday; i++) { @@ -52,14 +90,14 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { // 日期格子 for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const hasMemory = dates.includes(dateStr); + const hasContent = hasContentDates.includes(dateStr); const isSelected = selectedDate === dateStr; days.push(
hasMemory && setSelectedDate(dateStr)} + className={`calendar-day ${hasContent ? 'has-memory' : ''} ${isSelected ? 'selected' : ''}`} + onClick={() => hasContent && setSelectedDate(dateStr)} > {day}
@@ -73,25 +111,47 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + delta))); }; + const currentDates = activeTab === 'memory' ? dates : diaryDates; + const title = activeTab === 'memory' ? '📔 工作记忆' : '📖 成才之路'; + const emptyText = activeTab === 'memory' ? '这一天还没有工作记忆' : '这一天还没有日记'; + return (
e.stopPropagation()}>
-

📔 {lobsterName} - 工作日记

+

{title} - {lobsterName}

+ {/* 切换标签 */} +
+ + +
+
- {/* 日记内容 */} + {/* 内容区域 */}
- {content ? ( + {loading ? ( +
加载中...
+ ) : content ? (

📅 {selectedDate}

{content}
) : (
-

这一天还没有日记

+

{emptyText}

)}
@@ -110,8 +170,15 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { {renderCalendar()}
- ● 有日记 - ○ 无日记 + ● 有内容 + ○ 无内容 +
+
+ + 📅 本月 {currentDates.filter(d => + d.startsWith(`${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`) + ).length} 篇 +
@@ -153,6 +220,7 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { .memory-modal-header h2 { color: #1a365d; margin: 0; + font-size: 1.4em; } .close-btn { @@ -167,6 +235,35 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { color: #2d3748; } + .tab-header { + display: flex; + border-bottom: 2px solid #e2e8f0; + } + + .tab-btn { + flex: 1; + padding: 15px 20px; + background: #f7fafc; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + font-size: 1em; + font-weight: 600; + color: #718096; + transition: all 0.2s; + } + + .tab-btn:hover { + background: #edf2f7; + color: #4a5568; + } + + .tab-btn.active { + background: white; + color: #553c9a; + border-bottom-color: #553c9a; + } + .memory-modal-body { display: grid; grid-template-columns: 1fr 350px; @@ -184,6 +281,15 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { max-height: 600px; } + .loading-state { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: #718096; + font-size: 1.1em; + } + .memory-text h3 { color: #553c9a; margin-bottom: 15px; @@ -204,6 +310,7 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { justify-content: center; height: 100%; color: #a0aec0; + font-size: 1.1em; } .memory-calendar-panel { @@ -211,6 +318,8 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { border-radius: 8px; padding: 15px; background: white; + display: flex; + flex-direction: column; } .calendar-header { @@ -237,6 +346,7 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; + flex: 1; } .calendar-weekday { @@ -279,10 +389,12 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { } .calendar-legend { - margin-top: 15px; + margin-top: 10px; display: flex; gap: 20px; font-size: 0.85em; + padding-top: 10px; + border-top: 1px solid #e2e8f0; } .legend-item { @@ -301,6 +413,23 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { color: #a0aec0; } + .calendar-stats { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #e2e8f0; + display: flex; + justify-content: center; + } + + .stat-badge { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.9em; + font-weight: 600; + } + @media (max-width: 768px) { .memory-modal-body { grid-template-columns: 1fr; @@ -309,6 +438,11 @@ function MemoryModal({ lobsterId, lobsterName, onClose }) { .memory-calendar-panel { order: -1; } + + .tab-btn { + padding: 12px 15px; + font-size: 0.9em; + } } `} diff --git a/code/frontend/src/pages/Dashboard/index.js b/code/frontend/src/pages/Dashboard/index.js index 80d2d75..dc0bbae 100644 --- a/code/frontend/src/pages/Dashboard/index.js +++ b/code/frontend/src/pages/Dashboard/index.js @@ -49,7 +49,7 @@ function Dashboard() { 📊 详情