This commit is contained in:
freedakgmail
2025-12-10 15:21:27 +08:00
parent e7167f4340
commit 1ae59960e8
148 changed files with 681 additions and 477 deletions
+1 -1
View File
@@ -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
View File
@@ -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>
+111 -3
View File
@@ -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' }}
/>
{/* 自定义右键菜单 */}
+1 -1
View File
@@ -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" />