2025-12-06 14:08:20 +08:00
|
|
|
|
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,
|
2025-12-29 12:08:01 +08:00
|
|
|
|
SyncOutlined,
|
2025-12-06 14:08:20 +08:00
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
import {
|
|
|
|
|
|
fetchChangelog,
|
|
|
|
|
|
groupChangelogByDate,
|
|
|
|
|
|
cacheChangelog,
|
|
|
|
|
|
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: '新功能' },
|
2025-12-29 12:08:01 +08:00
|
|
|
|
update: { icon: <SyncOutlined />, color: 'geekblue', label: '更新' },
|
2025-12-06 14:08:20 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 加载更新日志
|
2025-12-11 17:01:25 +08:00
|
|
|
|
// 每次用户打开窗口时才同步获取最新数据,不自动刷新
|
2025-12-06 14:08:20 +08:00
|
|
|
|
const loadChangelog = async (pageNum: number = 1, append: boolean = false) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-11 17:01:25 +08:00
|
|
|
|
// 每次打开都从网络获取最新数据
|
2025-12-06 14:08:20 +08:00
|
|
|
|
const entries = await fetchChangelog(pageNum, 30);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-12-06 14:08:20 +08:00
|
|
|
|
if (entries.length === 0) {
|
|
|
|
|
|
setHasMore(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (append) {
|
|
|
|
|
|
setChangelog(prev => [...prev, ...entries]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setChangelog(entries);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
// 缓存第一页数据(用于分页加载时的数据持久化)
|
2025-12-06 14:08:20 +08:00
|
|
|
|
if (pageNum === 1) {
|
|
|
|
|
|
cacheChangelog(entries);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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} 天前`;
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-12-06 14:08:20 +08:00
|
|
|
|
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',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
background: 'var(--color-error-bg)',
|
|
|
|
|
|
border: '1px solid var(--color-error-border)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
borderRadius: '4px',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
color: 'var(--color-error)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
}}>
|
|
|
|
|
|
{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) || [];
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-12-06 14:08:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div key={date} style={{ marginBottom: '32px' }}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
fontWeight: 600,
|
2025-12-11 17:01:25 +08:00
|
|
|
|
color: 'var(--color-primary)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
paddingBottom: '8px',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
borderBottom: '2px solid var(--color-border-secondary)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
}}>
|
|
|
|
|
|
<ClockCircleOutlined style={{ marginRight: '8px' }} />
|
|
|
|
|
|
{formatDate(date)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Timeline>
|
|
|
|
|
|
{entries.map(entry => {
|
|
|
|
|
|
const config = typeConfig[entry.type] || typeConfig.other;
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-12-06 14:08:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Timeline.Item
|
|
|
|
|
|
key={entry.id}
|
|
|
|
|
|
dot={
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
width: '24px',
|
|
|
|
|
|
height: '24px',
|
|
|
|
|
|
borderRadius: '50%',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
background: 'var(--color-bg-container)',
|
|
|
|
|
|
border: `2px solid ${config.color === 'default' ? 'var(--color-border)' : config.color}`,
|
2025-12-06 14:08:20 +08:00
|
|
|
|
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>
|
|
|
|
|
|
)}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '12px' }}>
|
2025-12-06 14:08:20 +08:00
|
|
|
|
{formatTime(entry.date)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
marginTop: '8px',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
lineHeight: '1.6',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
color: 'var(--color-text-primary)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
}}>
|
|
|
|
|
|
{entry.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Space size="small" style={{ marginTop: '8px' }}>
|
|
|
|
|
|
{entry.author.avatar && (
|
|
|
|
|
|
<Avatar size="small" src={entry.author.avatar} />
|
|
|
|
|
|
)}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
<span style={{ color: 'var(--color-text-secondary)', fontSize: '13px' }}>
|
2025-12-06 14:08:20 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
2025-12-11 17:01:25 +08:00
|
|
|
|
{
|
|
|
|
|
|
hasMore && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', marginTop: '24px' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
onClick={handleLoadMore}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
>
|
|
|
|
|
|
加载更多
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-06 14:08:20 +08:00
|
|
|
|
|
2025-12-11 17:01:25 +08:00
|
|
|
|
{
|
|
|
|
|
|
!hasMore && changelog.length > 0 && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
color: 'var(--color-text-tertiary)',
|
|
|
|
|
|
padding: '16px 0',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
}}>
|
|
|
|
|
|
已显示所有更新日志
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-06 14:08:20 +08:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
marginTop: '24px',
|
|
|
|
|
|
padding: '12px',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
background: 'var(--color-info-bg)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
borderRadius: '4px',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
border: '1px solid var(--color-info-border)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
fontSize: '13px',
|
2025-12-11 17:01:25 +08:00
|
|
|
|
color: 'var(--color-primary)',
|
2025-12-06 14:08:20 +08:00
|
|
|
|
}}>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
💡 提示:每次打开窗口时自动获取最新更新日志,数据来源于 GitHub 提交历史
|
2025-12-06 14:08:20 +08:00
|
|
|
|
</div>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
</Modal >
|
2025-12-06 14:08:20 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|