# Sidebar.tsx · 功能08版本 · 2026-04-26 妈妈把这里的代码完整复制,粘贴到 VS Code 里的 Sidebar.tsx 文件,覆盖全部内容保存即可。 ```tsx import { useState, useEffect } from 'react' import { useWritingStore } from '../stores/writingStore' const TODAY = new Date().toISOString().slice(0, 10) function getDailyGoalData() { try { const raw = localStorage.getItem('dailyGoal') if (!raw) return { goal: 0, baseCount: 0, date: TODAY } return JSON.parse(raw) } catch { return { goal: 0, baseCount: 0, date: TODAY } } } function saveDailyGoalData(data: { goal: number; baseCount: number; date: string }) { localStorage.setItem('dailyGoal', JSON.stringify(data)) } export function Sidebar() { const { novels, currentNovelId, currentChapterId, createNovel, createChapter, deleteNovel, deleteChapter, setCurrentNovel, setCurrentChapter, reorderChapters, updateNovelInfo } = useWritingStore() const [isCreatingNovel, setIsCreatingNovel] = useState(false) const [newNovelTitle, setNewNovelTitle] = useState('') const [isCreatingChapter, setIsCreatingChapter] = useState(false) const [newChapterTitle, setNewChapterTitle] = useState('') const [expandedNovels, setExpandedNovels] = useState>(new Set()) const [editingInfoId, setEditingInfoId] = useState(null) const [editCover, setEditCover] = useState('') const [editDesc, setEditDesc] = useState('') const [goalData, setGoalData] = useState(getDailyGoalData) const [isEditingGoal, setIsEditingGoal] = useState(false) const [goalInput, setGoalInput] = useState('') const currentNovel = novels.find(n => n.id === currentNovelId) const totalWords = currentNovel ? currentNovel.chapters.reduce((sum, c) => sum + c.wordCount, 0) : novels.reduce((sum, n) => sum + n.chapters.reduce((s, c) => s + c.wordCount, 0), 0) useEffect(() => { const data = getDailyGoalData() if (data.date !== TODAY) { const newData = { goal: data.goal, baseCount: totalWords, date: TODAY } saveDailyGoalData(newData) setGoalData(newData) } }, []) const todayWords = Math.max(0, totalWords - goalData.baseCount) const goalProgress = goalData.goal > 0 ? Math.min(100, Math.round((todayWords / goalData.goal) * 100)) : 0 const goalDone = goalData.goal > 0 && todayWords >= goalData.goal const handleSetGoal = () => { const num = parseInt(goalInput) if (!isNaN(num) && num > 0) { const newData = { goal: num, baseCount: totalWords, date: TODAY } saveDailyGoalData(newData) setGoalData(newData) } else if (goalInput === '0' || goalInput === '') { const newData = { goal: 0, baseCount: totalWords, date: TODAY } saveDailyGoalData(newData) setGoalData(newData) } setIsEditingGoal(false) setGoalInput('') } const formatTime = (iso: string) => { const d = new Date(iso) const now = new Date() const diffMin = Math.floor((now.getTime() - d.getTime()) / 60000) if (diffMin < 1) return '刚刚' if (diffMin < 60) return diffMin + '分钟前' const diffH = Math.floor(diffMin / 60) if (diffH < 24) return diffH + '小时前' return (d.getMonth() + 1) + '/' + d.getDate() } const handleCreateNovel = () => { if (newNovelTitle.trim()) { createNovel(newNovelTitle.trim()) setNewNovelTitle('') setIsCreatingNovel(false) } } const handleCreateChapter = () => { if (newChapterTitle.trim() && currentNovelId) { createChapter(newChapterTitle.trim()) setNewChapterTitle('') setIsCreatingChapter(false) } } const toggleExpand = (id: string) => { const newExpanded = new Set(expandedNovels) if (newExpanded.has(id)) newExpanded.delete(id) else newExpanded.add(id) setExpandedNovels(newExpanded) } const openEditInfo = (novel: typeof novels[0]) => { setEditingInfoId(novel.id) setEditCover(novel.cover || '') setEditDesc(novel.description || '') } const saveEditInfo = (id: string) => { updateNovelInfo(id, editCover, editDesc) setEditingInfoId(null) } return (

光湖码字工作台

{isCreatingNovel ? (
setNewNovelTitle(e.target.value)} placeholder="作品名称" className="w-full px-2 py-1 text-sm border rounded mb-2" autoFocus onKeyDown={(e) => e.key === 'Enter' && handleCreateNovel()} />
) : ( )} {novels.map((novel) => (
{ setCurrentNovel(novel.id); toggleExpand(novel.id) }} > {expandedNovels.has(novel.id) ? '▼' : '▶'} {novel.title} {novel.chapters.length}章
{editingInfoId === novel.id && (
作品封面 / 简介
{editCover && ( 封面预览 (e.currentTarget.style.display = 'none')} /> )} setEditCover(e.target.value)} placeholder="封面图片链接(可选)" className="w-full px-2 py-1 text-xs border rounded mb-1" />