|
|
@@ -0,0 +1,273 @@
|
|
|
+import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
+import styles from './index.module.scss'
|
|
|
+import { NodeTurnRight, NodeRight, NodeBottom, RightLineDash, NodeActive, NodeTurnBottomRight } from '../Utils'
|
|
|
+import { useDrag } from '@use-gesture/react'
|
|
|
+import { myData } from '@/utils/http'
|
|
|
+
|
|
|
+const MAIN_CONTENT_WIDTH = 1920
|
|
|
+const MAIN_CONTENT_HEIGHT = 945
|
|
|
+const MINIMAP_SCALE = 0.045
|
|
|
+
|
|
|
+function Graph({ setCurrentNodeIndex }: { setCurrentNodeIndex: (index: number) => void }) {
|
|
|
+ // 新增拖拽相关状态
|
|
|
+ const [isDragging, setIsDragging] = useState(false)
|
|
|
+ const [startX, setStartX] = useState(0)
|
|
|
+ const [startY, setStartY] = useState(0)
|
|
|
+ const [offsetX, setOffsetX] = useState(0)
|
|
|
+ const [offsetY, setOffsetY] = useState(0)
|
|
|
+
|
|
|
+ // 设置初始位置
|
|
|
+ useEffect(() => {
|
|
|
+ setOffsetX(0)
|
|
|
+ setOffsetY(-200)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const startRef = useRef({
|
|
|
+ startX: 0, // 触摸起始点 X 坐标
|
|
|
+ startY: 0, // 触摸起始点 Y 坐标
|
|
|
+ startOffsetX: 0, // 元素起始偏移 X
|
|
|
+ startOffsetY: 0 // 元素起始偏移 Y
|
|
|
+ });
|
|
|
+
|
|
|
+ const graphRef = useRef<HTMLDivElement>(null); // 已存在,无需新增
|
|
|
+
|
|
|
+ // 原生事件处理函数(无需 useCallback,因为在 useEffect 内绑定)
|
|
|
+ // const nativeTouchStart = (e: TouchEvent) => {
|
|
|
+ // const el = graphRef.current;
|
|
|
+ // if (!el || e.target !== el) return; // 仅容器本身触发
|
|
|
+
|
|
|
+ // const touch = e.touches[0];
|
|
|
+ // if (!touch) return;
|
|
|
+
|
|
|
+ // startRef.current = {
|
|
|
+ // startX: touch.clientX,
|
|
|
+ // startY: touch.clientY,
|
|
|
+ // startOffsetX: offsetX,
|
|
|
+ // startOffsetY: offsetY,
|
|
|
+ // };
|
|
|
+ // };
|
|
|
+
|
|
|
+ // const nativeTouchMove = (e: TouchEvent) => {
|
|
|
+ // const el = graphRef.current;
|
|
|
+ // if (!el) return;
|
|
|
+
|
|
|
+ // const touch = e.touches[0];
|
|
|
+ // if (!touch) return;
|
|
|
+
|
|
|
+ // const { startX, startY, startOffsetX, startOffsetY } = startRef.current;
|
|
|
+ // const dx = touch.clientX - startX;
|
|
|
+ // const dy = touch.clientY - startY;
|
|
|
+
|
|
|
+ // const newX = startOffsetX + dx;
|
|
|
+ // const newY = startOffsetY + dy;
|
|
|
+
|
|
|
+ // // 打印坐标,确认是否持续触发
|
|
|
+ // console.log('原生touchmove:', touch.clientX, touch.clientY);
|
|
|
+
|
|
|
+ // setOffsetX(Math.min(0, Math.max(-1640, newX)));
|
|
|
+ // setOffsetY(Math.min(0, Math.max(-350, newY)));
|
|
|
+ // };
|
|
|
+
|
|
|
+ // useEffect(() => {
|
|
|
+ // const el = graphRef.current;
|
|
|
+ // console.log('offsetX, offsetY', el)
|
|
|
+ // if (!el) return;
|
|
|
+
|
|
|
+ // // 绑定原生事件(passive: false 可选,这里用了 touchAction: none 不需要)
|
|
|
+ // el.addEventListener('touchstart', nativeTouchStart);
|
|
|
+ // el.addEventListener('touchmove', nativeTouchMove);
|
|
|
+
|
|
|
+ // // 卸载时解绑
|
|
|
+ // // return () => {
|
|
|
+ // // el.removeEventListener('touchstart', nativeTouchStart);
|
|
|
+ // // el.removeEventListener('touchmove', nativeTouchMove);
|
|
|
+ // // };
|
|
|
+ // }, [offsetX, offsetY]);
|
|
|
+
|
|
|
+ // 新增事件处理函数
|
|
|
+ const handleMouseDown = (e: React.MouseEvent) => {
|
|
|
+ if (e.target === e.currentTarget) { // 仅当点击容器本身时触发拖拽
|
|
|
+ e.stopPropagation();
|
|
|
+ setIsDragging(true);
|
|
|
+ setStartX(e.clientX);
|
|
|
+ setStartY(e.clientY);
|
|
|
+ setOffsetX(offsetX);
|
|
|
+ setOffsetY(offsetY);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
|
|
+ const touch = e.touches[0]; // 获取第一个触摸点(单指拖拽)
|
|
|
+ console.log(touch.clientX, touch.clientY)
|
|
|
+ // if (!touch) return;
|
|
|
+
|
|
|
+ // 记录触摸起始坐标
|
|
|
+ startRef.current.startX = touch.clientX;
|
|
|
+ startRef.current.startY = touch.clientY;
|
|
|
+
|
|
|
+ // 记录元素当前偏移量(避免拖拽时跳跃)
|
|
|
+ startRef.current.startOffsetX = offsetX;
|
|
|
+ startRef.current.startOffsetY = offsetY;
|
|
|
+
|
|
|
+ }, [offsetX, offsetY])
|
|
|
+
|
|
|
+ const handleMouseMove = (e: React.MouseEvent) => {
|
|
|
+ if (isDragging) {
|
|
|
+ // 改为使用初始点击位置计算总位移
|
|
|
+ const dx = e.clientX - startX
|
|
|
+ const dy = e.clientY - startY
|
|
|
+
|
|
|
+ // 使用函数式更新确保获取最新状态值
|
|
|
+ setOffsetX(prev => {
|
|
|
+ const newX = prev + dx
|
|
|
+ return Math.min(0, Math.max(-1640, newX))
|
|
|
+ })
|
|
|
+
|
|
|
+ setOffsetY(prev => {
|
|
|
+ const newY = prev + dy
|
|
|
+ return Math.min(0, Math.max(-350, newY))
|
|
|
+ })
|
|
|
+
|
|
|
+ // 更新起始坐标为当前鼠标位置(保持相对移动)
|
|
|
+ setStartX(e.clientX)
|
|
|
+ setStartY(e.clientY)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
|
|
+ console.log(123)
|
|
|
+ const touch = e.touches[0];
|
|
|
+ if (!touch) return;
|
|
|
+
|
|
|
+ // 计算滑动距离 = 当前触摸位置 - 初始触摸位置
|
|
|
+ const dx = touch.clientY - startRef.current.startY;
|
|
|
+ const dy = -touch.clientX + startRef.current.startX;
|
|
|
+
|
|
|
+ // 更新 X 偏移量(限制范围:-1640 ~ 0,与你原有逻辑一致)
|
|
|
+ setOffsetX(prev => {
|
|
|
+ const newX = startRef.current.startOffsetX + dx; // 基于初始偏移量计算
|
|
|
+ return Math.min(0, Math.max(-1640, newX));
|
|
|
+ // return newX
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新 Y 偏移量(限制范围:-350 ~ 0)
|
|
|
+ setOffsetY(prev => {
|
|
|
+ const newY = startRef.current.startOffsetY + dy;
|
|
|
+ return Math.min(0, Math.max(-350, newY));
|
|
|
+ // return newY
|
|
|
+ });
|
|
|
+
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const handleMouseUp = () => {
|
|
|
+ setIsDragging(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新增小地图相关逻辑
|
|
|
+ // const graphRef = useRef<HTMLDivElement>(null)
|
|
|
+ const [contentSize, setContentSize] = useState({ width: 2000, height: 1200 })
|
|
|
+ const miniMapScale = 0.1
|
|
|
+
|
|
|
+ // 动态获取容器尺寸
|
|
|
+ useEffect(() => {
|
|
|
+ if (graphRef.current) {
|
|
|
+ setContentSize({ width: graphRef.current.clientWidth, height: graphRef.current.clientHeight })
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 小地图拖拽绑定
|
|
|
+ const bind = useDrag(({ offset: [x, y] }) => {
|
|
|
+ // 计算视口最大移动范围
|
|
|
+ const maxX = contentSize.width * miniMapScale - MAIN_CONTENT_WIDTH * MINIMAP_SCALE - 2
|
|
|
+ const maxY = contentSize.height * miniMapScale - MAIN_CONTENT_HEIGHT * MINIMAP_SCALE - 4
|
|
|
+ console.log(maxX, maxY, '------------')
|
|
|
+ // 钳制坐标范围
|
|
|
+ const clampedX = Math.max(0, Math.min(x, maxX))
|
|
|
+ const clampedY = Math.max(0, Math.min(y, maxY))
|
|
|
+ setOffsetX(-clampedX / miniMapScale)
|
|
|
+ setOffsetY(-clampedY / miniMapScale)
|
|
|
+ })
|
|
|
+
|
|
|
+ const handleNameClick = (index: number) => {
|
|
|
+ console.log(index, '------------')
|
|
|
+ setCurrentNodeIndex(index)
|
|
|
+ }
|
|
|
+
|
|
|
+ const NodeData = React.memo(() =>
|
|
|
+ <>
|
|
|
+ {myData.genealogyData.map((item, index) => {
|
|
|
+ let res
|
|
|
+ if (item.type === 'active') res = <NodeActive key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} nameClick={() => handleNameClick(index)} className='nodeActiveG' />
|
|
|
+ if (item.type === 'nodeRight_n') res = <NodeRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='normal' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeRight_a') res = <NodeRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='active' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeRight_f') res = <NodeRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='false' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeBottom_n') res = <NodeBottom key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='normal' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeTurnRight_n') res = <NodeTurnRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='normal' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeTurnRight_a') res = <NodeTurnRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='active' nameClick={() => handleNameClick(index)} />
|
|
|
+ if (item.type === 'nodeTurnBottomRight_a') res = <NodeTurnBottomRight key={index} data={item} style={{ transform: `translate(${item.position.x}px, ${item.position.y}px)` }} type='active' nameClick={() => handleNameClick(index)} />
|
|
|
+ return res
|
|
|
+ })}
|
|
|
+ </>
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className={styles.Graph}
|
|
|
+ ref={graphRef}
|
|
|
+ onMouseDown={handleMouseDown}
|
|
|
+ onMouseMove={handleMouseMove}
|
|
|
+ onMouseUp={handleMouseUp}
|
|
|
+ onMouseLeave={handleMouseUp}
|
|
|
+ onTouchStart={handleTouchStart}
|
|
|
+ onTouchMove={handleTouchMove}
|
|
|
+ style={{
|
|
|
+ cursor: isDragging ? 'grabbing' : 'grab',
|
|
|
+ transform: `translate(${offsetX}px, ${offsetY}px)`,
|
|
|
+ userSelect: 'none'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <NodeData />
|
|
|
+ </div>
|
|
|
+ <div className={styles.tip}>
|
|
|
+ <div className='t1'>
|
|
|
+ <div className='icon'>
|
|
|
+ <img src={require('@/assets/img/A6_gen_false.png')} draggable='false' alt='' />
|
|
|
+ </div>
|
|
|
+ <div className='txt'>为勘误之人</div>
|
|
|
+ </div>
|
|
|
+ <div className='t2'>
|
|
|
+ <RightLineDash className='t2_rightLine' />
|
|
|
+ <div className='txt'>为非直系父子关系</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className={styles.miniMap}>
|
|
|
+ <div
|
|
|
+ className={styles.viewport}
|
|
|
+ {...bind()}
|
|
|
+ style={{
|
|
|
+ transform: `translate(${-offsetX * miniMapScale}px, ${-offsetY * miniMapScale}px)`,
|
|
|
+ width: `${MAIN_CONTENT_WIDTH * MINIMAP_SCALE}px`,
|
|
|
+ height: `${MAIN_CONTENT_HEIGHT * MINIMAP_SCALE}px`
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <div
|
|
|
+ className={styles.miniContent}
|
|
|
+ style={{
|
|
|
+ transform: `scale(${miniMapScale})`,
|
|
|
+ width: `${contentSize.width}px`,
|
|
|
+ height: `${contentSize.height}px`
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <NodeData />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const MemoGraph = React.memo(Graph)
|
|
|
+
|
|
|
+export default MemoGraph
|