Files
diary-system/frontend/index.html

922 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 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;
}
/* Tab 切换 */
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab-btn {
padding: 12px 24px;
background: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
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;
touch-action: manipulation;
}
.tab-btn:hover {
background: #f8f9fa;
transform: translateY(-2px);
}
.tab-btn:active {
transform: scale(0.98);
}
.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;
}
/* 日历组件 */
.calendar-wrapper {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.calendar {
flex: 0 0 300px;
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;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.calendar-header button:hover {
background: #5568d3;
}
.calendar-header button:active {
background: #4857c0;
transform: scale(0.95);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.calendar-day-header {
text-align: center;
font-size: 0.75em;
color: #666;
padding: 5px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85em;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
min-width: 36px;
min-height: 36px;
}
.calendar-day:hover {
background: #e0e7ff;
}
.calendar-day:active {
background: #c7d2fe;
transform: scale(0.95);
}
.calendar-day.selected {
background: #667eea;
color: white;
}
.calendar-day.today {
border: 2px solid #667eea;
font-weight: bold;
}
.calendar-day.has-data {
position: relative;
}
.calendar-day.has-data::after {
content: '';
position: absolute;
bottom: 2px;
width: 6px;
height: 6px;
background: #10b981;
border-radius: 50%;
}
.calendar-day.empty {
cursor: default;
}
/* 内容区域 */
.content-area {
flex: 1;
min-width: 0;
}
.task-item, .diary-item {
padding: 15px;
border-left: 4px solid #667eea;
background: #f8f9fa;
margin-bottom: 15px;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.task-item:hover, .diary-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.task-item:active, .diary-item:active {
transform: scale(0.98);
background: #eef1f5;
}
.task-item.status-pending { border-left-color: #6b7280; }
.task-item.status-in_progress { border-left-color: #3b82f6; }
.task-item.status-blocked { border-left-color: #f59e0b; }
.task-item.status-completed { border-left-color: #10b981; }
.task-item.status-cancelled { border-left-color: #ef4444; opacity: 0.6; }
.task-item .header, .diary-item .header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.task-item .title, .diary-item .title {
font-weight: bold;
color: #333;
font-size: 1.1em;
}
.task-item .status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
color: white;
}
.status-pending .status { background: #6b7280; }
.status-in_progress .status { background: #3b82f6; }
.status-blocked .status { background: #f59e0b; }
.status-completed .status { background: #10b981; }
.status-cancelled .status { background: #ef4444; }
.task-item .priority {
font-size: 0.8em;
color: #666;
margin-bottom: 8px;
}
.task-item .progress-bar {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin: 10px 0;
}
.task-item .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s;
}
.task-item .progress-text {
font-size: 0.85em;
color: #666;
text-align: right;
}
.diary-item .date {
color: #667eea;
font-weight: bold;
margin-bottom: 10px;
}
.diary-item .section {
margin: 10px 0;
}
.diary-item .section-title {
font-weight: bold;
color: #555;
}
.diary-item .section-content {
color: #666;
margin-left: 20px;
white-space: pre-wrap;
}
/* 任务详情 */
.task-detail {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
}
.task-detail h3 {
color: #333;
margin-bottom: 15px;
}
.task-detail .meta {
display: flex;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.task-detail .meta-item {
background: white;
padding: 8px 15px;
border-radius: 5px;
font-size: 0.9em;
}
.task-detail .description {
margin-bottom: 15px;
white-space: pre-wrap;
color: #666;
}
.task-detail .progress-notes {
background: #fffbeb;
padding: 15px;
border-radius: 5px;
margin-top: 15px;
}
.task-detail .progress-notes h4 {
color: #92400e;
margin-bottom: 10px;
}
.task-detail .progress-notes .note {
font-size: 0.85em;
color: #666;
padding: 8px 0;
border-bottom: 1px solid #f3e5d5;
}
.experience-item {
padding: 15px;
border-left: 4px solid #f59e0b;
background: #fffbeb;
margin-bottom: 15px;
border-radius: 5px;
}
.experience-item .header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.experience-item .title {
font-weight: bold;
color: #333;
font-size: 1.1em;
}
.experience-item .category {
background: #f59e0b;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
}
.experience-item .problem, .experience-item .solution, .experience-item .lesson {
margin: 10px 0;
}
.experience-item .problem-title {
font-weight: bold;
color: #dc2626;
margin-bottom: 5px;
}
.experience-item .solution-title {
font-weight: bold;
color: #059669;
margin-bottom: 5px;
}
.experience-item .lesson {
padding: 10px;
background: #fef3c7;
border-radius: 5px;
}
.experience-item .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: #666;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 30px;
}
/* 移动端优化 */
@media (max-width: 768px) {
body {
padding: 10px;
}
header h1 {
font-size: 1.8em;
}
header p {
font-size: 0.9em;
}
.stats {
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.stat-card {
padding: 10px;
}
.stat-card h3 {
font-size: 1.4em;
}
.stat-card p {
font-size: 0.75em;
}
.tabs {
gap: 8px;
margin-bottom: 15px;
}
.tab-btn {
padding: 10px 16px;
font-size: 0.9em;
flex: 1;
text-align: center;
}
.section-box {
padding: 15px;
margin-bottom: 20px;
}
.section-box h2 {
font-size: 1.3em;
margin-bottom: 15px;
}
.calendar-wrapper {
flex-direction: column;
gap: 15px;
}
.calendar {
flex: none;
width: 100%;
padding: 10px;
}
.calendar-header span {
font-size: 0.9em;
}
.calendar-header button {
padding: 3px 8px;
font-size: 0.9em;
}
.calendar-day {
font-size: 0.75em;
aspect-ratio: 1;
}
.calendar-day-header {
font-size: 0.65em;
}
.content-area {
width: 100%;
}
.task-item, .diary-item {
padding: 12px;
margin-bottom: 12px;
}
.task-item .title, .diary-item .title {
font-size: 1em;
}
.task-item .status {
font-size: 0.75em;
padding: 3px 10px;
}
.task-item .priority {
font-size: 0.75em;
}
.task-detail .meta {
flex-direction: column;
gap: 8px;
}
.task-detail .meta-item {
font-size: 0.85em;
}
.experience-item .title {
font-size: 1em;
}
.experience-item .category {
font-size: 0.75em;
}
}
/* 超小屏幕(< 400px */
@media (max-width: 400px) {
.stats {
grid-template-columns: repeat(2, 1fr);
}
.stat-card h3 {
font-size: 1.2em;
}
.stat-card p {
font-size: 0.7em;
}
header h1 {
font-size: 1.5em;
}
.tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ 码神的日记系统</h1>
<p>记录每天的进步与成长</p>
</header>
<div id="app">
<div class="loading">加载中...</div>
</div>
</div>
<script>
const API_BASE = '/api';
let state = {
currentTab: 'tasks',
selectedDate: new Date().toISOString().split('T')[0],
selectedTask: null,
allTasks: [],
allEntries: [],
allExperiences: [],
taskStats: {},
diaryStats: {},
expStats: {}
};
async function loadData() {
try {
const [taskStatsRes, tasksRes, diaryStatsRes, entriesRes, expStatsRes, experiencesRes] = await Promise.all([
fetch(`${API_BASE}/tasks/stats/`),
fetch(`${API_BASE}/tasks/`),
fetch(`${API_BASE}/entries/stats/`),
fetch(`${API_BASE}/entries/`),
fetch(`${API_BASE}/experiences/stats/`),
fetch(`${API_BASE}/experiences/`)
]);
state.taskStats = await taskStatsRes.json();
state.allTasks = await tasksRes.json();
state.diaryStats = await diaryStatsRes.json();
state.allEntries = await entriesRes.json();
state.expStats = await expStatsRes.json();
state.allExperiences = await experiencesRes.json();
render();
} catch (error) {
document.getElementById('app').innerHTML = `
<div class="error">加载失败:${error.message}</div>
`;
}
}
function getTasksByDate(date) {
return state.allTasks.filter(task => {
const createdDate = task.created_at.split('T')[0];
return createdDate === date;
});
}
function getEntryByDate(date) {
return state.allEntries.find(entry => entry.date === date);
}
function renderCalendar(year, month, selectedDate, hasDataDates) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDay = firstDay.getDay();
const today = new Date().toISOString().split('T')[0];
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
const dayNames = ['日', '一', '二', '三', '四', '五', '六'];
let html = `
<div class="calendar">
<div class="calendar-header">
<button onclick="changeMonth(-1)">&#8592;</button>
<span style="font-weight: bold;">${year}${monthNames[month]}</span>
<button onclick="changeMonth(1)">&#8594;</button>
</div>
<div class="calendar-grid">
${dayNames.map(d => `<div class="calendar-day-header">${d}</div>`).join('')}
${Array(startDay).fill('<div class="calendar-day empty"></div>').join('')}
`;
for (let day = 1; day <= lastDay.getDate(); day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isSelected = dateStr === selectedDate;
const isToday = dateStr === today;
const hasData = hasDataDates.includes(dateStr);
html += `
<div class="calendar-day ${isSelected ? 'selected' : ''} ${isToday ? 'today' : ''} ${hasData ? 'has-data' : ''}"
onclick="selectDate('${dateStr}')">
${day}
</div>
`;
}
html += `</div></div>`;
return html;
}
function renderTasksView() {
const current = state.currentCalendar || new Date();
// 任务列表视图
if (!state.selectedTask) {
return `
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('tasks')">📋 工作任务</button>
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
</div>
<div class="section-box">
<h2>📋 所有任务</h2>
<div class="content-area">
${state.allTasks.length === 0 ? `
<div class="empty-state">
<p>暂无任务</p>
</div>
` : state.allTasks.map(task => `
<div class="task-item status-${task.status}" onclick="selectTask(${task.id})">
<div class="header">
<span class="title">${task.title}</span>
<span class="status">${task.status_display}</span>
</div>
<div class="priority">优先级:${task.priority_display} | 创建:${task.created_at.split('T')[0]}</div>
${task.description ? `<div class="description" style="max-height: 60px; overflow: hidden;">${task.description}</div>` : ''}
<div class="progress-bar">
<div class="progress-fill" style="width: ${task.progress_percent}%"></div>
</div>
<div class="progress-text">进展:${task.progress_percent}%</div>
</div>
`).join('')}
</div>
</div>
`;
}
// 任务详情视图(带日历)
const task = state.selectedTask;
const taskProgressDates = task.progress_notes ?
task.progress_notes.match(/\[(\d{4}-\d{2}-\d{2})\]/g)?.map(s => s.slice(1, 11)) || [] : [];
const calendarHtml = renderCalendar(current.getFullYear(), current.getMonth(),
state.selectedDate, taskProgressDates, true);
const selectedDateProgress = task.progress_notes
.split('\n')
.filter(line => line.startsWith(`[${state.selectedDate}]`))
.map(line => line.slice(13))
.join('\n');
return `
<div class="tabs">
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
</div>
<div class="section-box">
<h2>📋 任务详情</h2>
<button class="tab-btn" onclick="state.selectedTask = null; render();" style="margin-bottom: 20px;">← 返回列表</button>
<div class="task-detail">
<h3>${task.title}</h3>
<div class="meta">
<div class="meta-item">状态:${task.status_display}</div>
<div class="meta-item">优先级:${task.priority_display}</div>
<div class="meta-item">负责人:${task.assigned_to || '码神'}</div>
<div class="meta-item">创建:${task.created_at.split('T')[0]}</div>
<div class="meta-item">进展:${task.progress_percent}%</div>
</div>
${task.description ? `<div class="description"><strong>描述:</strong><br>${task.description}</div>` : ''}
<h3 style="margin-top: 25px; margin-bottom: 15px;">📅 任务进展日历</h3>
<p style="color: #666; margin-bottom: 15px; font-size: 0.9em;">点击日历查看当日的任务记录</p>
<div class="calendar-wrapper">
${calendarHtml}
<div class="content-area">
<div class="task-detail" style="background: white; border: 1px solid #e0e7ff;">
<h4 style="color: #667eea; margin-bottom: 10px;">📝 ${state.selectedDate} 的进展记录</h4>
${selectedDateProgress ? `
<div class="progress-notes" style="background: #f8f9fa;">
<div class="note" style="white-space: pre-wrap;">${selectedDateProgress}</div>
</div>
` : `
<div class="empty-state" style="padding: 20px;">
<p style="color: #999;">${state.selectedDate} 暂无记录</p>
</div>
`}
</div>
</div>
</div>
</div>
</div>
`;
}
function renderDiaryView() {
const entry = getEntryByDate(state.selectedDate);
const entryDates = state.allEntries.map(e => e.date);
const current = state.currentCalendar || new Date();
const calendarHtml = renderCalendar(current.getFullYear(), current.getMonth(), state.selectedDate, entryDates);
return `
<div class="tabs">
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
<button class="tab-btn active" onclick="switchTab('diary')">📝 日记</button>
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
</div>
<div class="section-box">
<h2>📝 日记</h2>
<p style="color: #666; margin-bottom: 15px; font-size: 0.9em;">点击日历选择日期查看日记</p>
<div class="calendar-wrapper">
${calendarHtml}
<div class="content-area">
${!entry ? `
<div class="empty-state">
<p>📅 ${state.selectedDate} 没有日记</p>
<p style="margin-top: 10px; font-size: 0.9em; color: #999;">点击日历选择其他日期</p>
</div>
` : `
<div class="diary-item" style="cursor: default;">
<div class="header">
<span class="title">${entry.title || '每日日记'}</span>
<span class="date">${entry.date}</span>
</div>
${entry.completed_tasks ? `
<div class="section">
<span class="section-title">✅ 完成的任务</span>
<div class="section-content">${entry.completed_tasks}</div>
</div>
` : ''}
${entry.learned ? `
<div class="section">
<span class="section-title">📚 学到的东西</span>
<div class="section-content">${entry.learned}</div>
</div>
` : ''}
${entry.problems ? `
<div class="section">
<span class="section-title">🐛 遇到的问题</span>
<div class="section-content">${entry.problems}</div>
</div>
` : ''}
${entry.reflections ? `
<div class="section">
<span class="section-title">💡 想法和反思</span>
<div class="section-content">${entry.reflections}</div>
</div>
` : ''}
${entry.improvements ? `
<div class="section">
<span class="section-title">📈 进步点</span>
<div class="section-content">${entry.improvements}</div>
</div>
` : ''}
${entry.plans ? `
<div class="section">
<span class="section-title">🎯 明日计划</span>
<div class="section-content">${entry.plans}</div>
</div>
` : ''}
</div>
`}
</div>
</div>
</div>
`;
}
function renderExperiencesView() {
return `
<div class="tabs">
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
<button class="tab-btn active" onclick="switchTab('experiences')">💡 经验总结</button>
</div>
<div class="section-box">
<h2>💡 经验总结</h2>
${state.allExperiences.length === 0 ? `
<div class="empty-state">
<p>暂无经验总结</p>
</div>
` : state.allExperiences.map(exp => `
<div class="experience-item">
<div class="header">
<span class="title">${exp.title}</span>
<span class="category">${exp.category}</span>
</div>
<div class="problem">
<div class="problem-title">🐛 问题</div>
<div class="section-content">${exp.problem}</div>
</div>
<div class="solution">
<div class="solution-title">✅ 解决方案</div>
<div class="section-content">${exp.solution}</div>
</div>
${exp.lesson_learned ? `
<div class="lesson">
<div class="lesson-title">📌 经验教训</div>
<div class="section-content">${exp.lesson_learned}</div>
</div>
` : ''}
</div>
`).join('')}
</div>
`;
}
function render() {
const app = document.getElementById('app');
let content = '';
if (state.currentTab === 'tasks') {
content = renderTasksView();
} else if (state.currentTab === 'diary') {
content = renderDiaryView();
} else {
content = renderExperiencesView();
}
app.innerHTML = `
<div class="stats">
<div class="stat-card">
<h3>${state.diaryStats.total_entries || 0}</h3>
<p>总日记</p>
</div>
<div class="stat-card">
<h3>${state.taskStats.total || 0}</h3>
<p>总任务</p>
</div>
<div class="stat-card">
<h3>${state.taskStats.in_progress || 0}</h3>
<p>进行中</p>
</div>
<div class="stat-card">
<h3>${state.taskStats.completed || 0}</h3>
<p>已完成</p>
</div>
<div class="stat-card">
<h3>${state.taskStats.completion_rate || 0}%</h3>
<p>完成率</p>
</div>
<div class="stat-card">
<h3>${state.expStats.total_experiences || 0}</h3>
<p>经验</p>
</div>
</div>
${content}
`;
}
function switchTab(tab) {
state.currentTab = tab;
state.selectedTask = null;
render();
}
function selectDate(date) {
state.selectedDate = date;
render();
}
function selectTask(taskId) {
state.selectedTask = state.allTasks.find(t => t.id === taskId);
render();
}
function changeMonth(delta) {
if (!state.currentCalendar) {
state.currentCalendar = new Date();
}
state.currentCalendar.setMonth(state.currentCalendar.getMonth() + delta);
render();
}
// 初始化
loadData();
</script>
</body>
</html>