feat: React 重构前端(组件化,清晰逻辑)

This commit is contained in:
maoshen
2026-04-15 02:39:18 +00:00
parent c8178ce98f
commit 75423d4e0e
9 changed files with 712 additions and 0 deletions

76
frontend-react/README.md Normal file
View File

@@ -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/` 对应文档。

View File

@@ -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"
]
}
}

100
frontend-react/src/App.js Normal file
View File

@@ -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 <div className="loading">加载中...</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div className="container">
<header>
<h1> 码神的日记系统</h1>
<p>记录每天的进步与成长</p>
</header>
<StatsPanel stats={stats} />
<div className="grid-2">
<div className="section-box">
<h2>📅 日历</h2>
<Calendar
selectedDate={selectedDate}
onDateSelect={handleDateSelect}
diaryDates={diaryDates}
/>
</div>
<div className="section-box">
<h2>📝 {selectedDate} 的日记</h2>
<DiaryDetail entry={selectedEntry} />
</div>
</div>
<div className="section-box">
<h2>💡 经验总结</h2>
<ExperienceList experiences={experiences} />
</div>
</div>
);
}
export default App;

View File

@@ -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(<div key={`empty-${i}`} className="calendar-day empty"></div>);
}
// 当月日期
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(
<div
key={day}
className={`calendar-day ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${hasDiary ? 'has-diary' : ''}`}
onClick={() => onDateSelect(dateStr)}
>
{day}
{hasDiary && <span className="diary-marker">📝</span>}
</div>
);
}
return days;
};
return (
<div className="calendar">
<div className="calendar-header">
<button onClick={prevMonth}>&#8592;</button>
<span>{year} {monthNames[month]}</span>
<button onClick={nextMonth}>&#8594;</button>
</div>
<div className="calendar-grid">
{dayNames.map(day => (
<div key={day} className="calendar-day-header">{day}</div>
))}
{renderDays()}
</div>
</div>
);
}
export default Calendar;

View File

@@ -0,0 +1,65 @@
import React from 'react';
/**
* 日记详情组件
* 显示选中日期的日记内容
*/
function DiaryDetail({ entry }) {
if (!entry) {
return (
<div className="empty-state">
<p>这一天还没有日记</p>
</div>
);
}
return (
<div className="diary-detail">
<h3>{entry.title || '每日日记'}</h3>
{entry.completed_tasks && (
<div className="diary-section">
<div className="section-title"> 完成的任务</div>
<div className="section-content">{entry.completed_tasks}</div>
</div>
)}
{entry.learned && (
<div className="diary-section">
<div className="section-title">📚 学到的东西</div>
<div className="section-content">{entry.learned}</div>
</div>
)}
{entry.problems && (
<div className="diary-section">
<div className="section-title">🐛 遇到的问题</div>
<div className="section-content">{entry.problems}</div>
</div>
)}
{entry.reflections && (
<div className="diary-section">
<div className="section-title">💡 想法和反思</div>
<div className="section-content">{entry.reflections}</div>
</div>
)}
{entry.improvements && (
<div className="diary-section">
<div className="section-title">📈 进步点</div>
<div className="section-content">{entry.improvements}</div>
</div>
)}
{entry.plans && (
<div className="diary-section">
<div className="section-title">🎯 明日计划</div>
<div className="section-content">{entry.plans}</div>
</div>
)}
</div>
);
}
export default DiaryDetail;

View File

@@ -0,0 +1,39 @@
import React from 'react';
/**
* 经验总结列表组件
*/
function ExperienceList({ experiences }) {
if (!experiences || experiences.length === 0) {
return <div className="empty-state">暂无经验总结</div>;
}
return (
<div className="experience-list">
{experiences.map(exp => (
<div key={exp.id} className="experience-item">
<div className="experience-header">
<span className="experience-title">{exp.title}</span>
<span className="experience-category">{exp.category}</span>
</div>
<div className="experience-problem">
<div className="experience-problem-title">🐛 问题</div>
<div>{exp.problem}</div>
</div>
<div className="experience-solution">
<div className="experience-solution-title"> 解决方案</div>
<div>{exp.solution}</div>
</div>
{exp.lesson_learned && (
<div className="experience-lesson">
<div className="experience-lesson-title">📌 经验教训</div>
<div>{exp.lesson_learned}</div>
</div>
)}
</div>
))}
</div>
);
}
export default ExperienceList;

View File

@@ -0,0 +1,38 @@
import React from 'react';
/**
* 统计面板组件
* 显示系统统计数据
*/
function StatsPanel({ stats }) {
return (
<div className="stats">
<div className="stat-card">
<h3>{stats.total_entries || 0}</h3>
<p>总日记</p>
</div>
<div className="stat-card">
<h3>{stats.total_tasks || 0}</h3>
<p>总任务</p>
</div>
<div className="stat-card">
<h3>{stats.progressing || 0}</h3>
<p>进行中</p>
</div>
<div className="stat-card">
<h3>{stats.completed || 0}</h3>
<p>已完成</p>
</div>
<div className="stat-card">
<h3>{stats.completion_rate || 0}%</h3>
<p>完成率</p>
</div>
<div className="stat-card">
<h3>{stats.total_experiences || 0}</h3>
<p>经验</p>
</div>
</div>
);
}
export default StatsPanel;

View File

@@ -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;
}
}

View File

@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);