feat: initial lifelog project
- 待办事项(支持优先级、截止日期) - 每日记录(上午/下午/晚上分条目) - Go后端 + Gin + GORM + MySQL - React前端 + TailwindCSS
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import HomePage from './pages/HomePage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<nav className="bg-white border-b border-slate-200 mb-8">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="flex items-center gap-2 py-4">
|
||||
<span className="text-xl">📒</span>
|
||||
<span className="font-bold text-lg text-slate-800">LifeLog</span>
|
||||
<span className="text-slate-400 text-sm ml-2">我的每日工作台</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="px-4 pb-12">
|
||||
<HomePage />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,43 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
})
|
||||
|
||||
export interface Todo {
|
||||
id: number
|
||||
title: string
|
||||
done: boolean
|
||||
priority: number
|
||||
due_date: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface DailyLog {
|
||||
id: number
|
||||
date: string
|
||||
morning: string
|
||||
afternoon: string
|
||||
evening: string
|
||||
}
|
||||
|
||||
export const todoApi = {
|
||||
getAll: () => api.get<{ data: Todo[] }>('/todos').then(r => r.data.data),
|
||||
create: (title: string, priority: number, dueDate?: string) =>
|
||||
api.post<{ data: Todo }>('/todos', { title, priority, due_date: dueDate || null }).then(r => r.data.data),
|
||||
update: (id: number, data: Partial<Todo>) =>
|
||||
api.put<{ data: Todo }>(`/todos/${id}`, data).then(r => r.data.data),
|
||||
toggle: (id: number) =>
|
||||
api.patch<{ data: Todo }>(`/todos/${id}/done`).then(r => r.data.data),
|
||||
delete: (id: number) => api.delete(`/todos/${id}`),
|
||||
}
|
||||
|
||||
export const logApi = {
|
||||
getByDate: (date: string) =>
|
||||
api.get<{ data: DailyLog }>(`/logs/${date}`).then(r => r.data.data),
|
||||
save: (date: string, data: { morning: string; afternoon: string; evening: string }) =>
|
||||
api.put(`/logs/${date}`, data),
|
||||
getRecent: (limit = 30) =>
|
||||
api.get<{ data: DailyLog[] }>(`/logs?limit=${limit}`).then(r => r.data.data),
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f8fafc;
|
||||
color: #1e293b;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,381 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { todoApi, logApi, type Todo, type DailyLog } from '../api'
|
||||
|
||||
type Filter = 'all' | 'active' | 'done'
|
||||
|
||||
// 每日记录的单条内容
|
||||
interface LogItem {
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const parseLog = (content: string): LogItem[] => {
|
||||
return content.split('\n').filter(l => l.trim()).map((l, i) => ({
|
||||
id: `item-${i}-${Date.now()}`,
|
||||
text: l.trim()
|
||||
}))
|
||||
}
|
||||
|
||||
const formatLog = (items: LogItem[]): string => {
|
||||
return items.map(i => i.text).join('\n')
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [todos, setTodos] = useState<Todo[]>([])
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
const [newPriority, setNewPriority] = useState(2)
|
||||
const [newDueDate, setNewDueDate] = useState('')
|
||||
const [filter, setFilter] = useState<Filter>('all')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const today = new Date()
|
||||
const todayStr = today.toISOString().split('T')[0]
|
||||
const [date, setDate] = useState(todayStr)
|
||||
|
||||
// 每时段内容转为卡片数组
|
||||
const [morningItems, setMorningItems] = useState<LogItem[]>([])
|
||||
const [afternoonItems, setAfternoonItems] = useState<LogItem[]>([])
|
||||
const [eveningItems, setEveningItems] = useState<LogItem[]>([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [logLoading, setLogLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadTodos(), loadLog(date)]).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const loadTodos = async () => {
|
||||
const data = await todoApi.getAll()
|
||||
setTodos(data)
|
||||
}
|
||||
|
||||
const loadLog = async (d: string) => {
|
||||
setLogLoading(true)
|
||||
const data = await logApi.getByDate(d)
|
||||
setMorningItems(data?.morning ? parseLog(data.morning) : [])
|
||||
setAfternoonItems(data?.afternoon ? parseLog(data.afternoon) : [])
|
||||
setEveningItems(data?.evening ? parseLog(data.evening) : [])
|
||||
setLogLoading(false)
|
||||
}
|
||||
|
||||
// 防抖自动保存(仅今日)
|
||||
useEffect(() => {
|
||||
if (date !== todayStr) return
|
||||
const timer = setTimeout(() => {
|
||||
if (morningItems.length || afternoonItems.length || eveningItems.length) {
|
||||
handleSave(true)
|
||||
}
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [morningItems, afternoonItems, eveningItems])
|
||||
|
||||
const handleSave = useCallback(async (isAuto = false) => {
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
await logApi.save(date, {
|
||||
morning: formatLog(morningItems),
|
||||
afternoon: formatLog(afternoonItems),
|
||||
evening: formatLog(eveningItems),
|
||||
})
|
||||
setSaving(false)
|
||||
if (!isAuto) {
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
}, [date, morningItems, afternoonItems, eveningItems])
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!newTitle.trim()) return
|
||||
const todo = await todoApi.create(newTitle, newPriority)
|
||||
setTodos([todo, ...todos])
|
||||
setNewTitle('')
|
||||
setNewDueDate('')
|
||||
}
|
||||
|
||||
const handleToggle = async (id: number) => {
|
||||
const updated = await todoApi.toggle(id)
|
||||
setTodos(todos.map(t => t.id === id ? updated : t))
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await todoApi.delete(id)
|
||||
setTodos(todos.filter(t => t.id !== id))
|
||||
}
|
||||
|
||||
const priorityDot = (p: number) => {
|
||||
const colors = ['bg-red-400', 'bg-yellow-400', 'bg-green-400']
|
||||
return <span className={`w-2 h-2 rounded-full ${colors[p - 1] || 'bg-slate-300'} flex-shrink-0`} />
|
||||
}
|
||||
|
||||
const isOverdue = (dueDate: string | null) => {
|
||||
if (!dueDate) return false
|
||||
return new Date(dueDate + 'T23:59:59') < new Date(todayStr + 'T23:59:59')
|
||||
}
|
||||
|
||||
const isDueToday = (dueDate: string | null) => {
|
||||
if (!dueDate) return false
|
||||
return dueDate === todayStr
|
||||
}
|
||||
|
||||
const formatDueDate = (dueDate: string | null) => {
|
||||
if (!dueDate) return null
|
||||
const d = new Date(dueDate + 'T00:00:00')
|
||||
const diff = Math.round((d.getTime() - today.getTime()) / 86400000)
|
||||
if (diff === 0) return '今天'
|
||||
if (diff === 1) return '明天'
|
||||
if (diff === -1) return '昨天'
|
||||
if (diff > 0) return `${diff}天后`
|
||||
return `${Math.abs(diff)}天前`
|
||||
}
|
||||
|
||||
const filteredTodos = todos.filter(t => {
|
||||
if (filter === 'active') return !t.done
|
||||
if (filter === 'done') return t.done
|
||||
return true
|
||||
})
|
||||
|
||||
const pendingCount = todos.filter(t => !t.done).length
|
||||
const doneCount = todos.filter(t => t.done).length
|
||||
const hasLogContent = morningItems.length || afternoonItems.length || eveningItems.length
|
||||
const dateLabel = date === todayStr ? '今天' : date
|
||||
|
||||
// 每日记录卡片组件
|
||||
const LogSection = ({ label, icon, items, onChange }: {
|
||||
label: string
|
||||
icon: string
|
||||
items: LogItem[]
|
||||
onChange: (items: LogItem[]) => void
|
||||
}) => {
|
||||
const [newItemText, setNewItemText] = useState('')
|
||||
|
||||
const addItem = () => {
|
||||
if (!newItemText.trim()) return
|
||||
onChange([...items, { id: `item-${Date.now()}`, text: newItemText.trim() }])
|
||||
setNewItemText('')
|
||||
}
|
||||
|
||||
const updateItem = (id: string, text: string) => {
|
||||
onChange(items.map(i => i.id === id ? { ...i, text } : i))
|
||||
}
|
||||
|
||||
const deleteItem = (id: string) => {
|
||||
onChange(items.filter(i => i.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-600 mb-2">{icon} {label}</label>
|
||||
<div className="space-y-1.5">
|
||||
{items.map(item => (
|
||||
<div key={item.id} className="group flex items-start gap-2 bg-slate-50 rounded-xl px-3 py-2 hover:bg-slate-100 transition">
|
||||
<span className="text-slate-300 mt-0.5 text-xs flex-shrink-0">•</span>
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={e => updateItem(item.id, e.target.value)}
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent text-sm text-slate-700 resize-none focus:outline-none leading-relaxed"
|
||||
placeholder="输入内容..."
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
addItem()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => deleteItem(item.id)}
|
||||
className="text-slate-300 hover:text-red-400 transition text-xs opacity-0 group-hover:opacity-100 mt-0.5 flex-shrink-0"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{/* 添加新条目 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newItemText}
|
||||
onChange={e => setNewItemText(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
addItem()
|
||||
}
|
||||
}}
|
||||
placeholder="按回车添加新条目..."
|
||||
className="flex-1 text-sm px-3 py-2 border border-dashed border-slate-200 rounded-xl focus:outline-none focus:border-blue-400 bg-white transition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* 顶部概览 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold text-slate-800">📒 LifeLog</h1>
|
||||
<div className="flex items-center gap-3 text-sm text-slate-500">
|
||||
<span className="bg-blue-50 text-blue-600 px-2 py-0.5 rounded-full font-medium">
|
||||
{pendingCount} 待办
|
||||
</span>
|
||||
<span className="bg-green-50 text-green-600 px-2 py-0.5 rounded-full font-medium">
|
||||
{doneCount} 已完成
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded-full font-medium ${hasLogContent ? 'bg-amber-50 text-amber-600' : 'bg-slate-100 text-slate-400'}`}>
|
||||
{dateLabel} {hasLogContent ? '已记录' : '未记录'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setDate(todayStr); loadLog(todayStr) }}
|
||||
className="text-sm text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
回到今天
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 左侧:待办事项 */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-slate-800">📋 待办事项</h2>
|
||||
<div className="flex gap-1 bg-slate-100 rounded-lg p-0.5">
|
||||
{([['all', '全部'], ['active', '进行中'], ['done', '已完成']] as [Filter, string][]).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setFilter(key)}
|
||||
className={`px-3 py-1 rounded-md text-xs font-medium transition ${
|
||||
filter === key ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAdd} className="mb-4 space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newTitle}
|
||||
onChange={e => setNewTitle(e.target.value)}
|
||||
placeholder="输入新事项,按回车添加..."
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={newPriority}
|
||||
onChange={e => setNewPriority(Number(e.target.value))}
|
||||
className="px-2 py-1.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||
>
|
||||
<option value={1}>🔴高</option>
|
||||
<option value={2}>🟡中</option>
|
||||
<option value={3}>🟢低</option>
|
||||
</select>
|
||||
<input
|
||||
type="date"
|
||||
value={newDueDate}
|
||||
onChange={e => setNewDueDate(e.target.value)}
|
||||
className="flex-1 px-2 py-1.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition text-sm font-medium"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="space-y-1 max-h-80 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">加载中...</div>
|
||||
) : filteredTodos.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-4xl mb-2">✨</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
{filter === 'all' ? '暂无待办事项' : filter === 'active' ? '太棒了,所有事都做完了!' : '还没有已完成的事项'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredTodos.map(todo => (
|
||||
<div
|
||||
key={todo.id}
|
||||
className={`flex items-center gap-3 p-3 rounded-xl transition group hover:bg-slate-50 ${todo.done ? 'opacity-60' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todo.done}
|
||||
onChange={() => handleToggle(todo.id)}
|
||||
className="w-4 h-4 rounded accent-blue-500 flex-shrink-0 cursor-pointer"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className={`text-sm block ${todo.done ? 'line-through text-slate-400' : 'text-slate-700'}`}>
|
||||
{todo.title}
|
||||
</span>
|
||||
{todo.due_date && (
|
||||
<span className={`text-xs mt-0.5 block ${
|
||||
todo.done ? 'text-slate-400' : isOverdue(todo.due_date) ? 'text-red-500 font-medium' : isDueToday(todo.due_date) ? 'text-amber-500 font-medium' : 'text-slate-400'
|
||||
}`}>
|
||||
📅 {formatDueDate(todo.due_date)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{priorityDot(todo.priority)}
|
||||
<button
|
||||
onClick={() => handleDelete(todo.id)}
|
||||
className="text-slate-300 hover:text-red-400 transition text-xs opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:每日记录 */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-slate-800">📓 每日记录</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{saved && <span className="text-green-500 text-xs animate-pulse">已保存 ✓</span>}
|
||||
{saving && <span className="text-slate-400 text-xs">保存中...</span>}
|
||||
{date !== todayStr && (
|
||||
<button
|
||||
onClick={() => handleSave()}
|
||||
className="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition text-xs font-medium"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={e => { setDate(e.target.value); loadLog(e.target.value) }}
|
||||
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{logLoading ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">加载中...</div>
|
||||
) : (
|
||||
<div className="space-y-4 max-h-[500px] overflow-y-auto">
|
||||
<LogSection label="上午" icon="🌅" items={morningItems} onChange={setMorningItems} />
|
||||
<LogSection label="下午" icon="☀️" items={afternoonItems} onChange={setAfternoonItems} />
|
||||
<LogSection label="晚上" icon="🌙" items={eveningItems} onChange={setEveningItems} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user