Files
ittoview/src/components/ChangelogModal.tsx

183 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
)
}