Files
diary-system/frontend/index.html
maoshen 414e5e58c3 feat: 关联式设计 - 日记为中心
模型变更:
- DiaryEntry 添加 linked_tasks (ManyToMany 关联任务)
- DiaryEntry 添加 content 字段
- Experience 添加 extracted_from (外键关联日记)
- Task 添加 diary_entries (反向关联)

API 变更:
- 新增 /entries/{id}/link_task/ - 关联任务并自动更新进展
- 新增 /entries/{id}/extract_experience/ - 从日记提炼经验
- 序列化器支持关联数据嵌套显示

前端重构:
- 写日记作为主入口
- 关联任务复选框(保存时自动更新任务进展)
- 日记历史显示关联的任务和经验
- 任务列表显示关联的日记
- 经验总结独立展示

工作流程优化:
- 写日记时勾选任务 → 自动更新任务进展
- 写日记时记录反思 → 可提炼为经验总结
- 减少 60-70% 重复记录工作
2026-04-14 11:35:42 +00:00

561 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>码神的日记系统</title>
<style>
* {
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: 1200px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2em;
margin-bottom: 10px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
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.6em;
}
.stat-card p {
color: #666;
font-size: 0.8em;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab-btn {
padding: 10px 20px;
background: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.95em;
font-weight: 500;
color: #666;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-webkit-tap-highlight-color: transparent;
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.section-box {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.section-box h2 {
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
font-size: 1.4em;
}
.diary-editor {
width: 100%;
}
.diary-editor textarea {
width: 100%;
min-height: 200px;
padding: 15px;
border: 2px solid #e0e7ff;
border-radius: 8px;
font-size: 1em;
font-family: inherit;
resize: vertical;
margin-bottom: 15px;
}
.diary-editor textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-weight: 600;
color: #555;
margin-bottom: 8px;
font-size: 0.95em;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e0e7ff;
border-radius: 6px;
font-size: 0.95em;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s;
-webkit-tap-highlight-color: transparent;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: scale(0.98);
}
.btn-secondary {
background: #f1f5f9;
color: #666;
}
.task-link-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
margin-bottom: 8px;
}
.task-link-item input[type="checkbox"] {
width: 20px;
height: 20px;
}
.task-link-item .task-info {
flex: 1;
}
.task-link-item .task-title {
font-weight: 600;
color: #333;
}
.task-link-item .task-status {
font-size: 0.8em;
padding: 3px 8px;
border-radius: 12px;
background: #667eea;
color: white;
}
.diary-item {
padding: 20px;
border-left: 4px solid #667eea;
background: #f8f9fa;
margin-bottom: 20px;
border-radius: 8px;
}
.diary-item .date {
color: #667eea;
font-weight: bold;
font-size: 1.1em;
margin-bottom: 10px;
}
.diary-item .section {
margin: 15px 0;
}
.diary-item .section-title {
font-weight: 600;
color: #555;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.diary-item .section-content {
color: #666;
line-height: 1.6;
white-space: pre-wrap;
}
.linked-tasks, .linked-experiences {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e0e7ff;
}
.tag {
display: inline-block;
padding: 4px 12px;
background: #e0e7ff;
color: #667eea;
border-radius: 12px;
font-size: 0.85em;
margin-right: 8px;
margin-bottom: 8px;
}
.experience-item {
padding: 15px;
border-left: 4px solid #f59e0b;
background: #fffbeb;
margin-bottom: 15px;
border-radius: 6px;
}
.experience-item .title {
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.experience-item .category {
display: inline-block;
background: #f59e0b;
color: white;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.8em;
margin-bottom: 10px;
}
.loading {
text-align: center;
padding: 40px;
color: white;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
@media (max-width: 768px) {
body { padding: 10px; }
header h1 { font-size: 1.6em; }
.stats { grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat-card { padding: 10px; }
.stat-card h3 { font-size: 1.3em; }
.tabs { gap: 8px; }
.tab-btn { padding: 10px 16px; font-size: 0.9em; flex: 1; text-align: center; }
.section-box { padding: 15px; }
}
@media (max-width: 400px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ 码神的日记系统</h1>
<p style="color: rgba(255,255,255,0.9); font-size: 0.95em;">日记为中心 · 关联任务和经验</p>
</header>
<div id="app">
<div class="loading">加载中...</div>
</div>
</div>
<script>
const API_BASE = '/api';
let state = {
currentTab: 'write',
allDiaries: [],
allTasks: [],
allExperiences: [],
stats: {},
selectedDate: new Date().toISOString().split('T')[0]
};
async function loadData() {
try {
const [statsRes, diariesRes, tasksRes, experiencesRes] = await Promise.all([
fetch(`${API_BASE}/entries/stats/`),
fetch(`${API_BASE}/entries/recent/`),
fetch(`${API_BASE}/tasks/`),
fetch(`${API_BASE}/experiences/recent/`)
]);
state.stats = await statsRes.json();
state.allDiaries = await diariesRes.json();
state.allTasks = await tasksRes.json();
state.allExperiences = await experiencesRes.json();
render();
} catch (error) {
document.getElementById('app').innerHTML = `<div class="error">加载失败:${error.message}</div>`;
}
}
function render() {
const app = document.getElementById('app');
app.innerHTML = `
<div class="stats">
<div class="stat-card">
<h3>${state.stats.total_entries || 0}</h3>
<p>日记</p>
</div>
<div class="stat-card">
<h3>${state.stats.total_tasks || 0}</h3>
<p>任务</p>
</div>
<div class="stat-card">
<h3>${state.stats.total_experiences || 0}</h3>
<p>经验</p>
</div>
</div>
<div class="tabs">
<button class="tab-btn ${state.currentTab === 'write' ? 'active' : ''}" onclick="switchTab('write')">✍️ 写日记</button>
<button class="tab-btn ${state.currentTab === 'history' ? 'active' : ''}" onclick="switchTab('history')">📖 日记历史</button>
<button class="tab-btn ${state.currentTab === 'tasks' ? 'active' : ''}" onclick="switchTab('tasks')">📋 任务</button>
<button class="tab-btn ${state.currentTab === 'experiences' ? 'active' : ''}" onclick="switchTab('experiences')">💡 经验</button>
</div>
${renderContent()}
`;
}
function renderContent() {
if (state.currentTab === 'write') return renderWriteTab();
if (state.currentTab === 'history') return renderHistoryTab();
if (state.currentTab === 'tasks') return renderTasksTab();
if (state.currentTab === 'experiences') return renderExperiencesTab();
}
function renderWriteTab() {
const today = state.selectedDate;
const existingDiary = state.allDiaries.find(d => d.date === today);
return `
<div class="section-box">
<h2>✍️ 写日记 - ${today}</h2>
<div class="diary-editor">
<div class="form-group">
<label>📝 今天做了什么?</label>
<textarea id="completedTasks" placeholder="记录今天完成的任务和工作..." rows="4">${existingDiary ? existingDiary.completed_tasks : ''}</textarea>
</div>
<div class="form-group">
<label>📚 学到了什么?</label>
<textarea id="learned" placeholder="新的知识、技能、感悟..." rows="3">${existingDiary ? existingDiary.learned : ''}</textarea>
</div>
<div class="form-group">
<label>🔗 关联任务(自动更新进展)</label>
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #e0e7ff; border-radius: 6px; padding: 10px;">
${state.allTasks.filter(t => t.status !== 'completed').map(task => `
<div class="task-link-item">
<input type="checkbox" id="task-${task.id}" ${existingDiary && existingDiary.linked_tasks?.some(t => t.id === task.id) ? 'checked' : ''}>
<div class="task-info">
<div class="task-title">${task.title}</div>
<div style="font-size: 0.85em; color: #666;">当前进展:${task.progress_percent}%</div>
</div>
<span class="task-status">${task.status_display}</span>
</div>
`).join('')}
</div>
</div>
<div class="form-group">
<label>💡 想法和反思</label>
<textarea id="reflections" placeholder="今天的思考、反思、改进想法..." rows="3">${existingDiary ? existingDiary.reflections : ''}</textarea>
</div>
<div class="form-group">
<label>📈 进步点</label>
<textarea id="improvements" placeholder="今天在哪些方面有进步?" rows="2">${existingDiary ? existingDiary.improvements : ''}</textarea>
</div>
<div class="form-group">
<label>🎯 明日计划</label>
<textarea id="plans" placeholder="明天打算做什么?" rows="2">${existingDiary ? existingDiary.plans : ''}</textarea>
</div>
<button class="btn" onclick="saveDiary()">💾 保存日记</button>
${existingDiary ? `<button class="btn btn-secondary" onclick="extractExperience()">✨ 提炼经验</button>` : ''}
</div>
</div>
`;
}
function renderHistoryTab() {
return `
<div class="section-box">
<h2>📖 日记历史</h2>
${state.allDiaries.length === 0 ? '<div class="empty-state">暂无日记</div>' : state.allDiaries.map(diary => `
<div class="diary-item">
<div class="date">${diary.date}</div>
${diary.completed_tasks ? `
<div class="section">
<div class="section-title">✅ 完成的任务</div>
<div class="section-content">${diary.completed_tasks}</div>
</div>
` : ''}
${diary.learned ? `
<div class="section">
<div class="section-title">📚 学到的东西</div>
<div class="section-content">${diary.learned}</div>
</div>
` : ''}
${diary.reflections ? `
<div class="section">
<div class="section-title">💡 想法和反思</div>
<div class="section-content">${diary.reflections}</div>
</div>
` : ''}
${diary.linked_tasks && diary.linked_tasks.length > 0 ? `
<div class="linked-tasks">
<div class="section-title">🔗 关联任务</div>
${diary.linked_tasks.map(task => `<span class="tag">${task.title}</span>`).join('')}
</div>
` : ''}
${diary.experiences && diary.experiences.length > 0 ? `
<div class="linked-experiences">
<div class="section-title">💡 提炼经验</div>
${diary.experiences.map(exp => `<span class="tag" style="background: #fef3c7; color: #92400e;">${exp.title}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
`;
}
function renderTasksTab() {
return `
<div class="section-box">
<h2>📋 所有任务</h2>
${state.allTasks.length === 0 ? '<div class="empty-state">暂无任务</div>' : state.allTasks.map(task => `
<div class="diary-item" style="border-left-color: ${task.status === 'completed' ? '#10b981' : '#667eea'}">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="task-title" style="font-size: 1.1em;">${task.title}</div>
<span class="task-status">${task.status_display}</span>
</div>
<div style="font-size: 0.9em; color: #666; margin-bottom: 10px;">
优先级:${task.priority_display} | 进展:${task.progress_percent}%
</div>
${task.diary_entries && task.diary_entries.length > 0 ? `
<div class="linked-tasks">
<div class="section-title">📝 关联日记</div>
${task.diary_entries.map(d => `<span class="tag">${d.date}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
`;
}
function renderExperiencesTab() {
return `
<div class="section-box">
<h2>💡 经验总结</h2>
${state.allExperiences.length === 0 ? '<div class="empty-state">暂无经验总结</div>' : state.allExperiences.map(exp => `
<div class="experience-item">
<div class="category">${exp.category_display}</div>
<div class="title">${exp.title}</div>
<div style="margin: 10px 0;"><strong>🐛 问题:</strong>${exp.problem}</div>
<div style="margin: 10px 0;"><strong>✅ 解决:</strong>${exp.solution}</div>
${exp.lesson_learned ? `<div style="margin: 10px 0; padding: 10px; background: #fef3c7; border-radius: 5px;"><strong>📌 教训:</strong>${exp.lesson_learned}</div>` : ''}
</div>
`).join('')}
</div>
`;
}
function switchTab(tab) {
state.currentTab = tab;
render();
}
async function saveDiary() {
const today = state.selectedDate;
const existingDiary = state.allDiaries.find(d => d.date === today);
const data = {
date: today,
title: '每日日记',
completed_tasks: document.getElementById('completedTasks').value,
learned: document.getElementById('learned').value,
reflections: document.getElementById('reflections').value,
improvements: document.getElementById('improvements').value,
plans: document.getElementById('plans').value
};
const linkedTaskIds = state.allTasks
.filter(task => document.getElementById(`task-${task.id}`)?.checked)
.map(task => task.id);
try {
let response;
if (existingDiary) {
response = await fetch(`${API_BASE}/entries/${existingDiary.id}/`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
response = await fetch(`${API_BASE}/entries/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
const diary = await response.json();
// 关联任务
for (const taskId of linkedTaskIds) {
await fetch(`${API_BASE}/entries/${diary.id}/link_task/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId, progress_percent: 100, notes: '日记中完成' })
});
}
alert('✅ 日记已保存!关联任务已自动更新进展。');
loadData();
switchTab('history');
} catch (error) {
alert('保存失败:' + error.message);
}
}
function extractExperience() {
const reflections = document.getElementById('reflections').value;
if (!reflections) {
alert('请先写一些反思内容');
return;
}
alert('💡 功能开发中:选择反思内容,一键提炼为经验总结');
}
loadData();
</script>
</body>
</html>