Browse Source

refactor (1.72.2 -> 1.72.3): 重构动物身体部位组件,实现SVG外部化和事件委托模式

- 将CatBody、DogBody、EquineBody和ExoticPets组件从直接嵌入SVG代码改为使用外部SVG文件
- 实现事件委托模式,通过单个根元素处理所有点击事件,提升性能和可维护性
- 添加高亮状态管理,使用CSS类控制交互状态,支持动态切换激活/非激活状态
- 优化CSS样式,添加hover效果、过渡动画和用户交互反馈
- 在webpack配置中添加对Animal图片资源的支持,并配置SVG优化选项
- 添加SVG优化配置,禁用会影响SVG结构的插件,保留必要的属性和命名空间

改动文件:
- config/index.ts
- src/components/CatBody.tsx
- src/components/DogBody.tsx
- src/components/EquineBody.tsx
- src/components/ExoticPets.tsx
- src/assets/imgs/Animal/(新增目录及SVG文件)
dengdx 14 hours ago
parent
commit
392cd7098a

+ 20 - 0
CHANGELOG.md

@@ -2,6 +2,26 @@
 
 本项目的所有重要变更都将记录在此文件中.
 
+## [1.72.3] - 2026-01-21 21:15
+
+refactor (1.72.2 -> 1.72.3): 重构动物身体部位组件,实现SVG外部化和事件委托模式
+
+- 将CatBody、DogBody、EquineBody和ExoticPets组件从直接嵌入SVG代码改为使用外部SVG文件
+- 实现事件委托模式,通过单个根元素处理所有点击事件,提升性能和可维护性
+- 添加高亮状态管理,使用CSS类控制交互状态,支持动态切换激活/非激活状态
+- 优化CSS样式,添加hover效果、过渡动画和用户交互反馈
+- 在webpack配置中添加对Animal图片资源的支持,并配置SVG优化选项
+- 添加SVG优化配置,禁用会影响SVG结构的插件,保留必要的属性和命名空间
+
+改动文件:
+
+- config/index.ts
+- src/components/CatBody.tsx
+- src/components/DogBody.tsx
+- src/components/EquineBody.tsx
+- src/components/ExoticPets.tsx
+- src/assets/imgs/Animal/(新增目录及SVG文件)
+
 ## [1.72.2] - 2026-01-21 17:59
 
 fix (1.72.1 -> 1.72.2): 修复系统模式R-S的描述错误

+ 26 - 0
config/index.ts

@@ -126,12 +126,38 @@ export default defineConfig<'webpack5'>(async (merge) => {
             path.resolve(__dirname, '../src/assets/Icons')
           )
           .add(path.resolve(__dirname, '../src/assets/imgs/VirtualHuman'))
+          .add(path.resolve(__dirname, '../src/assets/imgs/Animal'))
           .end()
           .use('svgr')
           .loader('@svgr/webpack')
           .options({
             dimensions: false,
             icon: false,
+            svgoConfig: {
+              plugins: [
+                {
+                  name: 'preset-default',
+                  params: {
+                    overrides: {
+                      // 禁用会合并 path 的插件
+                      mergePaths: false,
+                      // 禁用会移除 ID 的插件
+                      cleanupIds: false,
+                      // 禁用会移除 viewBox 的插件
+                      removeViewBox: false,
+                      // 移除编辑器命名空间(解决 namespace tags 错误)
+                      removeEditorsNSData: true,
+                      // 清理属性(移除 xmlns:inkscape 等)
+                      cleanupAttrs: true,
+                    },
+                  },
+                },
+                // 移除不必要的命名空间声明
+                {
+                  name: 'removeXMLNS',
+                },
+              ],
+            },
           });
         // chain.module
         //   .rule('svgr')

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.72.2",
+  "version": "1.72.3",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

File diff suppressed because it is too large
+ 4 - 0
src/assets/imgs/Animal/ExoticAnimal.svg


File diff suppressed because it is too large
+ 5 - 0
src/assets/imgs/Animal/cat.svg


File diff suppressed because it is too large
+ 5 - 0
src/assets/imgs/Animal/dog.svg


File diff suppressed because it is too large
+ 5 - 0
src/assets/imgs/Animal/horse.svg


+ 103 - 73
src/components/CatBody.tsx

@@ -1,79 +1,109 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import CatSvg from '@/assets/imgs/Animal/cat.svg';
 
-const DogBody = ({
-  onPathClick,
-  selectedId,
-}: {
-  onPathClick: (id: string) => void;
+interface CatBodyProps {
   selectedId: string | null;
-}) => {
+  onPathClick: (id: string) => void;
+}
+
+const CatBody: React.FC<CatBodyProps> = ({ selectedId, onPathClick }) => {
+  /**
+   * 事件委托: 在 SVG 根元素处理所有点击
+   */
+  const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
+    const target = e.target as SVGElement;
+    
+    // 查找被点击的 path 元素
+    let pathElement: SVGPathElement | null = null;
+    
+    if (target.tagName === 'path') {
+      pathElement = target as SVGPathElement;
+    } else if (target.tagName === 'g') {
+      // 如果点击在 g 上,查找子 path
+      pathElement = target.querySelector('path[id^="Cat_"]');
+    }
+    
+    if (!pathElement || !pathElement.id) return;
+    
+    console.log('点击猫部位:', pathElement.id);
+    onPathClick(pathElement.id);
+  };
+
+  /**
+   * 控制 SVG 高亮状态
+   */
+  useEffect(() => {
+    const svg = document.querySelector('.cat-svg') as SVGSVGElement | null;
+    if (!svg) return;
+
+    // 重置所有 path 状态
+    svg.querySelectorAll('path[id^="Cat_"]').forEach((path) => {
+      path.classList.remove('active');
+      path.classList.add('inactive');
+    });
+
+    // 高亮选中的 path
+    if (selectedId) {
+      const activePath = svg.querySelector(`#${CSS.escape(selectedId)}`);
+      if (activePath) {
+        activePath.classList.remove('inactive');
+        activePath.classList.add('active');
+      }
+    }
+  }, [selectedId]);
+
   return (
-    <svg viewBox="0 0 540 471" width="100%">
-      <path
-        id="Cat_Skull"
-        d="M112.1,193.8L78.499999,184.19999 49.699999,163.39999 36.099996,165.79999 37.699995,189 17.699993,209 8.8999924,239.4 0.53584219,258.80968 22.135844,278.00967 40.899899,282.60003 63.299997,273.80002 77.699998,267.40002 87.299999,265.80002z"
-        fill={selectedId === 'Cat_Skull' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_Skull' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_Skull')}
-      />
-      <path
-        id="Cat_Spine"
-        d="M116.6,194.3L147.3,201.8 161.70001,205 193.70002,199.4 220.10004,201 260.10005,206.6 292.10005,210.60001 327.30004,205.80001 354.50006,205.80001 360.10005,246.60002 288.10003,247.40002 215.30002,244.20002 105.7,221.80001 112.9,193.8"
-        fill={selectedId === 'Cat_Spine' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_Spine' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_Spine')}
-      />
-      <path
-        id="Cat_FrontExtremities"
-        d="M106.2,224.7L212.9,249 241.69999,359.40001 250.5,434.59992 237.06505,466.89291 191.59432,463.33113 209.82927,422.23826 212.9,397.80002 204.1,377.00002 172.89999,352.20002 151.3,386.60003 126.5,421.80004 103.3,461.00004 68.899997,461.00004 68.099994,440.20003 91.299997,419.40003 102.5,409.80003 110.81422,393.04704 118.5,370.60003 119.67324,337.50197 123.67324,310.30196 119.3,288.20001 97.699997,270.60001 93.699997,262.6z"
-        fill={selectedId === 'Cat_FrontExtremities' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_FrontExtremities' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_FrontExtremities')}
-      />
-      <path
-        id="Cat_Thorax"
-        d="M216.6,250.3L288.90001,250.3 315.29999,347.4 244.9,357.79999z"
-        fill={selectedId === 'Cat_Thorax' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_Thorax' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_Thorax')}
-      />
-      <path
-        id="Cat_Abdomen"
-        d="M359,250.3L385.7,346.6 358.5,349.80001 318.49998,349.80001 289.69995,249.8z"
-        fill={selectedId === 'Cat_Abdomen' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_Abdomen' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_Abdomen')}
-      />
-      <path
-        id="Cat_Hip"
-        d="M358.2,204.7L428.10001,199.4 460.1,199.4 474.50001,190.59999 492.9,160.19998 480.1,100.99996 471.3,56.999947 485.69999,19.399934 510.49999,3.3999273 499.3,47.399943 504.1,98.59996 520.1,131.39997 517.69998,176.19999 502.49998,213.8 494.49998,231.40001 496.09998,267.40002 504.89998,305.00004 387.30001,345.80004z"
-        fill={selectedId === 'Cat_Hip' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_Hip' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_Hip')}
-      />
-      <path
-        id="Cat_RearExtremities"
-        d="M389.4,350.3L504.09999,312.19999 508.10001,327.39998 524.10001,348.19999 528.10004,384.99999 529.70004,409.00001 536.9,444.19999 513.82413,460.9568 494.62414,405.75681 479.3,376.99998 442.5,367.39998 449.7,398.59999 419.3,442.59999 400.89999,460.19999 372.09999,459.39998 366.50001,440.19998 396.1,419.39998 407.3,383.39998z"
-        fill={selectedId === 'Cat_RearExtremities' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Cat_RearExtremities' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Cat_RearExtremities')}
-      />
-    </svg>
+    <>
+      {/* 样式定义 */}
+      <style>
+        {`
+          .cat-container {
+            width: 100%;
+            height: 100%;
+            user-select: none;
+          }
+
+          .cat-svg {
+            width: 100%;
+            height: 100%;
+          }
+
+          /* 默认状态 */
+          .cat-svg path[id^="Cat_"] {
+            cursor: pointer;
+            transition: all 0.2s ease;
+          }
+
+          /* hover 效果 */
+          .cat-svg path[id^="Cat_"]:hover {
+            filter: drop-shadow(0 0 8px #ff4d4f);
+          }
+
+          /* 激活状态 */
+          .cat-svg .active {
+            fill: #f5222d;
+            stroke: #ff4d4f;
+            stroke-width: 2;
+          }
+
+          /* 非激活状态 */
+          .cat-svg .inactive {
+            fill: #ccc;
+            stroke: #ccc;
+            stroke-width: 2;
+          }
+        `}
+      </style>
+
+      {/* SVG 容器 */}
+      <div className="cat-container">
+        <CatSvg
+          className="cat-svg"
+          onClick={handleSvgClick}
+        />
+      </div>
+    </>
   );
 };
 
-export default DogBody;
+export default CatBody;

+ 102 - 72
src/components/DogBody.tsx

@@ -1,78 +1,108 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import DogSvg from '@/assets/imgs/Animal/dog.svg';
 
-const DogBody = ({
-  onPathClick,
-  selectedId,
-}: {
-  onPathClick: (id: string) => void;
+interface DogBodyProps {
   selectedId: string | null;
-}) => {
+  onPathClick: (id: string) => void;
+}
+
+const DogBody: React.FC<DogBodyProps> = ({ selectedId, onPathClick }) => {
+  /**
+   * 事件委托: 在 SVG 根元素处理所有点击
+   */
+  const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
+    const target = e.target as SVGElement;
+    
+    // 查找被点击的 path 元素
+    let pathElement: SVGPathElement | null = null;
+    
+    if (target.tagName === 'path') {
+      pathElement = target as SVGPathElement;
+    } else if (target.tagName === 'g') {
+      // 如果点击在 g 上,查找子 path
+      pathElement = target.querySelector('path[id^="Dog_"]');
+    }
+    
+    if (!pathElement || !pathElement.id) return;
+    
+    console.log('点击狗部位:', pathElement.id);
+    onPathClick(pathElement.id);
+  };
+
+  /**
+   * 控制 SVG 高亮状态
+   */
+  useEffect(() => {
+    const svg = document.querySelector('.dog-svg') as SVGSVGElement | null;
+    if (!svg) return;
+
+    // 重置所有 path 状态
+    svg.querySelectorAll('path[id^="Dog_"]').forEach((path) => {
+      path.classList.remove('active');
+      path.classList.add('inactive');
+    });
+
+    // 高亮选中的 path
+    if (selectedId) {
+      const activePath = svg.querySelector(`#${CSS.escape(selectedId)}`);
+      if (activePath) {
+        activePath.classList.remove('inactive');
+        activePath.classList.add('active');
+      }
+    }
+  }, [selectedId]);
+
   return (
-    <svg viewBox="0 0 540 471" width="100%">
-      <path
-        id="Dog_Skull"
-        d="M145.4,92.7L123.29999,70.599997 105.7,26.600002 96.899998,37.000001 88.099994,25.800002 80.89999,63.4 65.699994,76.199998 51.299997,87.399997 37.7,98.599996 21.700002,105.8 7.3000037,113.79999 6.5000036,128.19999 18.500003,139.4 24.900003,147.39999 39.300001,150.59999 56.9,151.39999 76.899999,145.79999 90.499998,148.99999z"
-        fill={selectedId === 'Dog_Skull' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_Skull' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_Skull')}
-      />
-      <path
-        id="Dog_Spine"
-        d="M119.8,126.3L198.5,175.4 286.5,175.4 374.5,174.60001 372.1,139.4 296.1,138.6 201.7,136.2 146.5,93.799999 121.7,121"
-        fill={selectedId === 'Dog_Spine' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_Spine' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_Spine')}
-      />
-      <path
-        id="Dog_FrontExtremities"
-        d="M86.2,157.5L122.5,209 139.3,252.2 153.7,277.8 170.50002,288.99999 182.50002,360.2 184.10002,400.20001 181.70002,417.00001 155.30001,428.2 155.30001,441.80001 185.70002,445.00002 200.90003,425.80003 212.10004,421.00003 224.10004,388.20002 225.70004,293.80001 199.30003,178.6 115.30001,125 88.099998,152.2"
-        fill={selectedId === 'Dog_FrontExtremities' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_FrontExtremities' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_FrontExtremities')}
-      />
-      <path
-        id="Dog_Thorax"
-        d="M202.2,176.7L290.5,181 308.9,274.60007 226.5,293.80008z"
-        fill={selectedId === 'Dog_Thorax' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_Thorax' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_Thorax')}
-      />
-      <path
-        id="Dog_Abdomen"
-        d="M291.8,179.9L377.69999,177.79999 396.89998,253.00021 312.89999,275.40028z"
-        fill={selectedId === 'Dog_Abdomen' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_Abdomen' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_Abdomen')}
-      />
-      <path
-        id="Dog_Hip"
-        d="M372.6,142.3L398.50001,253 495.29998,225 486.49999,185 508.89999,169 528.89999,143.4 536.89996,109 532.09996,84.200009 512.89997,57.800014 488.89998,38.600016 468.89998,30.600018 486.49998,57.800015 500.09997,87.400013 508.09997,113.80001 495.29997,124.20001 472.09998,138.60001 440.09999,144.20001 373.70001,137.80001"
-        fill={selectedId === 'Dog_Hip' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_Hip' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_Hip')}
-      />
-      <path
-        id="Dog_RearExtremities"
-        d="M401.4,257.5L498.49999,228.2 499.3,309.00001 525.37726,349.00985 521.80024,440.19984 488.60743,438.70827 490.60017,415.39986 467.38135,428.48481 448.28146,415.68482 470.49999,401.79997 475.40014,378.59998 466.49999,344.99998 436.89999,313.79999z"
-        fill={selectedId === 'Dog_RearExtremities' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Dog_RearExtremities' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Dog_RearExtremities')}
-      />
-    </svg>
+    <>
+      {/* 样式定义 */}
+      <style>
+        {`
+          .dog-container {
+            width: 100%;
+            height: 100%;
+            user-select: none;
+          }
+
+          .dog-svg {
+            width: 100%;
+            height: 100%;
+          }
+
+          /* 默认状态 */
+          .dog-svg path[id^="Dog_"] {
+            cursor: pointer;
+            transition: all 0.2s ease;
+          }
+
+          /* hover 效果 */
+          .dog-svg path[id^="Dog_"]:hover {
+            filter: drop-shadow(0 0 8px #ff4d4f);
+          }
+
+          /* 激活状态 */
+          .dog-svg .active {
+            fill: #f5222d;
+            stroke: #ff4d4f;
+            stroke-width: 2;
+          }
+
+          /* 非激活状态 */
+          .dog-svg .inactive {
+            fill: #ccc;
+            stroke: #ccc;
+            stroke-width: 2;
+          }
+        `}
+      </style>
+
+      {/* SVG 容器 */}
+      <div className="dog-container">
+        <DogSvg
+          className="dog-svg"
+          onClick={handleSvgClick}
+        />
+      </div>
+    </>
   );
 };
 

+ 99 - 68
src/components/EquineBody.tsx

@@ -1,77 +1,108 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import HorseSvg from '@/assets/imgs/Animal/horse.svg';
 
-interface HorseBodyProps {
+interface EquineBodyProps {
   selectedId: string;
   onPathClick: (id: string) => void;
 }
 
-const EquineBody: React.FC<HorseBodyProps> = ({ selectedId, onPathClick }) => {
+const EquineBody: React.FC<EquineBodyProps> = ({ selectedId, onPathClick }) => {
+  /**
+   * 事件委托: 在 SVG 根元素处理所有点击
+   */
+  const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
+    const target = e.target as SVGElement;
+    
+    // 查找被点击的 path 元素
+    let pathElement: SVGPathElement | null = null;
+    
+    if (target.tagName === 'path') {
+      pathElement = target as SVGPathElement;
+    } else if (target.tagName === 'g') {
+      // 如果点击在 g 上,查找子 path
+      pathElement = target.querySelector('path[id^="Equine_"]');
+    }
+    
+    if (!pathElement || !pathElement.id) return;
+    
+    console.log('点击马部位:', pathElement.id);
+    onPathClick(pathElement.id);
+  };
+
+  /**
+   * 控制 SVG 高亮状态
+   */
+  useEffect(() => {
+    const svg = document.querySelector('.horse-svg') as SVGSVGElement | null;
+    if (!svg) return;
+
+    // 重置所有 path 状态
+    svg.querySelectorAll('path[id^="Equine_"]').forEach((path) => {
+      path.classList.remove('active');
+      path.classList.add('inactive');
+    });
+
+    // 高亮选中的 path
+    if (selectedId) {
+      const activePath = svg.querySelector(`#${CSS.escape(selectedId)}`);
+      if (activePath) {
+        activePath.classList.remove('inactive');
+        activePath.classList.add('active');
+      }
+    }
+  }, [selectedId]);
+
   return (
-    <svg viewBox="0 0 540 471" width="100%">
-      <path
-        id="Equine_Head"
-        d="M149.3,49.5L129.1,37.1 103.8,26 87.4,22 69.9,3.4 65.1,3.5 65.1,15.2 69.2,24.7 54.8,35.4 43.9,49.2 38.7,59.9 17.4,80.7 2.4,98 -1.2,106.2 2,118.3 10.3,127 22,131.3 31.9,129.3 37.3,122.7 53.9,112.9 64.3,109.4 75.5,109.6 82.9,106.3 88,101.8 93.6,101.7 97.3,103.5z"
-        fill={selectedId === 'Equine_Head' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Head' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Head')}
-      />
-      <path
-        id="Equine_Spine"
-        d="M147.7,49.5L119.2,78.7 220.4,175.8 392.7,169.2 475.7,227.3C475.7,227.3,480.5,194,480.5,194L478.7,174.6 472.6,160.8 476.1,160.1 485.4,167.4 491.2,187.2 492,279 498.8,313.1 512.6,348 523.8,364.7 531.3,370.9 533.6,359.1 534,347.3 529,304 529.8,279.8 533,250.6 533.5,225.4 528.8,204.2 516.2,177.3 495.3,148.3 475,131.4 450.3,118.6 423.4,112.3 389.8,111.3 348,121.4 306.2,127.2 270.1,127.6 251.8,123.4 224.4,111.3 176,70.2z"
-        fill={selectedId === 'Equine_Spine' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Spine' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Spine')}
-      />
-      <path
-        id="Equine_Left_Forelimb"
-        d="M119,78.4L96.5,102.2 110.6,126.7 140.4,200.2 144.5,230.9 154.8,253 163.7,261.6 168,286.6 173.3,308.4 176.2,315.6 178.4,337 179.4,360 183.6,380.7 185.6,404.5 183.1,423 157.6,451.6 160.6,456 166.3,457.3 187.3,457.6 190.7,455.1 194.8,448.5 190,441.6 206,427.4 208.4,421 205.4,391.4 205.4,369.4 208.4,355.1 209.7,324.7 214,295 223.7,271 223,173z"
-        fill={selectedId === 'Equine_Left_Forelimb' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Left_Forelimb' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Left_Forelimb')}
-      />{' '}
-      <path
-        id="Equine_Left_Hindlimb"
-        d="M396.8,166L397,253 411.9,290.6 423.3,321.6 432.8,339.7 436.8,356.1 440,363.9 444.8,414.7 444.8,429.8 437.8,466.6 440,469.7 452.2,467.6 455,462 455.5,456 452.6,448.7 454.3,435 455.8,432.5 455.6,412.7 452.2,381.5 450.2,350.6 449.6,331.2 447.9,323.8 444.6,316.2 438.3,290 435.7,272.3 435.8,262.2 439,225.3z"
-        fill={selectedId === 'Equine_Left_Hindlimb' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Left_Hindlimb' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Left_Hindlimb')}
-      />{' '}
-      <path
-        id="Equine_Right_Forelimb"
-        d="M221.5,273L235,273 240,295 242.3,308 246.4,314.6 246.5,321 256.4,331.6 264.3,342.5 264.5,345.7 261.8,348.5 259,348 254,354 258,357 258,359.6 252,363 226,364 223,363 222,361 240,346 239,340 230.5,325 226,322.5 207.7,298.7 213,283z"
-        fill={selectedId === 'Equine_Right_Forelimb' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Right_Forelimb' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Right_Forelimb')}
-      />{' '}
-      <path
-        id="Equine_Right_Hindlimb"
-        d="M382.6,244.9L395.4,249.21768 414.6,273.1 445.3,320.8 455.5,340.7 447.5,393 444.6,435.7 442.4,449 439.8,453.5 435.8,455 429,465.5 431.2,472.8 429,479.5 423.3,481.6 411.3,481 399.4,478 398.8,475 420.9,440.6 427,372 424.8,363.8 424.6,354.3 424.6,348.3 419.1,336.9 411.8,311 403.6,278.4 395.7,264.4 388,254 380.6,246.9z"
-        fill={selectedId === 'Equine_Right_Hindlimb' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_Right_Hindlimb' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_Right_Hindlimb')}
-      />
-      <path
-        id="Equine_THORAX"
-        d="M221.2,173.4 L400.2,170.5  400,253 Q375,233,350,243 L221.2,253z"
-        fill={selectedId === 'Equine_THORAX' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Equine_THORAX' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Equine_THORAX')}
-      />
-    </svg>
+    <>
+      {/* 样式定义 */}
+      <style>
+        {`
+          .horse-container {
+            width: 100%;
+            height: 100%;
+            user-select: none;
+          }
+
+          .horse-svg {
+            width: 100%;
+            height: 100%;
+          }
+
+          /* 默认状态 */
+          .horse-svg path[id^="Equine_"] {
+            cursor: pointer;
+            transition: all 0.2s ease;
+          }
+
+          /* hover 效果 */
+          .horse-svg path[id^="Equine_"]:hover {
+            filter: drop-shadow(0 0 8px #ff4d4f);
+          }
+
+          /* 激活状态 */
+          .horse-svg .active {
+            fill: #f5222d;
+            stroke: #ff4d4f;
+            stroke-width: 2;
+          }
+
+          /* 非激活状态 */
+          .horse-svg .inactive {
+            fill: #ccc;
+            stroke: #ccc;
+            stroke-width: 2;
+          }
+        `}
+      </style>
+
+      {/* SVG 容器 */}
+      <div className="horse-container">
+        <HorseSvg
+          className="horse-svg"
+          onClick={handleSvgClick}
+        />
+      </div>
+    </>
   );
 };
 

+ 97 - 50
src/components/ExoticPets.tsx

@@ -1,4 +1,5 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import ExoticSvg from '@/assets/imgs/Animal/ExoticAnimal.svg';
 
 interface ExoticPetsProps {
   selectedId: string;
@@ -6,56 +7,102 @@ interface ExoticPetsProps {
 }
 
 const ExoticPets: React.FC<ExoticPetsProps> = ({ selectedId, onPathClick }) => {
+  /**
+   * 事件委托: 在 SVG 根元素处理所有点击
+   */
+  const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
+    const target = e.target as SVGElement;
+    
+    // 查找被点击的 path 元素
+    let pathElement: SVGPathElement | null = null;
+    
+    if (target.tagName === 'path') {
+      pathElement = target as SVGPathElement;
+    } else if (target.tagName === 'g') {
+      // 如果点击在 g 上,查找子 path
+      pathElement = target.querySelector('path[id^="Exotic_"]');
+    }
+    
+    if (!pathElement || !pathElement.id) return;
+    
+    console.log('点击异宠部位:', pathElement.id);
+    onPathClick(pathElement.id);
+  };
+
+  /**
+   * 控制 SVG 高亮状态
+   */
+  useEffect(() => {
+    const svg = document.querySelector('.exotic-svg') as SVGSVGElement | null;
+    if (!svg) return;
+
+    // 重置所有 path 状态
+    svg.querySelectorAll('path[id^="Exotic_"]').forEach((path) => {
+      path.classList.remove('active');
+      path.classList.add('inactive');
+    });
+
+    // 高亮选中的 path
+    if (selectedId) {
+      const activePath = svg.querySelector(`#${CSS.escape(selectedId)}`);
+      if (activePath) {
+        activePath.classList.remove('inactive');
+        activePath.classList.add('active');
+      }
+    }
+  }, [selectedId]);
+
   return (
-    <svg viewBox="0 0 540 471" width="100%">
-      <path
-        width={194.6}
-        height={92.2}
-        id="Exotic_Lizard"
-        d="M153.4,116.7L54.499994,112.2 28.899992,68.999997 56.099992,29.800003 120.89999,29.000003 176.09999,52.200002 215.29999,78.6 222.49999,108.2 204.89999,120.2 156.89999,117.8"
-        fill={selectedId === 'Exotic_Lizard' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Exotic_Lizard' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Exotic_Lizard')}
-      />
-      <path
-        id="Exotic_Birds"
-        d="M366.2,76.7L383.30001,26.599997 410.50003,4.9999964 444.90004,14.599996 443.30005,51.399997 444.90005,86.599997 461.70006,134.6 475.30008,189.00001 472.90008,208.20002 480.90008,269.80002 464.10007,278.60002 444.90006,253.80001 415.30004,193.8 380.10002,130.6 383.30002,101 368.10001,81.799999"
-        fill={selectedId === 'Exotic_Birds' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Exotic_Birds' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Exotic_Birds')}
-      />
-      <path
-        id="Exotic_Rabbit"
-        d="M242.2,175.9L242.2,141.79999 253.7,136.99999 268.9,142.59999 264.89999,169.79999 256.1,189 272.09999,197 289.70001,201.8 318.50002,209.80001 330.50004,234.60001 335.30003,258.60001 349.70002,269.80001 341.70001,287.40002 309.70001,299.40003 284.9,297.00002 247.3,293.00002C247.3,293.00002 237.7,270.60002 239.3,267.40002 240.9,264.20002 230.50013,241.80001 230.50013,238.60001 230.50013,235.40001 218.50013,221.00001 216.90013,217.8 215.30014,214.6 212.10014,197 212.10014,197z"
-        fill={selectedId === 'Exotic_Rabbit' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Exotic_Rabbit' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Exotic_Rabbit')}
-      />
-      <path
-        id="Exotic_Snake"
-        d="M57.403644,229.3638L98.503644,230.4638 104.1,287.40028 152.1,303.40035 170.49999,333.00052 196.09998,340.20056 229.69997,359.40067 230.49997,385.80081 207.29997,398.60088 173.69998,390.60085 100.89999,397.80089 47.300002,382.6008 30.500006,345.8006 45.700003,310.6004 64.1,282.60025 45.700003,246.60005 56.900001,229.7998"
-        fill={selectedId === 'Exotic_Snake' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Exotic_Snake' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Exotic_Snake')}
-      />
-      <path
-        id="Exotic_Turtle"
-        d="M314.36502,336.18226L355.46503,349.28226 380.10002,351.4 416.90005,341.79999 468.90008,347.39999 489.7001,369.80001 518.50012,409.80006 512.10012,431.40007 513.70013,467.39995 457.80299,469.03595 359.40294,465.83595 327.09046,394.00471 293.49044,359.60468 311.30002,337.00025"
-        fill={selectedId === 'Exotic_Turtle' ? '#f5222d' : '#ccc'}
-        stroke={selectedId === 'Exotic_Turtle' ? '#ff4d4f' : '#ccc'}
-        strokeWidth={2}
-        style={{ cursor: 'pointer' }}
-        onClick={() => onPathClick('Exotic_Turtle')}
-      />
-    </svg>
+    <>
+      {/* 样式定义 */}
+      <style>
+        {`
+          .exotic-container {
+            width: 100%;
+            height: 100%;
+            user-select: none;
+          }
+
+          .exotic-svg {
+            width: 100%;
+            height: 100%;
+          }
+
+          /* 默认状态 */
+          .exotic-svg path[id^="Exotic_"] {
+            cursor: pointer;
+            transition: all 0.2s ease;
+          }
+
+          /* hover 效果 */
+          .exotic-svg path[id^="Exotic_"]:hover {
+            filter: drop-shadow(0 0 8px #ff4d4f);
+          }
+
+          /* 激活状态 */
+          .exotic-svg .active {
+            fill: #f5222d;
+            stroke: #ff4d4f;
+            stroke-width: 2;
+          }
+
+          /* 非激活状态 */
+          .exotic-svg .inactive {
+            fill: #ccc;
+            stroke: #ccc;
+            stroke-width: 2;
+          }
+        `}
+      </style>
+
+      {/* SVG 容器 */}
+      <div className="exotic-container">
+        <ExoticSvg
+          className="exotic-svg"
+          onClick={handleSvgClick}
+        />
+      </div>
+    </>
   );
 };
 

Some files were not shown because too many files changed in this diff