feat: React 重构前端(组件化,清晰逻辑)
This commit is contained in:
76
frontend-react/README.md
Normal file
76
frontend-react/README.md
Normal 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/` 对应文档。
|
||||
29
frontend-react/package.json
Normal file
29
frontend-react/package.json
Normal 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
100
frontend-react/src/App.js
Normal 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;
|
||||
84
frontend-react/src/components/Calendar.js
Normal file
84
frontend-react/src/components/Calendar.js
Normal 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}>←</button>
|
||||
<span>{year}年 {monthNames[month]}</span>
|
||||
<button onClick={nextMonth}>→</button>
|
||||
</div>
|
||||
<div className="calendar-grid">
|
||||
{dayNames.map(day => (
|
||||
<div key={day} className="calendar-day-header">{day}</div>
|
||||
))}
|
||||
{renderDays()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Calendar;
|
||||
65
frontend-react/src/components/DiaryDetail.js
Normal file
65
frontend-react/src/components/DiaryDetail.js
Normal 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;
|
||||
39
frontend-react/src/components/ExperienceList.js
Normal file
39
frontend-react/src/components/ExperienceList.js
Normal 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;
|
||||
38
frontend-react/src/components/StatsPanel.js
Normal file
38
frontend-react/src/components/StatsPanel.js
Normal 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;
|
||||
270
frontend-react/src/index.css
Normal file
270
frontend-react/src/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
11
frontend-react/src/index.js
Normal file
11
frontend-react/src/index.js
Normal 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>
|
||||
);
|
||||
Reference in New Issue
Block a user