0.7.0.0
This commit is contained in:
@@ -44,7 +44,7 @@ export function CalendarDialog({ open, onOpenChange }: CalendarDialogProps) {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl w-[90vw] max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-w-4xl w-[90vw] max-h-[90vh] overflow-y-auto" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-2xl font-serif">
|
||||
<CalendarIcon className="h-6 w-6 text-red-600" />
|
||||
|
||||
+20
-18
@@ -258,21 +258,21 @@ export function SiteHeader() {
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex-shrink-0">
|
||||
<div className="container mx-auto flex h-16 items-center px-4 gap-4">
|
||||
<div className="container mx-auto flex h-14 md:h-16 items-center px-2 md:px-4 gap-2 md:gap-4">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={getUrlWithTreeId("/")} className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/80 text-primary-foreground shadow-sm">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
<div className="flex items-center gap-1.5 md:gap-2 flex-shrink-0">
|
||||
<Link href={getUrlWithTreeId("/")} className="flex items-center gap-1.5 md:gap-2 hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-8 w-8 md:h-9 md:w-9 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/80 text-primary-foreground shadow-sm flex-shrink-0">
|
||||
<BookOpen className="h-4 w-4 md:h-5 md:w-5" />
|
||||
</div>
|
||||
<span className="text-xl font-serif font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
|
||||
<span className="text-base md:text-xl font-serif font-bold tracking-tight whitespace-nowrap">
|
||||
华夏家谱
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-1 ml-6" onClick={() => setTreeDropdownOpen(false)}>
|
||||
<nav className="hidden md:flex items-center gap-1 ml-6 flex-shrink-0" onClick={() => setTreeDropdownOpen(false)}>
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
@@ -281,15 +281,15 @@ export function SiteHeader() {
|
||||
key={item.href}
|
||||
href={getUrlWithTreeId(item.href)}
|
||||
className={`
|
||||
relative px-4 py-2 rounded-lg font-serif text-sm font-medium
|
||||
transition-all duration-200 flex items-center gap-2
|
||||
relative px-3 lg:px-4 py-2 rounded-lg font-serif text-sm font-medium
|
||||
transition-all duration-200 flex items-center gap-1.5 lg:gap-2 whitespace-nowrap
|
||||
${active
|
||||
? 'text-primary bg-primary/10'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{item.label}</span>
|
||||
{active && (
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-primary rounded-full" />
|
||||
@@ -395,12 +395,12 @@ export function SiteHeader() {
|
||||
|
||||
{/* 家族树选择器 */}
|
||||
{session && familyTrees.length > 0 && (
|
||||
<div className="ml-4 flex-shrink-0">
|
||||
<div className="ml-2 md:ml-4 flex-shrink min-w-0">
|
||||
<DropdownMenu open={treeDropdownOpen} onOpenChange={setTreeDropdownOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2 w-[180px] font-serif">
|
||||
<span className="flex-1 truncate text-left">{currentTree?.name || '选择家族树'}</span>
|
||||
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
||||
<Button variant="outline" className="gap-1 md:gap-2 w-auto max-w-[120px] md:max-w-[180px] font-serif text-xs md:text-sm h-8 md:h-9 px-2 md:px-3">
|
||||
<span className="flex-1 truncate text-left">{currentTree?.name || '选择'}</span>
|
||||
<ChevronDown className="h-3 w-3 md:h-4 md:w-4 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -549,15 +549,16 @@ export function SiteHeader() {
|
||||
</div>
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="flex-shrink-0">
|
||||
{status === "loading" ? (
|
||||
<div className="h-9 w-9 rounded-full bg-muted animate-pulse" />
|
||||
<div className="h-8 w-8 md:h-9 md:w-9 rounded-full bg-muted animate-pulse" />
|
||||
) : session ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-9 w-9 rounded-full hover:bg-muted">
|
||||
<Avatar className="h-9 w-9 border-2 border-border">
|
||||
<Button variant="ghost" className="relative h-8 w-8 md:h-9 md:w-9 rounded-full hover:bg-muted p-0">
|
||||
<Avatar className="h-8 w-8 md:h-9 md:w-9 border-2 border-border">
|
||||
<AvatarImage src={undefined} alt={session.user?.name || "User"} />
|
||||
<AvatarFallback className="bg-primary/10 text-primary font-serif">
|
||||
<AvatarFallback className="bg-primary/10 text-primary font-serif text-sm md:text-base">
|
||||
{getUserInitial()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -599,6 +600,7 @@ export function SiteHeader() {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -312,12 +312,50 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
chartRef.current.innerHTML = ''
|
||||
}
|
||||
|
||||
// iOS/iPadOS 检测(包括 Safari、Chrome 等所有浏览器)
|
||||
// iPad 上所有浏览器都使用 WebKit 引擎
|
||||
const container = chartRef.current
|
||||
const isTouchDevice = navigator.maxTouchPoints > 1
|
||||
const isWebKit = /AppleWebKit/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
|
||||
const isIOSChrome = /CriOS/.test(navigator.userAgent)
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && isTouchDevice) ||
|
||||
isIOSChrome
|
||||
|
||||
console.log('Device detection:', { isIOS, isTouchDevice, isWebKit, isIOSChrome, userAgent: navigator.userAgent })
|
||||
|
||||
// 对于 iOS,设置明确的像素高度
|
||||
if (isIOS) {
|
||||
const headerHeight = 120
|
||||
const calculatedHeight = window.innerHeight - headerHeight
|
||||
container.style.height = `${calculatedHeight}px`
|
||||
}
|
||||
|
||||
const data = convertToChartData()
|
||||
|
||||
// 获取容器实际尺寸 - iOS Safari 可能返回 0,所以使用 window 尺寸作为后备
|
||||
const containerRect = chartRef.current.getBoundingClientRect()
|
||||
let containerWidth = containerRect.width
|
||||
let containerHeight = containerRect.height
|
||||
|
||||
// 如果尺寸无效,使用 window 尺寸
|
||||
if (!containerWidth || containerWidth < 100) {
|
||||
containerWidth = window.innerWidth
|
||||
}
|
||||
if (!containerHeight || containerHeight < 100) {
|
||||
containerHeight = window.innerHeight - 120
|
||||
}
|
||||
|
||||
console.log('D3 Chart container size:', { containerWidth, containerHeight, isIOS })
|
||||
|
||||
// 初始化图表
|
||||
const chart = new OrgChart()
|
||||
.container(chartRef.current)
|
||||
.data(data)
|
||||
.svgWidth(containerWidth)
|
||||
.svgHeight(containerHeight)
|
||||
.scaleExtent([0.1, 2]) // 缩放范围
|
||||
.initialZoom(0.5) // 初始缩放级别
|
||||
.nodeWidth((d: any) => {
|
||||
const spouseCount = d.data.spouseCount || 0
|
||||
return spouseCount === 0 ? 170 : spouseCount === 1 ? 300 : 170 + spouseCount * 110
|
||||
@@ -425,9 +463,11 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
"
|
||||
onmouseover="this.style.boxShadow='0 12px 40px rgba(0,0,0,0.15)'; this.style.transform='translateY(-4px) scale(1.02)'"
|
||||
onmouseout="this.style.boxShadow='0 4px 20px rgba(0,0,0,0.08)'; this.style.transform='translateY(0) scale(1)'"
|
||||
onmouseout="this.style.boxShadow='0 4px 20px rgba(0,0,0,0.08)'; this.style.transform='translateZ(0)'"
|
||||
>
|
||||
<!-- 顶部装饰条 -->
|
||||
<div style="
|
||||
@@ -661,6 +701,54 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
.expandAll() // 初始化时展开所有节点
|
||||
.fit() // 自适应全图显示
|
||||
|
||||
// 延迟再次 fit
|
||||
setTimeout(() => chart.fit(), 100)
|
||||
setTimeout(() => chart.fit(), 300)
|
||||
|
||||
// 所有触摸设备都需要特殊处理 - iOS foreignObject bug 修复
|
||||
if (isTouchDevice) {
|
||||
setTimeout(() => {
|
||||
const w = window.innerWidth
|
||||
const h = window.innerHeight - 120
|
||||
|
||||
if (chartRef.current) {
|
||||
chartRef.current.style.height = `${h}px`
|
||||
chartRef.current.style.width = `${w}px`
|
||||
|
||||
// iOS foreignObject bug 修复:强制重绘 SVG
|
||||
const svg = chartRef.current.querySelector('svg')
|
||||
if (svg) {
|
||||
// 触发重绘
|
||||
svg.style.display = 'none'
|
||||
void (svg as any).getBBox() // 强制回流
|
||||
svg.style.display = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重新设置 SVG 尺寸并渲染
|
||||
chart.svgWidth(w).svgHeight(h).render().expandAll().fit()
|
||||
console.log('Touch device delayed render:', { w, h })
|
||||
}, 500)
|
||||
|
||||
// 再次强制重绘
|
||||
setTimeout(() => {
|
||||
if (chartRef.current) {
|
||||
const svg = chartRef.current.querySelector('svg')
|
||||
if (svg) {
|
||||
svg.style.transform = 'translateZ(0)'
|
||||
// 强制所有 foreignObject 重新定位
|
||||
const foreignObjects = svg.querySelectorAll('foreignObject')
|
||||
foreignObjects.forEach((fo: Element) => {
|
||||
(fo as HTMLElement).style.transform = 'translateZ(0)'
|
||||
})
|
||||
}
|
||||
}
|
||||
chart.fit()
|
||||
}, 1000)
|
||||
|
||||
setTimeout(() => chart.fit(), 1500)
|
||||
}
|
||||
|
||||
// 注册全局配偶点击处理函数
|
||||
window.d3ChartSpouseClick = (event: Event, spouseId: string) => {
|
||||
event.stopPropagation()
|
||||
@@ -708,6 +796,27 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
}
|
||||
}, [members, rootId, onMemberClick, avatarCache])
|
||||
|
||||
// 监听窗口大小变化,重新适应视图
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartInstanceRef.current) {
|
||||
// 延迟执行 fit,等待布局稳定
|
||||
setTimeout(() => {
|
||||
chartInstanceRef.current?.fit()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
// 监听屏幕方向变化(iPad 等设备)
|
||||
window.addEventListener('orientationchange', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('orientationchange', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 监听 selectedMembers 变化,通过 DOM 操作更新选中标记
|
||||
useEffect(() => {
|
||||
console.log('Selection useEffect triggered, selectedMembers:', selectedMembers, 'isReady:', isReady)
|
||||
@@ -825,12 +934,11 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')] bg-repeat relative" key={`d3-chart-${rootId}`}>
|
||||
<div className="w-full h-full bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')] bg-repeat relative overflow-hidden" key={`d3-chart-${rootId}`}>
|
||||
{/* 图表容器 */}
|
||||
<div
|
||||
ref={chartRef}
|
||||
className="w-full h-full"
|
||||
style={{ overflow: 'hidden' }}
|
||||
/>
|
||||
|
||||
{/* 自定义右键菜单 */}
|
||||
|
||||
@@ -199,7 +199,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
|
||||
|
||||
{/* 裁剪对话框 */}
|
||||
<Dialog open={showCropDialog} onOpenChange={setShowCropDialog}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogContent className="max-w-3xl" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Crop className="h-5 w-5" />
|
||||
|
||||
Reference in New Issue
Block a user