diff --git a/frontend-react/README.md b/frontend-react/README.md new file mode 100644 index 0000000..35ed414 --- /dev/null +++ b/frontend-react/README.md @@ -0,0 +1,76 @@ +# React 前端重构说明 + +## 📁 项目结构 + +``` +frontend-react/ +├── src/ +│ ├── components/ +│ │ ├── Calendar.js # ⭐ 日历组件(核心) +│ │ ├── DiaryDetail.js # 日记详情 +│ │ ├── ExperienceList.js # 经验总结列表 +│ │ └── StatsPanel.js # 统计面板 +│ ├── App.js # 主应用 +│ ├── index.js # 入口 +│ └── index.css # 样式 +├── package.json +└── README.md +``` + +## 🎯 组件化优势 + +### 之前的 HTML/JS 大杂烩 +- ❌ 所有逻辑混在一个文件 +- ❌ 难以维护 +- ❌ 看不清功能边界 +- ❌ 容易误删核心功能 + +### 现在的 React 组件 +- ✅ 每个功能独立组件 +- ✅ 清晰的依赖关系 +- ✅ 易于测试和维护 +- ✅ 组件级别注释保护 + +## 🔧 开发指南 + +### 修改日历功能 +```bash +# 只需修改这个文件 +src/components/Calendar.js + +# 阅读文档 +docs/CALENDAR.md + +# 测试 +npm start +``` + +### 添加新功能 +1. 在 `components/` 创建新组件 +2. 在 `App.js` 中引入 +3. 运行测试 + +## 🚀 构建部署 + +```bash +# 安装依赖 +npm install + +# 开发模式 +npm start + +# 生产构建 +npm run build + +# 部署构建产物 +cp -r build/* ../frontend/ +``` + +## ⚠️ 核心组件 + +**不可删除的组件:** +- `Calendar.js` - 日历组件 ⭐⭐⭐ +- `App.js` - 主应用 ⭐⭐ +- `StatsPanel.js` - 统计面板 ⭐ + +修改前必须阅读 `docs/` 对应文档。 diff --git a/frontend-react/package.json b/frontend-react/package.json new file mode 100644 index 0000000..bb7fb83 --- /dev/null +++ b/frontend-react/package.json @@ -0,0 +1,29 @@ +{ + "name": "diary-system-react", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "axios": "^1.6.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/frontend-react/src/App.js b/frontend-react/src/App.js new file mode 100644 index 0000000..33915ea --- /dev/null +++ b/frontend-react/src/App.js @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import StatsPanel from './components/StatsPanel'; +import Calendar from './components/Calendar'; +import DiaryDetail from './components/DiaryDetail'; +import ExperienceList from './components/ExperienceList'; + +const API_BASE = '/api'; + +/** + * ⚠️ 主应用组件 + * + * 核心功能: + * - 统计面板 + * - 日历组件 ⭐ + * - 日记详情 + * - 经验总结 + * + * ⚠️ 修改前阅读 docs/README.md + */ +function App() { + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + const [diaryDates, setDiaryDates] = useState([]); + const [entries, setEntries] = useState([]); + const [experiences, setExperiences] = useState([]); + const [stats, setStats] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const [statsRes, entriesRes, expRes] = await Promise.all([ + axios.get(`${API_BASE}/entries/stats/`), + axios.get(`${API_BASE}/entries/`), + axios.get(`${API_BASE}/experiences/recent/`) + ]); + + setStats(statsRes.data); + setEntries(entriesRes.data); + setExperiences(expRes.data); + setDiaryDates(entriesRes.data.map(e => e.date)); + setLoading(false); + } catch (err) { + setError(`加载失败:${err.message}`); + setLoading(false); + } + }; + + const handleDateSelect = (date) => { + setSelectedDate(date); + }; + + const selectedEntry = entries.find(e => e.date === selectedDate); + + if (loading) { + return
加载中...
; + } + + if (error) { + return
{error}
; + } + + return ( +
+
+

⚡ 码神的日记系统

+

记录每天的进步与成长

+
+ + + +
+
+

📅 日历

+ +
+ +
+

📝 {selectedDate} 的日记

+ +
+
+ +
+

💡 经验总结

+ +
+
+ ); +} + +export default App; diff --git a/frontend-react/src/components/Calendar.js b/frontend-react/src/components/Calendar.js new file mode 100644 index 0000000..cca2074 --- /dev/null +++ b/frontend-react/src/components/Calendar.js @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; + +/** + * ⚠️ 核心组件:日历组件 + * + * 功能: + * - 显示月历视图 + * - 标记有日记的日期 + * - 点击日期选择 + * - 上月/下月切换 + * + * ⚠️ 不可删除此组件,修改前阅读 docs/CALENDAR.md + */ +function Calendar({ selectedDate, onDateSelect, diaryDates }) { + const [currentDate, setCurrentDate] = useState(new Date()); + + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const startDay = firstDay.getDay(); + const totalDays = lastDay.getDate(); + + const today = new Date().toISOString().split('T')[0]; + const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']; + const dayNames = ['日', '一', '二', '三', '四', '五', '六']; + + const prevMonth = () => { + setCurrentDate(new Date(year, month - 1, 1)); + }; + + const nextMonth = () => { + setCurrentDate(new Date(year, month + 1, 1)); + }; + + const renderDays = () => { + let days = []; + + // 上个月的日期 + for (let i = 0; i < startDay; i++) { + days.push(
); + } + + // 当月日期 + for (let day = 1; day <= totalDays; day++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const isToday = dateStr === today; + const isSelected = dateStr === selectedDate; + const hasDiary = diaryDates.includes(dateStr); + + days.push( +
onDateSelect(dateStr)} + > + {day} + {hasDiary && 📝} +
+ ); + } + + return days; + }; + + return ( +
+
+ + {year}年 {monthNames[month]} + +
+
+ {dayNames.map(day => ( +
{day}
+ ))} + {renderDays()} +
+
+ ); +} + +export default Calendar; diff --git a/frontend-react/src/components/DiaryDetail.js b/frontend-react/src/components/DiaryDetail.js new file mode 100644 index 0000000..a4c766b --- /dev/null +++ b/frontend-react/src/components/DiaryDetail.js @@ -0,0 +1,65 @@ +import React from 'react'; + +/** + * 日记详情组件 + * 显示选中日期的日记内容 + */ +function DiaryDetail({ entry }) { + if (!entry) { + return ( +
+

这一天还没有日记

+
+ ); + } + + return ( +
+

{entry.title || '每日日记'}

+ + {entry.completed_tasks && ( +
+
✅ 完成的任务
+
{entry.completed_tasks}
+
+ )} + + {entry.learned && ( +
+
📚 学到的东西
+
{entry.learned}
+
+ )} + + {entry.problems && ( +
+
🐛 遇到的问题
+
{entry.problems}
+
+ )} + + {entry.reflections && ( +
+
💡 想法和反思
+
{entry.reflections}
+
+ )} + + {entry.improvements && ( +
+
📈 进步点
+
{entry.improvements}
+
+ )} + + {entry.plans && ( +
+
🎯 明日计划
+
{entry.plans}
+
+ )} +
+ ); +} + +export default DiaryDetail; diff --git a/frontend-react/src/components/ExperienceList.js b/frontend-react/src/components/ExperienceList.js new file mode 100644 index 0000000..69a0a27 --- /dev/null +++ b/frontend-react/src/components/ExperienceList.js @@ -0,0 +1,39 @@ +import React from 'react'; + +/** + * 经验总结列表组件 + */ +function ExperienceList({ experiences }) { + if (!experiences || experiences.length === 0) { + return
暂无经验总结
; + } + + return ( +
+ {experiences.map(exp => ( +
+
+ {exp.title} + {exp.category} +
+
+
🐛 问题
+
{exp.problem}
+
+
+
✅ 解决方案
+
{exp.solution}
+
+ {exp.lesson_learned && ( +
+
📌 经验教训
+
{exp.lesson_learned}
+
+ )} +
+ ))} +
+ ); +} + +export default ExperienceList; diff --git a/frontend-react/src/components/StatsPanel.js b/frontend-react/src/components/StatsPanel.js new file mode 100644 index 0000000..54dba61 --- /dev/null +++ b/frontend-react/src/components/StatsPanel.js @@ -0,0 +1,38 @@ +import React from 'react'; + +/** + * 统计面板组件 + * 显示系统统计数据 + */ +function StatsPanel({ stats }) { + return ( +
+
+

{stats.total_entries || 0}

+

总日记

+
+
+

{stats.total_tasks || 0}

+

总任务

+
+
+

{stats.progressing || 0}

+

进行中

+
+
+

{stats.completed || 0}

+

已完成

+
+
+

{stats.completion_rate || 0}%

+

完成率

+
+
+

{stats.total_experiences || 0}

+

经验

+
+
+ ); +} + +export default StatsPanel; diff --git a/frontend-react/src/index.css b/frontend-react/src/index.css new file mode 100644 index 0000000..7a015f6 --- /dev/null +++ b/frontend-react/src/index.css @@ -0,0 +1,270 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; +} + +header p { + opacity: 0.9; +} + +/* 统计面板 */ +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: white; + padding: 15px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.stat-card h3 { + color: #667eea; + font-size: 1.8em; + margin-bottom: 5px; +} + +.stat-card p { + color: #666; + font-size: 0.9em; +} + +/* 内容区域 */ +.grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 30px; + margin-bottom: 30px; +} + +.section-box { + background: white; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.section-box h2 { + color: #333; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #667eea; +} + +/* 日历组件 */ +.calendar { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.calendar-header button { + background: #667eea; + color: white; + border: none; + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; +} + +.calendar-header button:hover { + background: #5568d3; +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +.calendar-day-header { + text-align: center; + font-weight: bold; + color: #666; + padding: 5px; + font-size: 0.9em; +} + +.calendar-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + background: white; + border-radius: 5px; + cursor: pointer; + position: relative; + transition: all 0.2s; +} + +.calendar-day:hover { + background: #eef1f5; + transform: scale(1.05); +} + +.calendar-day.today { + background: #667eea; + color: white; + font-weight: bold; +} + +.calendar-day.selected { + border: 2px solid #667eea; +} + +.calendar-day.has-diary::after { + content: '📝'; + font-size: 0.7em; + position: absolute; + bottom: 2px; +} + +.calendar-day.empty { + cursor: default; + background: transparent; +} + +/* 日记详情 */ +.diary-detail { + padding: 15px; +} + +.diary-section { + margin: 15px 0; +} + +.section-title { + font-weight: bold; + color: #555; + margin-bottom: 8px; +} + +.section-content { + color: #666; + line-height: 1.6; + white-space: pre-wrap; + margin-left: 20px; +} + +/* 经验总结 */ +.experience-item { + padding: 15px; + border-left: 4px solid #f59e0b; + background: #fffbeb; + margin-bottom: 15px; + border-radius: 5px; +} + +.experience-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.experience-title { + font-weight: bold; + color: #333; +} + +.experience-category { + background: #f59e0b; + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85em; +} + +.experience-problem, .experience-solution { + margin: 10px 0; +} + +.experience-problem-title { + font-weight: bold; + color: #dc2626; + margin-bottom: 5px; +} + +.experience-solution-title { + font-weight: bold; + color: #059669; + margin-bottom: 5px; +} + +.experience-lesson { + margin: 10px 0; + padding: 10px; + background: #fef3c7; + border-radius: 5px; +} + +.experience-lesson-title { + font-weight: bold; + color: #92400e; + margin-bottom: 5px; +} + +/* 状态 */ +.loading { + text-align: center; + padding: 40px; + color: white; + font-size: 1.2em; +} + +.error { + background: #fee; + color: #c00; + padding: 20px; + border-radius: 10px; + margin-bottom: 20px; +} + +.empty-state { + text-align: center; + padding: 40px; + color: #999; +} + +/* 移动端 */ +@media (max-width: 768px) { + .grid-2 { + grid-template-columns: 1fr; + } +} diff --git a/frontend-react/src/index.js b/frontend-react/src/index.js new file mode 100644 index 0000000..2cb1087 --- /dev/null +++ b/frontend-react/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +);