|
|
@@ -0,0 +1,179 @@
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
+import { useDispatch, useSelector } from 'react-redux';
|
|
|
+import type { AppDispatch, RootState } from '@/states/store';
|
|
|
+import { fetchViewsOrProtocols } from '@/states/patient/viewSelection';
|
|
|
+import { setCurrentBodyPart } from '@/states/bodyPartSlice';
|
|
|
+
|
|
|
+// ✅ 独立 SVG 文件(不内联)
|
|
|
+// import { ReactComponent as HumanSvg } from '@/assets/imgs/VirtualHuman/human.svg';
|
|
|
+import HumanSvg from '@/assets/imgs/VirtualHuman/human.svg';
|
|
|
+
|
|
|
+
|
|
|
+interface HumanBodySvgProps {
|
|
|
+ // 容器样式(覆盖默认的 .human-container)
|
|
|
+ containerStyle?: React.CSSProperties;
|
|
|
+
|
|
|
+ // SVG 样式(覆盖默认的 .human-svg)
|
|
|
+ svgStyle?: React.CSSProperties;
|
|
|
+
|
|
|
+ // 自定义类名(追加到默认类名)
|
|
|
+ className?: string;
|
|
|
+
|
|
|
+ // 完全自定义的内联样式
|
|
|
+ style?: React.CSSProperties;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const HumanBody: React.FC<HumanBodySvgProps> = ({
|
|
|
+ containerStyle,
|
|
|
+ svgStyle,
|
|
|
+ className,
|
|
|
+ style
|
|
|
+}) => {
|
|
|
+ const [activePart, setActivePart] = useState<string | null>(null);
|
|
|
+ const dispatch = useDispatch<AppDispatch>();
|
|
|
+
|
|
|
+ const currentPatientType = useSelector(
|
|
|
+ (state: RootState) => state.patientType.current
|
|
|
+ );
|
|
|
+
|
|
|
+ const bodyPartsState = useSelector(
|
|
|
+ (state: RootState) => state.bodyPart.byPatientType
|
|
|
+ );
|
|
|
+
|
|
|
+ const selected = useSelector(
|
|
|
+ (state: RootState) => state.selection.selected
|
|
|
+ );
|
|
|
+
|
|
|
+ const bringToFront = (el: SVGElement) => {
|
|
|
+ const parent = el.parentNode;
|
|
|
+ if (!parent) return;
|
|
|
+ parent.appendChild(el);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleMouseOver = (e: React.MouseEvent<SVGSVGElement>) => {
|
|
|
+ const target = e.target as Element;
|
|
|
+ const partEl = target.closest('[data-part]') as SVGElement | null;
|
|
|
+ if (partEl) {
|
|
|
+ bringToFront(partEl);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SVG 统一点击入口(事件委托)
|
|
|
+ */
|
|
|
+ const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
|
|
|
+ const target = e.target as Element;
|
|
|
+
|
|
|
+ // 从当前节点向上找 data-part
|
|
|
+ const partEl = target.closest('[data-part]') as SVGElement | null;
|
|
|
+
|
|
|
+ if (!partEl) return;
|
|
|
+
|
|
|
+ const serverName = partEl.getAttribute('data-part');
|
|
|
+ if (!serverName) return;
|
|
|
+
|
|
|
+ console.log('点击身体部位:', serverName);
|
|
|
+
|
|
|
+
|
|
|
+ setActivePart(serverName);
|
|
|
+
|
|
|
+ const selectedBodyPart =
|
|
|
+ bodyPartsState.find(
|
|
|
+ (item) => item.body_part_id === serverName
|
|
|
+ ) || null;
|
|
|
+
|
|
|
+ dispatch(setCurrentBodyPart(selectedBodyPart));
|
|
|
+
|
|
|
+ dispatch(
|
|
|
+ fetchViewsOrProtocols({
|
|
|
+ selection: selected,
|
|
|
+ patientType: currentPatientType?.patient_type_id ?? null,
|
|
|
+ bodyPart: serverName,
|
|
|
+ })
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 控制 SVG 高亮状态
|
|
|
+ * 不修改 SVG 文件本身
|
|
|
+ */
|
|
|
+ useEffect(() => {
|
|
|
+ const svg = document.querySelector(
|
|
|
+ '.human-svg'
|
|
|
+ ) as SVGSVGElement | null;
|
|
|
+
|
|
|
+ if (!svg) return;
|
|
|
+
|
|
|
+ // 清空所有状态
|
|
|
+ svg.querySelectorAll('[id]').forEach((el) => {
|
|
|
+ el.classList.remove('active');
|
|
|
+ el.classList.add('inactive');
|
|
|
+ });
|
|
|
+
|
|
|
+ if (activePart) {
|
|
|
+ const activeEl = svg.querySelector(
|
|
|
+ `#${CSS.escape(activePart)}`
|
|
|
+ );
|
|
|
+ activeEl?.classList.remove('inactive');
|
|
|
+ activeEl?.classList.add('active');
|
|
|
+ }
|
|
|
+ }, [activePart]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {/* ===== 样式(集中在一个文件内)===== */}
|
|
|
+ <style>
|
|
|
+ {`
|
|
|
+ .human-container {
|
|
|
+ width: 294px;
|
|
|
+ // height: 594px;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .human-svg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ opacity: 0.95;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 默认状态 */
|
|
|
+ .human-svg [data-part] {
|
|
|
+ cursor: pointer;
|
|
|
+ // opacity: 0.35;
|
|
|
+ transition: opacity 0.2s ease, filter 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* hover */
|
|
|
+ .human-svg [data-part]:hover {
|
|
|
+ //opacity: 0.2;
|
|
|
+ filter: drop-shadow(0 0 16px red);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 激活状态 */
|
|
|
+ .human-svg .active {
|
|
|
+ opacity: 1;
|
|
|
+ filter: drop-shadow(0 0 6px #409eff);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 非激活 */
|
|
|
+ .human-svg .inactive {
|
|
|
+ opacity: 0.25;
|
|
|
+ }
|
|
|
+ `}
|
|
|
+ </style>
|
|
|
+
|
|
|
+ {/* ===== SVG 容器 ===== */}
|
|
|
+ <div className="human-container" style={containerStyle}>
|
|
|
+ <HumanSvg
|
|
|
+ className="human-svg"
|
|
|
+ style={svgStyle}
|
|
|
+ onMouseOver={handleMouseOver}
|
|
|
+ onClick={handleSvgClick}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default HumanBody;
|