183 lines
7.9 KiB
TypeScript
183 lines
7.9 KiB
TypeScript
import { motion, AnimatePresence } from 'framer-motion'
|
||
import { createPortal } from 'react-dom'
|
||
import { X, History, CalendarDays, Tag } from 'lucide-react'
|
||
import { changelogEntries } from '@/data'
|
||
import type { ChangelogType } from '@/types/itto'
|
||
|
||
interface ChangelogModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
}
|
||
|
||
const containerVariants = {
|
||
hidden: { opacity: 0 },
|
||
visible: {
|
||
opacity: 1,
|
||
transition: {
|
||
staggerChildren: 0.06,
|
||
},
|
||
},
|
||
}
|
||
|
||
const itemVariants = {
|
||
hidden: { opacity: 0, y: 12 },
|
||
visible: {
|
||
opacity: 1,
|
||
y: 0,
|
||
transition: {
|
||
duration: 0.25,
|
||
},
|
||
},
|
||
}
|
||
|
||
const typeMeta: Record<ChangelogType, { label: string; className: string }> = {
|
||
feat: { label: '新功能', className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' },
|
||
fix: { label: '修复', className: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300' },
|
||
style: { label: '样式', className: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300' },
|
||
refactor: { label: '重构', className: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300' },
|
||
docs: { label: '文档', className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' },
|
||
perf: { label: '性能', className: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' },
|
||
test: { label: '测试', className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300' },
|
||
chore: { label: '工程', className: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' },
|
||
}
|
||
|
||
function formatDate(date: string) {
|
||
const [year, month, day] = date.split('-').map(Number)
|
||
if (!year || !month || !day) return date
|
||
return `${year}年${month}月${day}日`
|
||
}
|
||
|
||
// 按日期分组
|
||
function groupByDate(entries: typeof changelogEntries) {
|
||
const groups = new Map<string, typeof changelogEntries>()
|
||
entries.forEach((entry) => {
|
||
const existing = groups.get(entry.date) || []
|
||
groups.set(entry.date, [...existing, entry])
|
||
})
|
||
return Array.from(groups.entries()).sort((a, b) => b[0].localeCompare(a[0]))
|
||
}
|
||
|
||
export function ChangelogModal({ isOpen, onClose }: ChangelogModalProps) {
|
||
const groupedEntries = groupByDate(changelogEntries)
|
||
|
||
// 使用 Portal 将模态框渲染到 body,避免被 Header 的层叠上下文限制
|
||
if (typeof document === 'undefined') return null
|
||
|
||
return createPortal(
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<>
|
||
{/* 背景遮罩 */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
onClick={onClose}
|
||
className="fixed inset-0 bg-black/50 z-[9999]"
|
||
/>
|
||
|
||
{/* 模态框 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
className="fixed inset-4 md:inset-8 lg:inset-16 z-[10000] flex items-center justify-center"
|
||
>
|
||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full h-full flex flex-col overflow-hidden">
|
||
{/* 头部 */}
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/50">
|
||
<History size={20} className="text-indigo-600 dark:text-indigo-400" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">更新日志</h2>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
共 {changelogEntries.length} 条记录
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="flex h-10 w-10 items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 transition-colors"
|
||
aria-label="关闭"
|
||
>
|
||
<X size={20} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 内容区域 */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
{changelogEntries.length > 0 ? (
|
||
<motion.div
|
||
variants={containerVariants}
|
||
initial="hidden"
|
||
animate="visible"
|
||
className="max-w-4xl mx-auto space-y-8"
|
||
>
|
||
{/* 按日期分组显示 */}
|
||
{groupedEntries.map(([date, entries]) => (
|
||
<div key={date} className="space-y-4">
|
||
{/* 日期标题 */}
|
||
<div className="flex items-center gap-3">
|
||
<CalendarDays size={20} className="text-indigo-600 dark:text-indigo-400" />
|
||
<h3 className="text-lg font-bold text-gray-900 dark:text-white">
|
||
{formatDate(date)}
|
||
</h3>
|
||
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||
</div>
|
||
|
||
{/* 该日期下的更新列表 - 统一卡片 */}
|
||
<motion.div
|
||
variants={itemVariants}
|
||
className="bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"
|
||
>
|
||
{entries.map((entry, index) => {
|
||
const meta = typeMeta[entry.type]
|
||
return (
|
||
<div
|
||
key={entry.id || `${entry.date}-${index}`}
|
||
className="p-4 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
{/* 标签 */}
|
||
<div className="flex items-center gap-2 flex-shrink-0">
|
||
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium ${meta.className}`}>
|
||
<Tag size={12} />
|
||
{meta.label}
|
||
</span>
|
||
{entry.scope && (
|
||
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||
{entry.scope}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 标题 */}
|
||
<p className="text-sm text-gray-900 dark:text-white flex-1">
|
||
{entry.title}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</motion.div>
|
||
</div>
|
||
))}
|
||
</motion.div>
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center py-16 text-gray-400 dark:text-gray-500">
|
||
<History size={48} className="mb-4" />
|
||
<p className="text-sm">暂无更新记录</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>,
|
||
document.body
|
||
)
|
||
}
|