update:1.新增手动创建大纲和章节,编写章节规划内容

2.新增项目更新日志页面,同步GitHub更新日志
3.新增章节内容生成时,选择本次生成人称
4.修复1 - N模式下,章节标题无法修改的问题
5.修复章节管理界面,批量生成后没有更新页面内容和状态
This commit is contained in:
xiamuceer
2025-12-06 14:08:20 +08:00
parent 187feac671
commit f831d07864
15 changed files with 1398 additions and 131 deletions
+1
View File
@@ -291,6 +291,7 @@ export default function AppFooter() {
</Space>
)}
</div>
</div>
);
}
@@ -0,0 +1,28 @@
import { useState } from 'react';
import { FloatButton } from 'antd';
import { FileTextOutlined } from '@ant-design/icons';
import ChangelogModal from './ChangelogModal';
export default function ChangelogFloatingButton() {
const [showChangelog, setShowChangelog] = useState(false);
return (
<div style={{ position: 'fixed', zIndex: 9999 }}>
<FloatButton
icon={<FileTextOutlined />}
type="primary"
tooltip="查看更新日志"
style={{
right: 24,
bottom: 100,
}}
onClick={() => setShowChangelog(true)}
/>
<ChangelogModal
visible={showChangelog}
onClose={() => setShowChangelog(false)}
/>
</div>
);
}
+325
View File
@@ -0,0 +1,325 @@
import { Modal, Timeline, Tag, Avatar, Empty, Spin, Button, Space, Tooltip } from 'antd';
import { useState, useEffect } from 'react';
import {
BugOutlined,
StarOutlined,
FileTextOutlined,
BgColorsOutlined,
ThunderboltOutlined,
ExperimentOutlined,
ToolOutlined,
QuestionCircleOutlined,
GithubOutlined,
ReloadOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import {
fetchChangelog,
groupChangelogByDate,
getCachedChangelog,
cacheChangelog,
markChangelogFetched,
shouldFetchChangelog,
clearChangelogCache,
type ChangelogEntry,
} from '../services/changelogService';
interface ChangelogModalProps {
visible: boolean;
onClose: () => void;
}
// 提交类型图标和颜色配置
const typeConfig: Record<ChangelogEntry['type'], { icon: React.ReactNode; color: string; label: string }> = {
feature: { icon: <StarOutlined />, color: 'green', label: '新功能' },
fix: { icon: <BugOutlined />, color: 'red', label: '修复' },
docs: { icon: <FileTextOutlined />, color: 'blue', label: '文档' },
style: { icon: <BgColorsOutlined />, color: 'purple', label: '样式' },
refactor: { icon: <ThunderboltOutlined />, color: 'orange', label: '重构' },
perf: { icon: <ThunderboltOutlined />, color: 'gold', label: '性能' },
test: { icon: <ExperimentOutlined />, color: 'cyan', label: '测试' },
chore: { icon: <ToolOutlined />, color: 'default', label: '杂项' },
other: { icon: <QuestionCircleOutlined />, color: 'default', label: '其他' },
};
export default function ChangelogModal({ visible, onClose }: ChangelogModalProps) {
const [changelog, setChangelog] = useState<ChangelogEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
// 加载更新日志
const loadChangelog = async (pageNum: number = 1, append: boolean = false) => {
setLoading(true);
setError(null);
try {
// 如果是第一页,先尝试使用缓存
if (pageNum === 1 && !append) {
const cached = getCachedChangelog();
if (cached && cached.length > 0) {
setChangelog(cached);
// 后台刷新
if (shouldFetchChangelog()) {
fetchChangelog(pageNum, 30)
.then(entries => {
setChangelog(entries);
cacheChangelog(entries);
markChangelogFetched();
})
.catch(console.error);
}
setLoading(false);
return;
}
}
const entries = await fetchChangelog(pageNum, 30);
if (entries.length === 0) {
setHasMore(false);
} else {
if (append) {
setChangelog(prev => [...prev, ...entries]);
} else {
setChangelog(entries);
// 缓存第一页数据
if (pageNum === 1) {
cacheChangelog(entries);
markChangelogFetched();
}
}
}
} catch (err) {
setError(err instanceof Error ? err.message : '获取更新日志失败');
} finally {
setLoading(false);
}
};
// 初始加载
useEffect(() => {
if (visible) {
loadChangelog(1, false);
setPage(1);
setHasMore(true);
}
}, [visible]);
// 加载更多
const handleLoadMore = () => {
const nextPage = page + 1;
setPage(nextPage);
loadChangelog(nextPage, true);
};
// 刷新(清除缓存并重新加载)
const handleRefresh = () => {
clearChangelogCache();
setPage(1);
setHasMore(true);
loadChangelog(1, false);
};
// 按日期分组
const groupedChangelog = groupChangelogByDate(changelog);
const sortedDates = Array.from(groupedChangelog.keys()).sort((a, b) => b.localeCompare(a));
// 格式化日期
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const now = new Date();
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return '今天';
if (diffDays === 1) return '昨天';
if (diffDays < 7) return `${diffDays} 天前`;
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
};
// 格式化时间
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
};
return (
<Modal
title={
<Space>
<GithubOutlined />
<span></span>
<Tooltip title="刷新">
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
/>
</Tooltip>
</Space>
}
open={visible}
onCancel={onClose}
footer={null}
width={800}
centered
styles={{
body: {
maxHeight: '70vh',
overflowY: 'auto',
padding: '24px',
},
}}
>
{error && (
<div style={{
padding: '16px',
marginBottom: '16px',
background: '#fff2e8',
border: '1px solid #ffbb96',
borderRadius: '4px',
color: '#d4380d',
}}>
{error}
</div>
)}
{loading && changelog.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Spin size="large" tip="加载更新日志中..." />
</div>
) : changelog.length === 0 ? (
<Empty description="暂无更新日志" />
) : (
<>
{sortedDates.map(date => {
const entries = groupedChangelog.get(date) || [];
return (
<div key={date} style={{ marginBottom: '32px' }}>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#1890ff',
marginBottom: '16px',
paddingBottom: '8px',
borderBottom: '2px solid #e8e8e8',
}}>
<ClockCircleOutlined style={{ marginRight: '8px' }} />
{formatDate(date)}
</div>
<Timeline>
{entries.map(entry => {
const config = typeConfig[entry.type] || typeConfig.other;
return (
<Timeline.Item
key={entry.id}
dot={
<div style={{
width: '24px',
height: '24px',
borderRadius: '50%',
background: '#fff',
border: `2px solid ${config.color === 'default' ? '#d9d9d9' : config.color}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
}}>
{config.icon}
</div>
}
>
<div style={{ marginLeft: '8px' }}>
<Space size="small" wrap>
<Tag color={config.color} icon={config.icon}>
{config.label}
</Tag>
{entry.scope && (
<Tag color="blue">{entry.scope}</Tag>
)}
<span style={{ color: '#999', fontSize: '12px' }}>
{formatTime(entry.date)}
</span>
</Space>
<div style={{
marginTop: '8px',
fontSize: '14px',
lineHeight: '1.6',
color: '#333',
}}>
{entry.message}
</div>
<Space size="small" style={{ marginTop: '8px' }}>
{entry.author.avatar && (
<Avatar size="small" src={entry.author.avatar} />
)}
<span style={{ color: '#666', fontSize: '13px' }}>
{entry.author.username || entry.author.name}
</span>
<a
href={entry.commitUrl}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: '12px' }}
>
</a>
</Space>
</div>
</Timeline.Item>
);
})}
</Timeline>
</div>
);
})}
{hasMore && (
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Button
type="default"
onClick={handleLoadMore}
loading={loading}
>
</Button>
</div>
)}
{!hasMore && changelog.length > 0 && (
<div style={{
textAlign: 'center',
color: '#999',
padding: '16px 0',
fontSize: '14px',
}}>
</div>
)}
</>
)}
<div style={{
marginTop: '24px',
padding: '12px',
background: '#f0f5ff',
borderRadius: '4px',
border: '1px solid #adc6ff',
fontSize: '13px',
color: '#1d39c4',
}}>
💡 GitHub
</div>
</Modal>
);
}