lanxin 2 ngày trước cách đây
mục cha
commit
8c5dc0ecdb
68 tập tin đã thay đổi với 12823 bổ sung8926 xóa
  1. 19 0
      展示端/craco.config.js
  2. 7 3
      展示端/package.json
  3. 1 2
      展示端/public/index.html
  4. 0 164
      展示端/public/myData/data.js
  5. 21 9
      展示端/src/App.tsx
  6. BIN
      展示端/src/assets/img/bottomSearchBg.png
  7. BIN
      展示端/src/assets/img/bottomSearchBgBtn.png
  8. BIN
      展示端/src/assets/img/close.png
  9. BIN
      展示端/src/assets/img/finding.png
  10. BIN
      展示端/src/assets/img/flooImg.jpg
  11. BIN
      展示端/src/assets/img/found.png
  12. BIN
      展示端/src/assets/img/foundBg.png
  13. BIN
      展示端/src/assets/img/headLineBg.png
  14. BIN
      展示端/src/assets/img/infoContainerBg.png
  15. BIN
      展示端/src/assets/img/infoDetailAdd.png
  16. BIN
      展示端/src/assets/img/infoDetailBox.png
  17. BIN
      展示端/src/assets/img/infoItem.png
  18. BIN
      展示端/src/assets/img/introPic.png
  19. BIN
      展示端/src/assets/img/label.png
  20. BIN
      展示端/src/assets/img/label2.png
  21. BIN
      展示端/src/assets/img/mainInfoBg.png
  22. BIN
      展示端/src/assets/img/mainInfoBoxBg.png
  23. BIN
      展示端/src/assets/img/mainInfoList.png
  24. 1799 0
      展示端/src/assets/img/map.svg
  25. BIN
      展示端/src/assets/img/mapBg.png
  26. BIN
      展示端/src/assets/img/modelInfoBg.png
  27. BIN
      展示端/src/assets/img/modelPic.png
  28. BIN
      展示端/src/assets/img/prefixIcon.png
  29. BIN
      展示端/src/assets/img/relationBg.png
  30. BIN
      展示端/src/assets/img/relationTitle.png
  31. BIN
      展示端/src/assets/img/sceneThumb.png
  32. BIN
      展示端/src/assets/img/searchResultBg.png
  33. BIN
      展示端/src/assets/img/slide.png
  34. BIN
      展示端/src/assets/img/vr.png
  35. 1 2
      展示端/src/assets/styles/base.css
  36. 1 0
      展示端/src/components/LookDom/index.tsx
  37. 47 83
      展示端/src/pages/A1home/index.module.scss
  38. 464 135
      展示端/src/pages/A1home/index.tsx
  39. 82 0
      展示端/src/pages/A1home/util.ts
  40. 7 0
      展示端/src/pages/A2Layout/index.module.scss
  41. 15 0
      展示端/src/pages/A2Layout/index.tsx
  42. 0 9
      展示端/src/pages/A2scene/index.module.scss
  43. 0 32
      展示端/src/pages/A2scene/index.tsx
  44. 70 0
      展示端/src/pages/components/BottomSearch/index.module.scss
  45. 100 0
      展示端/src/pages/components/BottomSearch/index.tsx
  46. 11 0
      展示端/src/pages/components/Detail/RelationEcharts/index.module.scss
  47. 140 0
      展示端/src/pages/components/Detail/RelationEcharts/index.tsx
  48. 367 0
      展示端/src/pages/components/Detail/index.module.scss
  49. 220 0
      展示端/src/pages/components/Detail/index.tsx
  50. 161 0
      展示端/src/pages/components/Left/index.module.scss
  51. 164 0
      展示端/src/pages/components/Left/index.tsx
  52. 46 0
      展示端/src/pages/components/MapTab/index.module.scss
  53. 62 0
      展示端/src/pages/components/MapTab/index.tsx
  54. 115 0
      展示端/src/pages/components/Right/index.module.scss
  55. 75 0
      展示端/src/pages/components/Right/index.tsx
  56. 0 30
      展示端/src/store/action/A1list.ts
  57. 41 0
      展示端/src/store/action/clue.ts
  58. 89 0
      展示端/src/store/action/martyr.ts
  59. 6 4
      展示端/src/store/reducer/A1list.ts
  60. 4 2
      展示端/src/store/reducer/index.ts
  61. 28 0
      展示端/src/store/reducer/martyr.ts
  62. 61 0
      展示端/src/types/api/clue.d.ts
  63. 77 0
      展示端/src/types/api/martyr.d.ts
  64. 0 2
      展示端/src/types/declaration.d.ts
  65. 2 0
      展示端/src/types/index.d.ts
  66. 4 29
      展示端/src/utils/history.ts
  67. 5 15
      展示端/src/utils/http.ts
  68. 8511 8405
      展示端/yarn.lock

+ 19 - 0
展示端/craco.config.js

@@ -0,0 +1,19 @@
+const path = require('path')
+
+module.exports = {
+  webpack: {
+    configure: (webpackConfig, { env, paths }) => {
+      // 添加 SVG 处理规则,用 @svgr/webpack 加载
+      webpackConfig.module.rules.push({
+        test: /\.svg$/,
+        use: ['@svgr/webpack'],
+        include: path.resolve(__dirname, 'src') // 限制范围,可选
+      })
+      return webpackConfig
+    },
+    alias: {
+      // 配置 @ 指向 src 目录
+      '@': path.resolve(__dirname, 'src')
+    }
+  }
+}

+ 7 - 3
展示端/package.json

@@ -4,6 +4,8 @@
   "private": true,
   "dependencies": {
     "@ant-design/cssinjs": "^1.5.6",
+    "@craco/craco": "^7.1.0",
+    "@react-spring/web": "^10.0.1",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",
@@ -18,6 +20,7 @@
     "braft-editor": "^2.3.9",
     "braft-utils": "^3.0.12",
     "dayjs": "^1.11.10",
+    "echarts": "^6.0.0",
     "js-base64": "^3.7.3",
     "react": "^18.2.0",
     "react-countup": "^6.5.3",
@@ -30,13 +33,14 @@
     "redux-devtools-extension": "^2.13.9",
     "redux-thunk": "^2.4.1",
     "sass": "^1.55.0",
+    "svg-pan-zoom": "^3.6.2",
     "typescript": "^4.8.4",
     "web-vitals": "^2.1.4"
   },
   "scripts": {
-    "dev": "react-app-rewired start",
-    "build": "react-app-rewired build",
-    "test": "react-app-rewired test",
+    "dev": "craco start",
+    "build": "craco build",
+    "test": "craco test",
     "eject": "react-scripts eject"
   },
   "eslintConfig": {

+ 1 - 2
展示端/public/index.html

@@ -11,9 +11,8 @@
     />
     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
     <!-- <link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" /> -->
-    <script src="./myData/data.js"></script>
 
-    <title>博物中国</title>
+    <title>英烈寻亲-济南革命烈士寻亲中心</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 0 - 164
展示端/public/myData/data.js

@@ -1,164 +0,0 @@
-const serverUrlTemp = 'https://ysxwyzl.4dage.com'
-
-const infoTemo = {
-  serverUrl: serverUrlTemp,
-  swArr: [
-    {
-      id: 13,
-      name: '历代中央政权有效治理新疆的历史起点',
-      partOf: '西域都护府博物馆',
-      link: `${serverUrlTemp}/bwCN/scene/xyhdf/index.html`,
-      code: 'xyhdf',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 12,
-      name: '布达拉宫-来自高原的世界文化遗产',
-      partOf: '三星堆博物馆',
-      link: `${serverUrlTemp}/bwCN/scene/SG-Hu9rVKJBQHt/scene/index.html?m=SG-Hu9rVKJBQHt`,
-      code: 'SG-Hu9rVKJBQHt',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 11,
-      name: '百团大战纪念馆',
-      partOf: '红色基因库',
-      link: `${serverUrlTemp}/bwCN/scene/baiTuan/index.html`,
-      code: 'baiTuan',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 8,
-      name: '家园-中国少数民族传统体育文化展',
-      partOf: '中国民族博物馆',
-      link: `${serverUrlTemp}/bwCN/scene/SG-SkJEj1Vhf7l/index.html?m=SG-SkJEj1Vhf7l`,
-      code: 'SG-SkJEj1Vhf7l',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 10,
-      name: '渺渺大象-浙江古代造像艺术',
-      partOf: '平湖市博物馆',
-      link: `${serverUrlTemp}/bwCN/scene/mm1574/index.html?m=mm-1574`,
-      code: 'mm-1574',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 9,
-      name: '热的血-纪念五卅运动100周年文物史料专题展',
-      partOf: '中共一大纪念馆',
-      link: `${serverUrlTemp}/bwCN/scene/mm1567/index.html?m=mm-1567`,
-      code: 'mm-1567',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-
-    {
-      id: 7,
-      name: '深蓝宝藏—南海西北陆坡一二号沉船考古成果特展',
-      partOf: '中国(海南)南海博物馆 ',
-      link: `${serverUrlTemp}/bwCN/scene/mm1537/index.html?m=mm-1537`,
-      code: 'mm-1537',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 6,
-      name: '盛世永兴-萧山历史文化陈列展',
-      partOf: '萧山博物馆',
-      link: `${serverUrlTemp}/bwCN/scene/1390/index.html?m=1390`,
-      code: '1390',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 5,
-      name: '莞·藏-100件文物里的东莞故事',
-      partOf: '东莞市博物馆',
-      link: `${serverUrlTemp}/4dwall/spg.html?m=KJ-TRxKDdPYwKp&lang=zh`,
-      code: 'KJ-TRxKDdPYwKp',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 3,
-      name: '"宜子孙--汉代玉器集萃"展',
-      partOf: '良渚博物院 ',
-      link: `${serverUrlTemp}/bwCN/scene/mm1557/index.html?m=mm-1557`,
-      code: 'mm-1557',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 4,
-      name: '东方的起点',
-      partOf: '宁波博物院',
-      link: `${serverUrlTemp}/bwCN/scene/mm1541/index.html?m=mm-1541`,
-      code: 'mm-1541',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1, //属于第一个模板
-      isSW: true //属于轮播图
-    },
-    {
-      id: 2,
-      name: '观天下-大明的世界',
-      partOf: '南京博物院',
-      link: `${serverUrlTemp}/4dwall/spg.html?m=KJ-hEnSvVKE0wl&lang=zh`,
-      code: 'KJ-hEnSvVKE0wl',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1,
-      isSW: true
-    },
-    {
-      id: 1,
-      name: '灵影仙踪',
-      partOf: '上海博物馆',
-      link: `${serverUrlTemp}/4dwall/spg.html?m=SG-Yu3JBqaVsxP&lang=zh`,
-      code: 'SG-Yu3JBqaVsxP',
-      oldNum: 0,
-      newNum: 0,
-      changeSta: true,
-      loc: 1,
-      isSW: true
-    }
-  ]
-}

+ 21 - 9
展示端/src/App.tsx

@@ -2,21 +2,23 @@ import '@/assets/styles/base.css'
 // 关于路由
 import React, { useCallback, useEffect } from 'react'
 import { Router, Route, Switch } from 'react-router-dom'
-import history, { isMobileFu } from './utils/history'
+import history from './utils/history'
 import SpinLoding from './components/SpinLoding'
 import AsyncSpinLoding from './components/AsyncSpinLoding'
-
 import UpAsyncLoding from './components/UpAsyncLoding'
 import MessageCom from './components/Message'
-
+import LookDom from './components/LookDom'
+import { useSelector } from 'react-redux'
+import { RootState } from './store'
+import store from './store'
 import screenImg from '@/assets/img/landtip.png'
+import { Image } from 'antd'
 
 const A1home = React.lazy(() => import('./pages/A1home'))
-const A2scene = React.lazy(() => import('./pages/A2scene'))
 
 export default function App() {
   // 从仓库中获取查看图片的信息
-  // const lookBigImg = useSelector((state: RootState) => state.A0Layout.lookBigImg)
+  const lookBigImg = useSelector((state: RootState) => state.A0Layout.lookBigImg)
 
   const rootDomFu = useCallback(() => {
     const rootDom: HTMLDivElement = document.querySelector('#root')!
@@ -25,6 +27,17 @@ export default function App() {
     }
   }, [])
 
+  // 判断是手机端还是pc端
+  const isMobileFu = () => {
+    if (
+      window.navigator.userAgent.match(
+        /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
+      )
+    ) {
+      return true
+    } else return false
+  }
+
   useEffect(() => {
     window.addEventListener('resize', rootDomFu, true)
     rootDomFu()
@@ -37,7 +50,6 @@ export default function App() {
         <React.Suspense fallback={<SpinLoding />}>
           <Switch>
             {/* <Route path='/codeSucc/:id' component={A3codeSucc} /> */}
-            <Route path='/scene/:id' component={A2scene} />
             <Route path='/' component={A1home} />
           </Switch>
         </React.Suspense>
@@ -47,7 +59,7 @@ export default function App() {
       <AsyncSpinLoding />
 
       {/* 所有图片点击预览查看大图 */}
-      {/* <Image
+      <Image
         preview={{
           visible: lookBigImg.show,
           src: lookBigImg.url,
@@ -59,13 +71,13 @@ export default function App() {
             })
           }
         }}
-      /> */}
+      />
 
       {/* 上传附件的进度条元素 */}
       <UpAsyncLoding />
 
       {/* 查看视频音频 */}
-      {/* <LookDom /> */}
+      <LookDom />
 
       {/* antd 轻提示 ---兼容360浏览器 */}
       <MessageCom />

BIN
展示端/src/assets/img/bottomSearchBg.png


BIN
展示端/src/assets/img/bottomSearchBgBtn.png


BIN
展示端/src/assets/img/close.png


BIN
展示端/src/assets/img/finding.png


BIN
展示端/src/assets/img/flooImg.jpg


BIN
展示端/src/assets/img/found.png


BIN
展示端/src/assets/img/foundBg.png


BIN
展示端/src/assets/img/headLineBg.png


BIN
展示端/src/assets/img/infoContainerBg.png


BIN
展示端/src/assets/img/infoDetailAdd.png


BIN
展示端/src/assets/img/infoDetailBox.png


BIN
展示端/src/assets/img/infoItem.png


BIN
展示端/src/assets/img/introPic.png


BIN
展示端/src/assets/img/label.png


BIN
展示端/src/assets/img/label2.png


BIN
展示端/src/assets/img/mainInfoBg.png


BIN
展示端/src/assets/img/mainInfoBoxBg.png


BIN
展示端/src/assets/img/mainInfoList.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1799 - 0
展示端/src/assets/img/map.svg


BIN
展示端/src/assets/img/mapBg.png


BIN
展示端/src/assets/img/modelInfoBg.png


BIN
展示端/src/assets/img/modelPic.png


BIN
展示端/src/assets/img/prefixIcon.png


BIN
展示端/src/assets/img/relationBg.png


BIN
展示端/src/assets/img/relationTitle.png


BIN
展示端/src/assets/img/sceneThumb.png


BIN
展示端/src/assets/img/searchResultBg.png


BIN
展示端/src/assets/img/slide.png


BIN
展示端/src/assets/img/vr.png


+ 1 - 2
展示端/src/assets/styles/base.css

@@ -54,7 +54,6 @@ textarea {
   width: 240px;
 }
 #root {
-  max-width: 500px;
   margin: 0 auto;
   width: 100vw;
   height: 100vh;
@@ -221,4 +220,4 @@ textarea {
     opacity: 1;
     pointer-events: auto;
   }
-}
+}

+ 1 - 0
展示端/src/components/LookDom/index.tsx

@@ -9,6 +9,7 @@ function LookDom() {
   const { src, type, flag } = useSelector(
     (state: RootState) => state.A0Layout.lookDom
   );
+  console.log(src, type, flag, '123');
   return (
     <div
       className={styles.LookDom}

+ 47 - 83
展示端/src/pages/A1home/index.module.scss

@@ -1,98 +1,62 @@
 .A1home {
   position: relative;
-
+  overflow: hidden;
   :global {
-    .A1main {
+    .map {
       width: 100%;
       height: 100%;
-      overflow-y: auto;
-
-      .A1top {
-        img {
-          width: 100%;
-          height: auto;
-          pointer-events: none;
-        }
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
       }
-      .A1tit {
-        font-size: 22px;
-        font-weight: 700;
-        margin-bottom: 16px;
-      }
-
-      .A1box1 {
-        padding: 24px;
-        padding-bottom: 0;
-        .A1_1list {
-          width: 100%;
-          height: auto;
-          overflow-x: auto;
-          white-space: nowrap;
-          display: inline-block;
-          padding-bottom: 24px;
-          border-bottom: 1px solid #ccc;
-          .A1_1row {
-            width: 40%;
-            height: auto;
-            display: inline-block;
-            margin-right: 15px;
-            vertical-align: top;
-            & > img {
-              cursor: pointer;
-              width: 100%;
-              height: auto;
-            }
-            .A1_1row1 {
-              font-weight: 700;
-              font-size: 16px;
-              margin: 10px 0 2px;
-              width: 100%;
-              white-space: break-spaces;
-              word-wrap: break-word;
-            }
-            .A1_1row2 {
-              color: #9a9a9a;
-              width: 100%;
-              white-space: break-spaces;
-              word-wrap: break-word;
-            }
-            .A1_1row3 {
-              display: flex;
-              align-items: center;
-              margin-top: 3px;
-              color: #9a9a9a;
-              transition: all 1s;
-            }
-            .A1_1row3Ac {
-              color: var(--themeColor);
-              font-weight: 700;
+      #mapSvg {
+        .svg-pan-zoom_viewport {
+          path {
+            // stroke: transparent;
+            &:hover {
+              stroke: #ff6600;
+              stroke-width: 2.5px;
+              transition: all 0.3s ease-in-out;
             }
           }
+          [id^="tap-"] {
+            pointer-events: none;
+          }
         }
       }
+    }
 
-      .FlooImg {
-        padding: 24px;
-      }
-
-      .A1BottomTxt {
-        text-align: center;
-        color: #9a9a9a;
-      }
+    .headLine {
+      position: absolute;
+      top: 0;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 100%;
+      object-fit: contain;
+      pointer-events: none;
+      transition: all 0.5s ease-in-out;
+    }
 
-      // 公司logo
-      .A1logo {
-        margin-top: 50px;
-        pointer-events: none;
-        height: 60px;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        img {
-          pointer-events: none;
-          height: 30px;
-        }
-      }
+    .animatedH {
+      transform: translate(-50%, -100px);
+      transition: all 0.5s ease-in-out;
+    }
+    .animatedL {
+      transform: translate(-350px, 0);
+      transition: all 0.5s ease-in-out;
+    }
+    .animatedR {
+      transform: translate(450px, 0);
+      transition: all 0.5s ease-in-out;
+    }
+    .animatedB {
+      transform: translate(-50%, 100px);
+      transition: all 0.5s ease-in-out;
+    }
+    .animatedD {
+      transform: translate(0, 0);
+      transition: all 0.3s ease-in-out;
     }
   }
 }

+ 464 - 135
展示端/src/pages/A1home/index.tsx

@@ -1,162 +1,491 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
 import styles from './index.module.scss'
-import { Swiper } from 'antd-mobile'
-import { A1listType, myInfo } from '@/utils/history'
-import classNames from 'classnames'
-import { EyeOutlined } from '@ant-design/icons'
-import CountUp from 'react-countup'
-import { A1_APIgetNumList } from '@/store/action/A1list'
-import logoImg from '../../assets/img/logo.png'
-import flooImg from '../../assets/img/flooImg.jpg'
-
-type NumListRowType = {
-  num: string
-  showVisit: number
-}
+import Left from '@/pages/components/Left'
+import Right from '@/pages/components/Right'
+import BottomSearch from '@/pages/components/BottomSearch'
+import MapTab from '@/pages/components/MapTab'
+import Detail from '@/pages/components/Detail'
+import { ReactComponent as MapSvg } from '@/assets/img/map.svg'
+import svgPanZoom from 'svg-pan-zoom'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+  Martyr_APIgetList,
+  Martyr_APIgetRelativeList,
+  Martyr_APIgetClueList,
+  Martyr_APIgetDetail,
+  Martyr_APIgetRelationList
+} from '@/store/action/martyr'
+import { RootState } from '@/store'
+import { MartyrItem, RelativeItem, RelationShipItem } from '@/types/api/martyr'
+import { ClueItem } from '@/types/api/clue'
+import { cityIdToName, addDefs, nameToCityId } from '@/pages/A1home/util'
 
 function A1home() {
-  const [list, setList] = useState<A1listType[]>([])
+  const svgRef = useRef<SVGSVGElement>(null)
+  const panZoomInstance = useRef<ReturnType<typeof svgPanZoom> | null>(null)
+  const [isAddClassName, setIsAddClassName] = useState(false)
+  const [curCityId, setCurCityId] = useState('')
+  const [isDragging, setIsDragging] = useState(false)
+  const [enableTooltipEvents, setEnableTooltipEvents] = useState(true)
+  const [relativeList, setRelativeList] = useState<Record<string, RelativeItem[]>>({})
+  const [clueList, setClueList] = useState<Record<string, ClueItem[]>>({})
+  const [martyrDetail, setMartyrDetail] = useState<MartyrItem>()
+  const [relationList, setRelationList] = useState<RelationShipItem[]>()
+
+  const dispatch = useDispatch()
+  // 获取烈士列表,亲属列表,线索列表,烈士详情,人物关系
+  const getListFu = useCallback(
+    () =>
+      dispatch(
+        Martyr_APIgetList({
+          nativeProvince: '山东省'
+        })
+      ),
+    [dispatch]
+  )
+  const getRelativeList = useCallback(
+    async () =>
+      await Martyr_APIgetRelativeList({
+        nativeProvince: '山东省'
+      }),
+    []
+  )
+  const getClueList = useCallback(
+    async () =>
+      await Martyr_APIgetClueList({
+        nativeProvince: '山东省'
+      }),
+    []
+  )
+  const getMartyrDetail = useCallback(
+    async (id: number) =>
+      await Martyr_APIgetDetail({
+        id
+      }),
+    []
+  )
+  const getRelationList = useCallback(
+    async (id: number) =>
+      await Martyr_APIgetRelationList({
+        martyrId: id
+      }),
+    []
+  )
+
+
+  const { list: listAll } = useSelector((state: RootState) => state.Martyr.tableInfo)
+  const martyrListByCity = useMemo(
+    () =>
+      listAll.reduce((acc, current) => {
+        const key = current.nativeCity
+        if (!acc[key]) acc[key] = []
+        acc[key].push(current)
+        return acc
+      }, {} as Record<string, typeof listAll>),
+    [listAll]
+  )
+
+  console.log(martyrListByCity, 'relativeList', relativeList)
 
-  const listRef = useRef<A1listType[]>([])
   useEffect(() => {
-    listRef.current = list
-  }, [list])
-
-  // 获取全部访问量
-  const getNumListFu = useCallback(async (base?: boolean) => {
-    // 参数 base =》第一次进页面
-
-    const res = await A1_APIgetNumList()
-    if (res.code === 0) {
-      const arr: NumListRowType[] = res.data
-
-      const listTemp = [...myInfo.swArr]
-
-      arr.forEach(v1 => {
-        listTemp.forEach(v2 => {
-          if (v2.code === v1.num) {
-            if (!base) {
-              const listRefNum = listRef.current.find(c => c.code === v1.num)!.newNum
-              v2.oldNum = listRefNum
-              if (listRefNum !== v1.showVisit) v2.changeSta = true
+    getListFu()
+    getRelativeList().then(res => {
+      setRelativeList(
+        res.data.reduce((acc: Record<string, RelativeItem[]>, current: RelativeItem) => {
+          const key = current.city
+          if (!acc[key]) acc[key] = []
+          acc[key].push(current)
+          return acc
+        }, {} as Record<string, typeof listAll>)
+      )
+    })
+    getClueList().then(res => {
+      setClueList(
+        res.data.reduce((acc: Record<string, ClueItem[]>, current: ClueItem) => {
+          const key = current.city
+          if (!acc[key]) acc[key] = []
+          acc[key].push(current)
+          return acc
+        }, {} as Record<string, typeof listAll>)
+      )
+    })
+  }, [getClueList, getListFu, getRelativeList])
+
+  // 聚焦到指定城市
+  const focusOnCity = useCallback((cityId: string) => {
+    if (panZoomInstance.current && svgRef.current) {
+      const zoomLevel = 1.8
+      const svgPoint = svgRef.current.createSVGPoint()
+      const city = svgRef.current.querySelector(`#${cityId}`)?.querySelector('ellipse')!
+      svgPoint.x = city?.cx.baseVal.value
+      svgPoint.y = city?.cy.baseVal.value
+      const transformedPoint = svgPoint.matrixTransform(svgRef.current.getCTM()!)
+      const { width: viewportWidth, height: viewportHeight } =
+        panZoomInstance.current.getSizes().viewBox
+      const panX = viewportWidth / 2 - transformedPoint.x * zoomLevel
+      const panY = viewportHeight / 2 - transformedPoint.y * zoomLevel
+      const viewport = svgRef.current.querySelector(
+        '.svg-pan-zoom_viewport'
+      ) as SVGGElement
+      viewport.style.transition = 'all 1s ease-in-out'
+      panZoomInstance.current.zoom(zoomLevel)
+      panZoomInstance.current.pan({ x: panX, y: panY })
+
+      setTimeout(() => {
+        viewport.style.transition = 'all 0.05s linear'
+        setIsDragging(false)
+      }, 1000)
+    }
+  }, [])
+
+  // 添加tooltip到指定城市下方
+  const addTooltip = useCallback((cityId: string, tooltipText: string) => {
+    if (svgRef.current) {
+      const city = svgRef.current.querySelector(`#${cityId}`)?.querySelector('ellipse')!
+
+      // 创建提示框元素
+      const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g')
+      tooltip.id = `${cityId}Tooltip`
+      const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
+      const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
+
+      // 设置初始属性(修改以下部分)
+      rect.setAttribute('fill', 'url(#jinanGradient)') // 使用渐变引用
+      // rect.setAttribute('stroke', '#6d330fff')
+      rect.setAttribute('rx', '10')
+      rect.setAttribute('width', '100')
+      rect.setAttribute('height', '20')
+      text.setAttribute('text-anchor', 'middle')
+      text.setAttribute('dominant-baseline', 'middle')
+      text.setAttribute('fill', '#F5EEED')
+      text.setAttribute('font-size', '6px')
+      text.setAttribute('font-weight', '600')
+      text.textContent = tooltipText
+
+      // 坐标
+      const cityCX = city?.cx.baseVal.value
+      const cityCY = city?.cy.baseVal.value
+
+      // 计算提示框位置(城市下方)
+      rect.setAttribute('x', (cityCX - 50).toString()) // 100宽度居中
+      rect.setAttribute('y', (cityCY + 20).toString()) // 下移20px
+      text.setAttribute('x', cityCX.toString())
+      text.setAttribute('y', (cityCY + 30).toString()) // 文字在矩形内居中
+
+      tooltip.appendChild(rect)
+      tooltip.appendChild(text)
+
+      // 添加到 SVG 的 viewport 中
+      const viewport = svgRef.current.querySelector('.svg-pan-zoom_viewport')
+      viewport?.appendChild(tooltip)
+
+      // 隐藏tooltip,待mouseEnter再显示
+      tooltip.style.opacity = '0'
+      tooltip.style.pointerEvents = 'none'
+      tooltip.style.transition = 'all 0.3s ease-in-out'
+    }
+  }, [])
+
+  // 更改tooltip文本内容
+  const changeTooltipText = useCallback((cityId: string, text: string) => {
+    if (svgRef.current) {
+      const tooltip = svgRef.current.querySelector(`#${cityId}Tooltip`) as SVGGElement
+      if (tooltip) {
+        const textElement = tooltip.querySelector('text')
+        tooltip.style.opacity = '1'
+        const cityDom = svgRef.current
+          .querySelector('g')
+          ?.querySelector('g')
+          ?.querySelectorAll('path')
+        cityDom?.forEach(item => {
+          item.removeEventListener('mouseleave', (e: MouseEvent) => {
+            const target = e.target as SVGElement
+            const tooltip = svgRef.current?.querySelector(`#tap-${target.id}Tooltip`)
+            if (tooltip) {
+              ; (tooltip as HTMLElement).style.opacity = '0'
             }
-            v2.newNum = v1.showVisit
-          }
+          })
         })
-      })
+        if (textElement) {
+          textElement.textContent = text
+        }
+      }
+    }
+  }, [])
 
-      setList(listTemp)
+  // 初始化添加tooltip
+  const initialTooltip = useCallback(
+    (martyrListByCity: Record<string, MartyrItem[]>) => {
+      if (svgRef.current) {
+        // 去除原来的tooltip
+        svgRef.current.querySelectorAll('g[id$="Tooltip"]').forEach(item => {
+          item.remove()
+        })
+
+        Object.keys(martyrListByCity).forEach(item => {
+          console.log(martyrListByCity[item][0].cityId)
+          addTooltip(
+            `${martyrListByCity[item][0].cityId}`,
+            `烈士 ${martyrListByCity[item].length} | 亲属 ${relativeList[item]?.length || 0
+            } | 线索 ${clueList[item]?.length || 0}`
+          )
+        })
+      }
+    },
+    [addTooltip, clueList, relativeList]
+  )
+
+  // 鼠标移入移出点击城市版块
+  const mouseEnter = useCallback((e: MouseEvent) => {
+    const target = e.target as SVGElement
+    const tooltip = svgRef.current?.querySelector(`#tap-${target.id}Tooltip`)
+    if (tooltip) {
+      ; (tooltip as HTMLElement).style.opacity = '1'
+    }
+  }, [])
+  const mouseLeave = useCallback((e: MouseEvent) => {
+    const target = e.target as SVGElement
+    const tooltip = svgRef.current?.querySelector(`#tap-${target.id}Tooltip`)
+    if (tooltip) {
+      ; (tooltip as HTMLElement).style.opacity = '0'
     }
   }, [])
+  const clickCity = useCallback(
+    (e: MouseEvent) => {
+      if (isDragging) {
+        // 添加拖拽状态判断
+        setIsDragging(false)
+        return
+      }
+      const target = e.target as SVGElement
+      console.log(target.id)
+      focusOnCity(`tap-${target.id}`)
+      setTimeout(() => {
+        if (martyrListByCity[cityIdToName[`tap-${target.id}`]]) {
+          setCurCityId(`tap-${target.id}`)
+        } else {
+          setCurCityId('')
+        }
 
-  useEffect(() => {
-    getNumListFu(true)
-  }, [getNumListFu])
+      }, 800)
+    },
+    [focusOnCity, isDragging, martyrListByCity]
+  )
 
-  // 定时发送
-  const timeRef = useRef(-1)
+  // 绘制箭头
+  const drawArrows = useCallback((startCityId: string, targetCityIds: string[]) => {
+    if (!svgRef.current) return
 
-  useEffect(() => {
-    clearInterval(timeRef.current)
-    timeRef.current = window.setInterval(() => {
-      getNumListFu()
-    }, 120000)
-    return () => {
-      clearInterval(timeRef.current)
+    // 清除旧箭头
+    svgRef.current.querySelectorAll('.connection-arrow').forEach(arrow => arrow.remove())
+
+    const startCity = svgRef.current
+      .querySelector(`#${startCityId}`)
+      ?.querySelector('ellipse')
+    if (!startCity) return
+
+    // 获取起点坐标
+    const startX = startCity.cx.baseVal.value
+    const startY = startCity.cy.baseVal.value
+
+    targetCityIds.forEach(targetCityId => {
+      const targetCity = svgRef.current
+        ?.querySelector(`#${targetCityId}`)
+        ?.querySelector('ellipse')
+      if (!targetCity) return
+
+      // 获取终点坐标
+      const endX = targetCity.cx.baseVal.value
+      const endY = targetCity.cy.baseVal.value
+
+      // 修改弯曲幅度计算
+      const dx = endX - startX
+      const dy = endY - startY
+      const distance = Math.sqrt(dx * dx + dy * dy) // 计算两点距离
+      const curvature = distance * 0.2 // 动态弯曲幅度(原50px改为距离的20%)
+      const angle = Math.atan2(dy, dx)
+
+      // 调整控制点计算(增加最小弯曲限制)
+      const minCurvature = 30 // 最小弯曲幅度
+      const finalCurvature = Math.max(curvature, minCurvature)
+      const controlX =
+        (startX + endX) / 2 + finalCurvature * Math.cos(angle - Math.PI / 2)
+      const controlY =
+        (startY + endY) / 2 + finalCurvature * Math.sin(angle - Math.PI / 2)
+
+      // 创建路径
+      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
+      path.classList.add('connection-arrow')
+      path.setAttribute(
+        'd',
+        `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`
+      )
+      path.setAttribute('fill', 'none')
+      path.setAttribute('stroke', '#ff6600')
+      path.setAttribute('stroke-width', '1.5')
+      path.setAttribute('stroke-dasharray', '5,2')
+      path.setAttribute('marker-end', 'url(#arrowhead)')
+
+      // 添加到viewport
+      const viewport = svgRef?.current?.querySelector('.svg-pan-zoom_viewport')
+      // viewport?.appendChild(path);
+      // 插入在第一个tooltip前
+      viewport?.insertBefore(path, viewport.querySelector('g[id$="Tooltip"]'))
+    })
+  }, [])
+
+  // 删除箭头
+  const removeArrows = useCallback(() => {
+    if (svgRef.current) {
+      svgRef.current
+        .querySelectorAll('.connection-arrow')
+        .forEach(arrow => arrow.remove())
     }
-  }, [getNumListFu])
-
-  // 动画是否在进行中
-  const moveStaFu = useCallback(
-    (code: string) => {
-      const arr = [...list]
-      list.forEach(v => {
-        if (v.changeSta && v.code === code) v.changeSta = false
+  }, [])
+
+  // 点击进入烈士详情
+  const handleItemClick = useCallback(
+    (cityId: string, name: string, martyrId: number) => {
+      getMartyrDetail(martyrId).then(res => {
+        setMartyrDetail(res.data)
+        const arrowToCity = res.data.kinship.map((item: any) => nameToCityId[item.city]).filter((item: any) => item?.city !== res.data.nativeCity)
+        console.log(arrowToCity, 'arrowToCity')
+        drawArrows(cityId, arrowToCity)
+      })
+      getRelationList(martyrId).then(res => {
+        setRelationList(res.data)
       })
-      setList(arr)
+      setIsAddClassName(true)
+      focusOnCity(cityId)
+      changeTooltipText(cityId, name)
+      setEnableTooltipEvents(false)
+
     },
-    [list]
+    [changeTooltipText, drawArrows, focusOnCity, getMartyrDetail, getRelationList]
   )
 
-  // 新窗口打开页面
-  const openPage = useCallback((url: string) => {
-    window.open(url, '_blank')
-  }, [])
+  // 设置拖拽
+  useEffect(() => {
+    if (svgRef.current) {
+      // 动态设置 viewBox 属性
+      svgRef.current.setAttribute('viewBox', '0 0 1420 945')
+      // 初始化 svg-pan-zoom
+      svgRef.current.style.width = '100%'
+      svgRef.current.style.height = '100%'
+      panZoomInstance.current = svgPanZoom(svgRef.current, {
+        zoomEnabled: true,
+        panEnabled: true,
+        controlIconsEnabled: false,
+        fit: true,
+        center: true,
+        onPan: () => {
+          setIsDragging(true)
+          setCurCityId('')
+        },
+        onZoom: () => setIsDragging(true)
+      })
+
+      // 组件加载完成后,将济南坐标放到屏幕中心
+      focusOnCity('tap-jinan')
+      const viewport = svgRef.current.querySelector(
+        '.svg-pan-zoom_viewport'
+      ) as SVGGElement
+      if (viewport) {
+        viewport.addEventListener('click', e => {
+          setIsAddClassName(false)
+          setEnableTooltipEvents(true)
+          removeArrows()
+        })
+        viewport.dispatchEvent(new MouseEvent('click'))
+      }
+    }
+
+    return () => {
+      // 组件卸载时销毁实例
+      if (panZoomInstance.current) {
+        panZoomInstance.current.destroy()
+      }
+    }
+  }, [focusOnCity, initialTooltip, removeArrows])
 
-  // 跳甲方链接
-  const toNewUrl = useCallback(() => {
-    window.open(
-      'https://m.cctvnews.cctv.com/collect/index.html?actNumber=15091951045133564423',
-      '_blank'
-    )
+  // 初始添加tooltip
+  useEffect(() => {
+    const viewport = svgRef?.current?.querySelector(
+      '.svg-pan-zoom_viewport'
+    ) as SVGGElement
+    if (martyrListByCity && viewport) {
+      viewport.addEventListener('click', e => {
+        initialTooltip(martyrListByCity)
+      })
+    }
+    viewport.dispatchEvent(new MouseEvent('click'))
+    viewport.removeEventListener('click', e => {
+      initialTooltip(martyrListByCity)
+    })
+  }, [martyrListByCity, initialTooltip])
+
+  // 鼠标移入移出点击事件
+  useEffect(() => {
+    if (svgRef.current) {
+      const cityDom = svgRef.current
+        .querySelector('g')
+        ?.querySelector('g')
+        ?.querySelectorAll('path')
+      cityDom?.forEach(item => {
+        item.removeEventListener('mouseenter', mouseEnter)
+        item.removeEventListener('mouseleave', mouseLeave)
+        item.removeEventListener('click', clickCity)
+
+        if (enableTooltipEvents) {
+          item.addEventListener('mouseenter', mouseEnter)
+          item.addEventListener('mouseleave', mouseLeave)
+          item.addEventListener('click', clickCity)
+        }
+      })
+      return () => {
+        cityDom?.forEach(item => {
+          item.removeEventListener('mouseenter', mouseEnter)
+          item.removeEventListener('mouseleave', mouseLeave)
+          item.removeEventListener('click', clickCity)
+        })
+      }
+    }
+  }, [addTooltip, enableTooltipEvents, mouseEnter, mouseLeave, clickCity])
+
+  // 在useEffect中添加箭头标记定义
+  useEffect(() => {
+    if (svgRef.current) {
+      addDefs(svgRef)
+    }
   }, [])
 
   return (
     <div className={styles.A1home}>
-      <div className='A1main'>
-        <div className='A1top'>
-          <Swiper loop autoplay>
-            {myInfo.swArr
-              .filter(v => v.isSW)
-              .map(item => (
-                <Swiper.Item key={item.id}>
-                  <div
-                    //  onClick={() => history.push(`/scene/${item.id}`)}
-                    onClick={() => openPage(item.link)}
-                  >
-                    <img
-                      src={`${myInfo.serverUrl}/bwCN/myData/img/${item.id}h.jpg`}
-                      alt=''
-                    />
-                  </div>
-                </Swiper.Item>
-              ))}
-          </Swiper>
-        </div>
-
-        <div className='A1box1'>
-          <div className='A1tit'>展览精选</div>
-          <div className='A1_1list'>
-            {list
-              .filter(v => v.loc === 1)
-              .map(item => (
-                <div className='A1_1row' key={item.id}>
-                  <img
-                    src={`${myInfo.serverUrl}/bwCN/myData/img/${item.id}s.jpg`}
-                    alt=''
-                    // onClick={() => history.push(`/scene/${item.id}`)}
-                    onClick={() => openPage(item.link)}
-                  />
-                  <div className='A1_1row1'>{item.name}</div>
-                  <div className='A1_1row2'>{item.partOf}</div>
-                  <div
-                    className={classNames('A1_1row3', item.changeSta ? 'A1_1row3Ac' : '')}
-                  >
-                    <EyeOutlined rev={undefined} />
-                    &nbsp;
-                    <CountUp
-                      onEnd={() => moveStaFu(item.code)}
-                      start={item.oldNum}
-                      end={item.newNum}
-                    />
-                  </div>
-                </div>
-              ))}
-          </div>
-        </div>
-
-        <div className='FlooImg' onClick={toNewUrl}>
-          <img src={flooImg} alt='' />
-        </div>
-
-        {/* <div onClick={() => getNumListFu()}>15456465</div> */}
-        <div className='A1BottomTxt'>持续上新,敬请期待</div>
-
-        {/* 公司logo */}
-        <div className='A1logo'>
-          <img src={logoImg} alt='' />
-        </div>
+      <div className='map'>
+        <MapSvg ref={svgRef} id='mapSvg' />
+        {curCityId && (
+          <MapTab
+            cityId={curCityId}
+            setCityId={setCurCityId}
+            clueList={clueList}
+            relativeList={relativeList}
+            martyrListByCity={martyrListByCity}
+            handleItemClick={handleItemClick}
+          />
+        )}
       </div>
+      <Left classN={isAddClassName ? 'animatedL' : ''} />
+      <Right
+        classN={isAddClassName ? 'animatedR' : ''}
+        handleItemClick={handleItemClick}
+      />
+      <Detail relationList={relationList} martyrDetail={martyrDetail} classN={isAddClassName ? 'animatedD' : ''} />
+      <img
+        className={isAddClassName ? 'headLine animatedH' : 'headLine'}
+        src={require('@/assets/img/headLineBg.png')}
+        alt=''
+      />
+      <BottomSearch handleItemClick={handleItemClick} classN={isAddClassName ? 'animatedB' : ''} />
     </div>
   )
 }

+ 82 - 0
展示端/src/pages/A1home/util.ts

@@ -0,0 +1,82 @@
+
+
+export const cityIdToName: Record<string, string> = {
+  'tap-jinan': '济南市',
+  'tap-linyi': '临沂市',
+  'tap-weifang': '潍坊市',
+  'tap-binzhou': '滨州市',
+  'tap-dezhou': '德州市',
+  'tap-heze': '菏泽市',
+  'tap-jining': '济宁市',
+  'tap-dongying': '东营市',
+  'tap-qingdao': '青岛市',
+  'tap-zibo': '淄博市',
+  'tap-yantai': '烟台市',
+  'tap-weihai': '威海市',
+  'tap-rizhao': '日照市',
+  'tap-zaozhuang': '枣庄市',
+  'tap-liaocheng': '聊城市',
+  'tap-taian': '泰安市'
+}
+
+export const nameToCityId :Record<string, string> ={
+  '济南市': 'tap-jinan',
+  '临沂市': 'tap-linyi',
+  '潍坊市': 'tap-weifang',
+  '滨州市': 'tap-binzhou',
+  '德州市': 'tap-dezhou',
+  '菏泽市': 'tap-heze',
+  '济宁市': 'tap-jining',
+  '东营市': 'tap-dongying',
+  '青岛市': 'tap-qingdao',
+  '淄博市': 'tap-zibo',
+  '烟台市': 'tap-yantai',
+  '威海市': 'tap-weihai',
+  '日照市': 'tap-rizhao',
+  '枣庄市': 'tap-zaozhuang',
+  '聊城市': 'tap-liaocheng',
+  '泰安市': 'tap-taian',
+}
+
+export const addDefs =(svgRef: React.RefObject<SVGSVGElement>)=>{
+   // 添加箭头标记定义
+      const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
+      const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker')
+      marker.id = 'arrowhead'
+      marker.setAttribute('markerWidth', '10')
+      marker.setAttribute('markerHeight', '10')
+      marker.setAttribute('refX', '9')
+      marker.setAttribute('refY', '3')
+      marker.setAttribute('orient', 'auto')
+      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
+      path.setAttribute('d', 'M0,0 L0,6 L9,3 z')
+      path.setAttribute('fill', '#ff6600')
+      marker.appendChild(path)
+      defs.appendChild(marker)
+      svgRef.current?.prepend(defs)
+
+      // 创建渐变定义
+      const ldefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
+      const gradient = document.createElementNS(
+        'http://www.w3.org/2000/svg',
+        'linearGradient'
+      )
+      gradient.id = 'jinanGradient'
+      gradient.setAttribute('x1', '0%')
+      gradient.setAttribute('y1', '0%')
+      gradient.setAttribute('x2', '0%')
+      gradient.setAttribute('y2', '100%')
+
+      // 添加渐变颜色节点
+      const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
+      stop1.setAttribute('offset', '0%')
+      stop1.setAttribute('stop-color', '#21542E')
+      const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
+      stop2.setAttribute('offset', '100%')
+      stop2.setAttribute('stop-color', '#bd6f30b6')
+
+      gradient.appendChild(stop1)
+      gradient.appendChild(stop2)
+      ldefs.appendChild(gradient)
+      svgRef.current?.prepend(ldefs)
+}

+ 7 - 0
展示端/src/pages/A2Layout/index.module.scss

@@ -0,0 +1,7 @@
+.A2Layout {
+  position: fixed;
+  :global {
+   
+    
+  }
+}

+ 15 - 0
展示端/src/pages/A2Layout/index.tsx

@@ -0,0 +1,15 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import styles from './index.module.scss'
+
+
+function A2Layout() {
+  return (
+    <div className={styles.A2Layout}>
+
+    </div>
+  )
+}
+
+const MemoA2Layout = React.memo(A2Layout)
+
+export default MemoA2Layout

+ 0 - 9
展示端/src/pages/A2scene/index.module.scss

@@ -1,9 +0,0 @@
-.A2scene {
-  overflow: hidden;
-  :global {
-    iframe {
-      width: 100%;
-      height: 100%;
-    }
-  }
-}

+ 0 - 32
展示端/src/pages/A2scene/index.tsx

@@ -1,32 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import styles from './index.module.scss'
-import { useParams } from 'react-router-dom'
-import { myInfo } from '@/utils/history'
-function A2scene() {
-  const urlObjTemp: any = useParams()
-
-  const [url, setUrl] = useState('')
-
-  useEffect(() => {
-    if (urlObjTemp.id) {
-      const obj = myInfo.swArr.find(v => v.id === Number(urlObjTemp.id))
-      if (obj && obj.link) {
-        setUrl(obj.link)
-        // // 新窗口打开页面
-        // window.open(obj.link, '_blank')
-      }
-    }
-
-    console.log(123, urlObjTemp.id)
-  }, [urlObjTemp])
-
-  return (
-    <div className={styles.A2scene}>
-      {url ? <iframe src={url} frameBorder='0' title='场景'></iframe> : null}
-    </div>
-  )
-}
-
-const MemoA2scene = React.memo(A2scene)
-
-export default MemoA2scene

+ 70 - 0
展示端/src/pages/components/BottomSearch/index.module.scss

@@ -0,0 +1,70 @@
+.BottomSearch {
+  position: absolute;
+  bottom: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 35%;
+  padding: 5px 10px;
+  background: url('../../../assets/img/bottomSearchBg.png') no-repeat 100% 100%;
+  border-radius: 10px;
+  margin: 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+transition: all 0.5s ease-in-out;
+  :global {
+    .ant-input-affix-wrapper ,.ant-input-affix-wrapper-focused {
+      width: 80%;
+      margin-right: 20px;
+      background: transparent ! important;
+      border: none !important;
+      box-shadow:   none !important;
+    }
+    .ant-input {
+      background: transparent !important;
+    }
+    .searchBtn{
+      cursor: pointer;
+    }
+
+    .searchResult {
+      width: 100%;
+      height: 400px;
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 70px;
+      background: url('../../../assets/img/searchResultBg.png') ;
+      background-size: 100% 100%;
+      border-radius: 15px;
+      padding: 0 20px;
+      .ant-tabs-extra-content{
+        &>img{
+          cursor: pointer;
+        }
+      }
+      .ant-tabs-content-holder {
+        height: 330px;
+        overflow: auto;
+        &::-webkit-scrollbar {
+          width: 0;
+        }
+        .ant-tabs-tabpane{
+        &>div{
+          cursor: pointer;
+        }
+      }
+      }
+    }
+  }
+}
+
+.mood {
+      position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.5);
+}

+ 100 - 0
展示端/src/pages/components/BottomSearch/index.tsx

@@ -0,0 +1,100 @@
+import React, { useState, useEffect } from 'react'
+import styles from './index.module.scss'
+import { Input, Tabs } from 'antd'
+import type { TabsProps } from 'antd'
+import classNames from 'classnames'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { MartyrItem } from '@/types/api/martyr'
+import { nameToCityId } from '../../A1home/util'
+
+type BottomSearchProps = {
+  classN: string
+  handleItemClick: (cityId: string, name: string, martyrId: number) => void
+}
+function BottomSearch({ classN, handleItemClick }: BottomSearchProps) {
+  const [isShowResult, setIsShowResult] = useState(false)
+  const [searchName, setSearchName] = useState('')
+  const [searchList, setSearchList] = useState<MartyrItem[]>([])
+
+  const { list: listAll } = useSelector((state: RootState) => state.Martyr.tableInfo)
+
+
+  useEffect(() => {
+    const martyrSearch = (name: string) => {
+      return listAll.filter(item => item.name.includes(name))
+    }
+
+    setSearchList(martyrSearch(searchName))
+  }, [listAll, searchName])
+
+  const handleContentClick = (item: MartyrItem, e: React.MouseEvent) => {
+    console.log(nameToCityId[item.nativeCity])
+    handleItemClick(nameToCityId[item.nativeCity], item.name, item.id)
+    setIsShowResult(false)
+    e.stopPropagation()
+  }
+
+
+  const items: TabsProps['items'] = [
+    {
+      key: '1',
+      label: '全部',
+      children: searchList.map(item => <div onClick={(e) => handleContentClick(item, e)} key={item.id} > {item.name}</div >)
+    },
+    {
+      key: '2',
+      label: '山东',
+      children: searchList.filter(item => item.nativeProvince === '山东省').map(item => <div onClick={(e) => handleContentClick(item, e)} key={item.id}>{item.name}</div>)
+    },
+    {
+      key: '3',
+      label: '陕西',
+      children: searchList.filter(item => item.nativeProvince === '陕西省').map(item => <div onClick={(e) => handleContentClick(item, e)} key={item.id}>{item.name}</div>)
+    }
+  ]
+
+  const operations = (
+    <img
+      onClick={e => {
+        e.stopPropagation()
+        setIsShowResult(false)
+      }}
+      src={require('@/assets/img/close.png')}
+      alt=''
+    />
+  )
+  return (
+    <>
+      {isShowResult && <div className={styles.mood} />}
+      <div
+        className={classNames(styles.BottomSearch, classN)}
+        onClick={() => setIsShowResult(true)}
+      >
+        <Input
+          prefix={<img src={require('@/assets/img/prefixIcon.png')} alt='' />}
+          size='large'
+          placeholder='请输入烈士姓名'
+          value={searchName}
+          onChange={e => setSearchName(e.target.value)}
+        />
+        <div className='searchBtn'>
+          <img src={require('@/assets/img/bottomSearchBgBtn.png')} alt='' />
+        </div>
+        {isShowResult && (
+          <div className='searchResult'>
+            <Tabs
+              defaultActiveKey='1'
+              tabBarExtraContent={operations}
+              items={items}
+            />
+          </div>
+        )}
+      </div>
+    </>
+  )
+}
+
+const MemoBottomSearch = React.memo(BottomSearch)
+
+export default MemoBottomSearch

+ 11 - 0
展示端/src/pages/components/Detail/RelationEcharts/index.module.scss

@@ -0,0 +1,11 @@
+.A1echarts {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  :global {
+    #A1echarts {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}

+ 140 - 0
展示端/src/pages/components/Detail/RelationEcharts/index.tsx

@@ -0,0 +1,140 @@
+import React, { useCallback, useEffect } from 'react'
+import styles from './index.module.scss'
+
+import * as echarts from 'echarts/core'
+import { TitleComponent, TooltipComponent } from 'echarts/components'
+import { GraphChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { RelationShipItem } from '@/types/api/martyr'
+
+echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer])
+
+
+type Props = {
+  martyrName?: string
+  relationList?: RelationShipItem[]
+}
+
+function A1echarts({ martyrName, relationList }: Props) {
+
+  const initFu = useCallback(async () => {
+    const list1: any[] = []
+    const list2: any[] = []
+    console.log(relationList, '================')
+    relationList?.forEach((v, i) => {
+      // 生成唯一节点ID
+      const nodeId = `node_${i}_${v.moduleName}`
+      list1.push({
+        name: v.moduleName,
+        id: nodeId,
+        symbolSize: [60, 60]
+      })
+
+      // 使用ID建立连接关系
+      list2.push({
+        source: 'main_node',
+        target: nodeId,
+        value: v.relationModule
+      })
+      list2.push({
+        source: nodeId,
+        target: 'main_node',
+        value: v.relationMartyr
+      })
+    })
+
+    // 主节点数据定义
+    const mainNode = {
+      name: martyrName || '',
+      id: 'main_node',
+      symbolSize: [80, 80],
+      label: {
+        color: '#fff', // 标签颜色
+        fontSize: 16, // 标签字体大小
+        fontWeight: 'bold' // 标签字体粗细
+      },
+      itemStyle: {
+        color: '#d9622c' // 节点颜色
+      }
+    }
+
+    setTimeout(() => {
+      const dom: HTMLDivElement = document.querySelector('#A1echarts')!
+      const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
+
+      const option = {
+        series: [
+          {
+            type: 'graph', // 图表类型为关系图
+            layout: 'force', // 使用力导向布局
+            emphasis: { focus: 'adjacency' }, // 高亮时显示相邻节点和边
+            force: {
+              repulsion: 1000, // 节点之间的斥力大小
+              edgeLength: 150 // 边的理想长度
+            },
+            symbolSize: 70, // 默认节点大小
+            roam: true, // 允许缩放和平移
+            // draggable: true, // 允许节点拖动(已注释)
+            edgeSymbol: ['arrow'], // 边的箭头样式
+            label: {
+              show: true, // 显示节点标签
+              position: 'inside', // 标签显示在节点内部
+              color: 'black' // 标签默认颜色
+            },
+            edgeLabel: {
+              show: true, // 显示边标签
+              fontSize: 12, // 边标签字体大小
+              color: 'red', // 边标签颜色
+              formatter: '{c}', // 显示边的value值
+              rotate: 0, // 强制边标签文字水平显示(新增)
+              position: 'middle',
+            },
+            itemStyle: {
+              borderColor: '#d9622c', // 节点边框颜色
+              borderWidth: 2, // 节点边框宽度
+              shadowBlur: 10, // 阴影模糊大小
+              shadowColor: '#d9622c', // 阴影颜色
+              color: '#fff' // 节点默认颜色
+            },
+            lineStyle: {
+              opacity: 0.9, // 边透明度
+              width: 2, // 边宽度
+              curveness: 0.3, // 边弯曲度
+              color: {
+                // 边颜色使用线性渐变
+                type: 'linear',
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#e99a75ff'
+                  },
+                  {
+                    offset: 1,
+                    color: '#d9622c'
+                  }
+                ]
+              }
+            },
+            data: [mainNode, ...list1],  // 使用新的主节点定义
+            links: list2
+          }
+        ]
+      }
+      option && myChart.setOption(option)
+    }, 200)
+  }, [martyrName, relationList])
+
+  useEffect(() => {
+    initFu()
+  }, [initFu])
+
+  return (
+    <div className={styles.A1echarts}>
+      <div id='A1echarts'></div>
+    </div>
+  )
+}
+
+const MemoA1echarts = React.memo(A1echarts)
+
+export default MemoA1echarts

+ 367 - 0
展示端/src/pages/components/Detail/index.module.scss

@@ -0,0 +1,367 @@
+.detail {
+  position: absolute;
+  top: 0;
+  right: 0;
+  transform: translate(100%, 0);
+  width: 36%;
+  height: 100%;
+  padding: 20px;
+  display: flex;
+  transition: all 0.5s ease-in-out;
+  :global {
+    .infoDetailBox {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      .infoDetail {
+        width: 55%;
+        height: 100%;
+        background: url('../../../assets/img/infoDetailBox.png');
+        background-size: 100% 100%;
+
+        .scrollContainner {
+          width: 100%;
+          height: 96%;
+          display: flex;
+          flex-direction: column;
+          gap: 20px;
+          padding: 0 20px;
+          margin: 20px 0;
+          overflow-y: auto;
+          &::-webkit-scrollbar {
+            width: 0;
+          }
+          .mainInfo {
+            display: flex;
+            align-items: center;
+            width: 100%;
+            height: 185px;
+            gap: 16px;
+            .right {
+              width: 67%;
+              height: 100%;
+              display: flex;
+              flex-direction: column;
+              gap: 10px;
+              .name {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                .Name {
+                  font-weight: 500;
+                  font-size: 16px;
+                }
+                .other {
+                  align-self: end;
+                }
+              }
+              .home,
+              .sacrifice {
+                height: fit-content;
+                max-height: 35px;
+                display: -webkit-box;
+                -webkit-box-orient: vertical;
+                -webkit-line-clamp: 2;
+                line-clamp: 2;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                line-height: 1.4em;
+              }
+            }
+            .left {
+              width: 33%;
+              height: 100%;
+              & > img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                border-radius: 10px;
+              }
+            }
+          }
+          .vr {
+            width: 100%;
+            height: 90px;
+            position: relative;
+            cursor: pointer;
+            & > img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+              border-radius: 10px;
+            }
+            .botvr {
+              width: 44px;
+              height: 22px;
+              object-fit: contain;
+              position: absolute;
+              right: 5px;
+              bottom: 6px;
+            }
+          }
+          .content {
+            width: 100%;
+            height: fit-content;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            border-bottom: 2px dashed #9c543b;
+            padding-bottom: 20px;
+            .text {
+              word-break: break-word;
+            }
+            .media {
+              width: 100%;
+              height: fit-content;
+              display: flex;
+              align-items: center;
+              flex-wrap: wrap;
+              gap: 3px;
+              video {
+                width: 32%;
+                height: 70px;
+                object-fit: cover;
+                cursor: pointer;
+              }
+            }
+          }
+          .relation {
+            width: 100%;
+            height: fit-content;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            .title {
+              width: 38%;
+              height: 35px;
+              & > img {
+                width: 100%;
+                height: 100%;
+                object-fit: contain;
+              }
+            }
+            .relationPic {
+              height: 300px;
+              width: 100%;
+              background: url('../../../assets/img/relationBg.png');
+              background-size: 100% 100%;
+            }
+          }
+          .infoLabel {
+            width: 100%;
+            height: fit-content;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            div.title {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              width: 100%;
+              height: 35px;
+
+              padding: 0 5px;
+              .name {
+                width: 150px;
+                font-size: 20px;
+                font-weight: 600;
+              }
+              .label {
+                width: 0;
+                flex: 1;
+                display: flex;
+                align-items: center;
+                gap: 5px;
+                & > div {
+                  width: 85px;
+                  height: 25px;
+                  text-align: center;
+                  line-height: 25px;
+                  border-radius: 10px;
+                  background: linear-gradient(to bottom, #fcaf80, #b15013);
+                  color: #f5eeed;
+                  font-size: 11px;
+                  font-weight: 600;
+                }
+              }
+            }
+
+            .media {
+              width: 100%;
+              height: fit-content;
+              display: flex;
+              align-items: center;
+              flex-wrap: wrap;
+              gap: 3px;
+              video {
+                width: 32%;
+                height: 70px;
+                object-fit: cover;
+                cursor: pointer;
+              }
+            }
+          }
+        }
+      }
+      .infoDetailAdd {
+        width: 45%;
+        height: 65%;
+        display: flex;
+        justify-content: center;
+        background: url('../../../assets/img/infoDetailAdd.png');
+        background-size: 100% 100%;
+        .scrollContainner {
+          width: 100%;
+          height: 93%;
+          display: flex;
+          flex-direction: column;
+          gap: 10px;
+          padding: 0 20px;
+          margin: 10px 0;
+          overflow-y: auto;
+          &::-webkit-scrollbar {
+            width: 0;
+          }
+          .title1 {
+            width: 67%;
+            height: 60px;
+            & > img {
+              width: 100%;
+              height: 100%;
+              object-fit: contain;
+            }
+          }
+          .modelList {
+            width: 100%;
+            height: fit-content;
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+            .model {
+              width: 48%;
+              height: 90px;
+              display: flex;
+              flex-direction: column;
+              align-items: center;
+              gap: 2px;
+              & > img {
+                width: 100%;
+                height: 70px;
+                object-fit: contain;
+              }
+              .txt {
+                width: 100%;
+                height: 18px;
+                font-size: 13px;
+                color: #3d2c14;
+                display: flex;
+                align-items: center;
+                .dot {
+                  width: 11px;
+                  height: 11px;
+                  background: #cb2a0e;
+                  border-radius: 50%;
+                  margin-right: 5px;
+                }
+              }
+            }
+          }
+          .title2 {
+            width: 67%;
+            height: 60px;
+            & > img {
+              width: 100%;
+              height: 100%;
+              object-fit: contain;
+            }
+          }
+          .introDetailBox {
+            width: 100%;
+            height: 100%;
+            .ant-timeline-item-head {
+              background: #cb2a0e;
+              border: none;
+              color: #cb2a0e;
+            }
+            .introDetail {
+              display: flex;
+              flex-direction: column;
+              gap: 6px;
+              color: #cb2a0e;
+              .year {
+                font-size: 18px;
+                font-weight: 500;
+              }
+              & > div > img {
+                width: 60%;
+                object-fit: contain;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .noInfoDetail {
+      justify-content: end;
+      .infoDetailAdd {
+        display: none;
+      }
+    }
+  }
+}
+
+.modelInfo {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background-color: rgba(0, 0, 0, 0.7);
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  :global {
+    .content {
+      width: 60%;
+      height: 60%;
+      background: url('../../../assets/img/modelInfoBg.png');
+      background-size: 100% 100%;
+      padding: 20px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      & > img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+    }
+    .txtBox {
+      padding: 20px;
+      width: 65%;
+      height: 20%;
+      color: #fff;
+      text-align: center;
+      .title {
+        font-size: 20px;
+        font-weight: 600;
+      }
+      .txt {
+        font-size: 18px;
+      }
+    }
+    .close {
+      cursor: pointer;
+      position: absolute;
+      top: 5%;
+      right: 17%;
+      width: 40px;
+      height: 40px;
+      background: url('../../../assets/img/close.png');
+      background-size: 100% 100%;
+    }
+  }
+}

+ 220 - 0
展示端/src/pages/components/Detail/index.tsx

@@ -0,0 +1,220 @@
+import React, { useState, useCallback, useMemo } from 'react'
+import styles from './index.module.scss'
+import classNames from 'classnames'
+import RelationEcharts from './RelationEcharts/index'
+import { Timeline } from 'antd'
+import store from '@/store'
+import { MartyrItem, RelationShipItem } from '@/types/api/martyr'
+import { ClueItem } from '@/types/api/clue'
+import ImageLazy from '@/components/ImageLazy/index'
+import { baseURL } from '@/utils/http'
+
+type DetailProps = {
+  classN?: string
+  relationList?: RelationShipItem[]
+  martyrDetail?: MartyrItem
+}
+function Detail({ classN, relationList, martyrDetail }: DetailProps) {
+  const [currentRelic, setCurrentRelic] = useState<any>('')
+
+  // 预览视频
+  const lookFileFu = useCallback((file: string) => {
+    store.dispatch({
+      type: 'layout/lookDom',
+      payload: { src: file, type: 'video' }
+    })
+  }, [])
+
+  const clueList = useMemo(() => {
+    return martyrDetail?.clue
+      .map((item: ClueItem) => {
+        return {
+          ...item,
+          status: {
+            0: '待确定',
+            1: '跟进中',
+            2: '已找到',
+            3: '未找到',
+            4: '作废',
+          }[item.status] as string,
+        }
+      })
+      .filter(
+        (item: ClueItem) => item.status !== '待确定' && item.status !== '作废'
+      )
+  }, [martyrDetail])
+
+  return (
+    <>
+      <div className={classNames(styles.detail, classN)}>
+        <div className={classNames('infoDetailBox', (!martyrDetail?.life.length && !martyrDetail?.relic.length) && 'noInfoDetail')}>
+          <div className='infoDetail'>
+            <div className='scrollContainner'>
+              <div className='mainInfo'>
+                <div className='left'>
+                  <img src={martyrDetail?.thumb} alt='' />
+                </div>
+                <div className='right'>
+                  <div className='name'>
+                    <div className='Name'>{martyrDetail?.name}</div>
+                    <div className='other'>{martyrDetail?.dictPanName}</div>
+                  </div>
+                  <div className='gender'>{martyrDetail?.gender === 1 ? '男' : '女' || '未知'}|{martyrDetail?.nation}</div>
+                  <div className='birth'>{martyrDetail?.dateStart}-{martyrDetail?.dateEnd}</div>
+                  <div className='home'>籍贯:{martyrDetail?.nativeProvince}-{martyrDetail?.nativeCity}-{martyrDetail?.nativeRegion},{martyrDetail?.nativeAddress}</div>
+                  <div className='sacrifice'>{`牺牲地:${martyrDetail?.lossProvince}-${martyrDetail?.lossCity}-${martyrDetail?.lossRegion},${martyrDetail?.lossAddress}`}</div>
+                </div>
+              </div>
+              <div className='vr' onClick={() => window.open(`${martyrDetail?.link}`)}>
+                <img src={require('@/assets/img/sceneThumb.png')} alt='' />
+                <img src={require('@/assets/img/vr.png')} alt="" className="botvr" />
+              </div>
+              <div className='content'>
+                <div className='text'>{martyrDetail?.intro}</div>
+                {(martyrDetail?.img || martyrDetail?.video) && (
+                  <div className='media'>
+                    <>
+                      {martyrDetail?.img?.map((item: any, index: number) => {
+                        return (
+                          <ImageLazy
+                            width={'32%'}
+                            height={'70px'}
+                            src={item.thumb}
+                            srcBig={item.filePath}
+                            key={index}
+                          />
+                        )
+                      })}
+                      {martyrDetail?.video?.map((item: any, index: number) => {
+                        return (
+                          <video
+                            onClick={e => {
+                              e.stopPropagation()
+                              lookFileFu(item.filePath)
+                            }}
+                            src={baseURL + item.thumb}
+                            key={index}
+                          />
+                        )
+                      })}
+                    </>
+                  </div>
+                )}
+              </div>
+              <div className='relation'>
+                <div className='title'>
+                  <img src={require('@/assets/img/relationTitle.png')} alt='' />
+                </div>
+                <div className='relationPic'>
+                  <RelationEcharts martyrName={martyrDetail?.name} relationList={relationList} />
+                </div>
+              </div>
+              {clueList?.map((item: any) => (
+                <div className='infoLabel' key={item.id}>
+                  <div className='title'>
+                    <div className='name'>烈士:{martyrDetail?.name}</div>
+                    <div className='label'>
+                      <div>{item.type}</div>
+                      <div>{item.status}</div>
+                    </div>
+                  </div>
+                  <div className='address'> {item.province + item.city + item.region + item.address}</div>
+                  <div className='labelContent'>{item.remark}</div>
+                  <div className='labelContent'>{item.result}</div>
+                  {(item.img || item.video) && (
+                    <div className='media'>
+                      <>
+                        {item.img?.map((item: any, index: number) => {
+                          return (
+                            <ImageLazy
+                              width={'32%'}
+                              height={'70px'}
+                              src={item.thumb}
+                              srcBig={item.filePath}
+                              key={index}
+                            />
+                          )
+                        })}
+                        {item.video?.map((item: any, index: number) => {
+                          return (
+                            <video
+                              onClick={e => {
+                                e.stopPropagation()
+                                lookFileFu(item.filePath)
+                              }}
+                              src={baseURL + item.thumb}
+                              key={index}
+                            />
+                          )
+                        })}
+                      </>
+                    </div>
+                  )}
+                  <div className='updateTime'>{item.updateTime}</div>
+                </div>))}
+
+            </div>
+          </div>
+
+          <div className='infoDetailAdd'>
+            <div className='scrollContainner'>
+              <div className='title1'>
+                <img src={require('@/assets/img/modelPic.png')} alt='' />
+              </div>
+              <div className='modelList' >
+                {martyrDetail?.relic.map((item: any) => (
+                  <div className='model' key={item.id}>
+                    <img src={item.thumb} alt='' onClick={() => setCurrentRelic(item)} />
+                    <div className='txt'>
+                      <div className='dot' />
+                      {item.name}
+                    </div>
+                  </div>
+                ))}
+
+              </div>
+              <div className='title2'>
+                <img src={require('@/assets/img/introPic.png')} alt='' />
+              </div>
+              <div className='introDetailBox'>
+                <Timeline
+                  items={martyrDetail?.life?.map((item: any) => {
+                    const content = JSON.parse(item.rtf);
+                    console.log(content, '123321')
+                    return {
+                      children: (
+                        <div className='introDetail' key={item.id}>
+                          <div className='year'>{item.date}</div>
+                          <div dangerouslySetInnerHTML={{ __html: content.txtArr[0].txt }} />
+                          <div>
+                            <img src={baseURL + content.txtArr[0].fileInfo.filePath} alt='' />
+                          </div>
+                        </div>
+                      )
+                    };
+                  })}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      {currentRelic && (
+        <div className={styles.modelInfo}>
+          <div className='content'>
+            <img src={currentRelic.thumbPc} alt='' />
+          </div>
+          <div className='txtBox'>
+            <div className='title'>{currentRelic.name}</div>
+            <div className='txt'>{currentRelic.intro}</div>
+          </div>
+          <div className='close' onClick={() => setCurrentRelic('')} />
+        </div>
+      )}
+    </>
+  )
+}
+
+const MemoDetail = React.memo(Detail)
+
+export default MemoDetail

+ 161 - 0
展示端/src/pages/components/Left/index.module.scss

@@ -0,0 +1,161 @@
+.Left {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 20%;
+  height: 100%;
+  padding: 20px;
+transition: all 0.5s ease-in-out;
+  :global {
+    .infoContainer {
+      display: flex;
+      flex-direction: column;
+      width: 100%;
+      height: 100%;
+      background: url('../../../assets/img/infoContainerBg.png') no-repeat 100% 100%;
+      border-radius: 20px;
+      padding: 20px;
+      .topInfo {
+        display: flex;
+        width: 100%;
+        height: 20%;
+        gap: 16px;
+        .finding,
+        .found {
+          width: 50%;
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+          border-radius: 10px;
+          background: url('../../../assets/img/foundBg.png') no-repeat center;
+          .title {
+            width: 100%;
+            height: 65%;
+            color: #fff;
+            font-weight: 600;
+            font-size: 18px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            & > img {
+              height: 70px;
+              object-fit: none;
+            }
+          }
+          .num {
+            height: 0;
+            width: 100%;
+            color: #fff;
+            font-weight: 600;
+            font-size: 24px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .mainInfoBox {
+        padding: 20px;
+        border-radius: 10px;
+        background: url('../../../assets/img/mainInfoBoxBg.png') no-repeat 100% 100%;
+        height: 80%;
+        width: 100%;
+        .tab {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          width: 100%;
+          height: 30px;
+          margin-bottom: 20px;
+          font-size: 20px;
+          font-weight: 500;
+          & > div {
+            padding-bottom: 5px;
+          }
+          .active {
+            font-weight: 600;
+            padding-bottom: 1px;
+            border-bottom: 4px solid;
+          }
+        }
+        .mainInfoListBox {
+          position: relative;
+          display: flex;
+          width: 100%;
+          height: calc(100% - 50px);
+          .slide {
+            position: absolute;
+            left: -20px;
+            width: 20px;
+            height: 100%;
+            background: url(../../../assets/img/slide.png) no-repeat 100% 100%;
+          }
+          .mainInfoList {
+            display: flex;
+            flex-direction: column;
+            width: 100%;
+            height: 100%;
+            overflow-y: auto;
+            overflow-x: hidden;
+            gap: 10px;
+            &::-webkit-scrollbar {
+              display: none; /* 隐藏滚动条 */
+            }
+            .mainInfo {
+              width: 100%;
+              height: fit-content;
+              padding: 16px;
+              border-radius: 5px;
+              background: url('../../../assets/img/mainInfoBg.png') repeat center;
+              display: flex;
+              flex-direction: column;
+              gap: 10px;
+              .labelList {
+                display: flex;
+                gap: 10px;
+                height: 35px;
+                .label {
+                  width: 95px;
+                  height: 100%;
+                  font-size: 13px;
+                  display: flex;
+                  align-items: center;
+                  justify-content: center;
+                  background: url('../../../assets/img/label.png')  ;
+                      background-size: cover;
+                }
+              }
+              .name {
+                font-weight: 600;
+                font-size: 20px;
+                width: 100%;
+                height: 30px;
+              }
+              .content {
+                word-break: break-word;
+              }
+              .media {
+                width: 100%;
+                height: fit-content;
+                display: flex;
+                align-items: center;
+                flex-wrap: wrap;
+                gap: 3px;
+                video {
+                  width: 32%;
+                  height: 70px;
+                  object-fit: cover;
+                  cursor: pointer;
+                }
+              }
+            }
+            .ant-pagination{
+              margin:  0 auto;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 164 - 0
展示端/src/pages/components/Left/index.tsx

@@ -0,0 +1,164 @@
+import React, { useEffect, useCallback, useMemo, useState } from 'react'
+import styles from './index.module.scss'
+import { useDispatch, useSelector } from 'react-redux'
+import { Clue_APIgetClueList } from '@/store/action/clue'
+import { RootState } from '@/store'
+import store from '@/store'
+import { ClueItem } from '@/types/api/clue'
+import { baseURL } from '@/utils/http'
+import classNames from 'classnames'
+import { Pagination } from 'antd'
+import ImageLazy from '@/components/ImageLazy'
+
+type LeftProps = {
+  classN?: string
+}
+function Left(props: LeftProps) {
+  const dispatch = useDispatch()
+  const getListFu = useCallback(
+    (pageNum: number, pageSize: number) => {
+      dispatch(
+        Clue_APIgetClueList({
+          pageNum: pageNum,
+          pageSize: pageSize
+        })
+      )
+    },
+    [dispatch]
+  )
+
+  const { list: listAll, total } = useSelector((state: RootState) => state.Clue.tableInfo)
+
+  useEffect(() => {
+    getListFu(1, 20)
+  }, [getListFu])
+
+  const fingingList = useMemo(() => {
+    return listAll.filter((item: ClueItem) => item.status === '跟进中')
+  }, [listAll])
+  const foundList = useMemo(() => {
+    return listAll.filter((item: ClueItem) => item.status === '已找到')
+  }, [listAll])
+  const clueList = useMemo(() => {
+    return listAll.filter(
+      (item: ClueItem) => item.status !== '待确定' && item.status !== '作废'
+    )
+  }, [listAll])
+  const [curClueList, setCurClueList] = useState<ClueItem[]>()
+
+  useEffect(() => {
+    if (foundList.length && !curClueList) {
+      setCurClueList(foundList)
+    }
+  }, [curClueList, foundList])
+
+  // 预览视频
+  const lookFileFu = useCallback((file: string) => {
+    store.dispatch({
+      type: 'layout/lookDom',
+      payload: { src: file, type: 'video' }
+    })
+  }, [])
+
+  return (
+    <div className={classNames(styles.Left, props.classN)}>
+      <div className='infoContainer'>
+        <div className='topInfo'>
+          <div className='finding'>
+            <div className='title'>
+              <img src={require('@/assets/img/finding.png')} alt='' />
+              <div>寻亲中</div>
+            </div>
+            <div className='num'>{fingingList.length}</div>
+          </div>
+          <div className='found'>
+            <div className='title'>
+              <img src={require('@/assets/img/found.png')} alt='' />
+              <div>已找到</div>
+            </div>
+            <div className='num'>{foundList.length}</div>
+          </div>
+        </div>
+        <div className='mainInfoBox'>
+          <div className='tab'>
+            <div
+              className={curClueList === foundList ? 'active' : ''}
+              onClick={() => setCurClueList(foundList)}
+            >
+              寻亲案例
+            </div>
+            <div
+              className={curClueList === clueList ? 'active' : ''}
+              onClick={() => setCurClueList(clueList)}
+            >
+              线索更新
+            </div>
+          </div>
+          <div className='mainInfoListBox'>
+            <div className='slide'></div>
+            <div className='mainInfoList'>
+              {curClueList?.map((item: ClueItem) => {
+                return (
+                  <div className='mainInfo' key={item.id}>
+                    <div className='labelList'>
+                      <div className='label'>{item.type}</div>
+                      <div className='label'>{item.status}</div>
+                    </div>
+                    <div className='name'>烈士:{item.martyrName}</div>
+                    <div className='address'>
+                      {item.province + item.city + item.region + item.address}
+                    </div>
+                    {item.remark && <div className='content'>{item.remark}</div>}
+                    {item.result && <div className='content'>{item.result}</div>}
+                    {(item.img || item.video) && (
+                      <div className='media'>
+                        <>
+                          {item.img?.map((item: any, index: number) => {
+                            return (
+                              <ImageLazy
+                                width={'32%'}
+                                height={'70px'}
+                                src={item.thumb}
+                                srcBig={item.filePath}
+                                key={index}
+                              />
+                            )
+                          })}
+                          {item.video?.map((item: any, index: number) => {
+                            return (
+                              <video
+                                onClick={e => {
+                                  e.stopPropagation()
+                                  lookFileFu(item.filePath)
+                                }}
+                                src={baseURL + item.thumb}
+                                key={index}
+                              />
+                            )
+                          })}
+                        </>
+                      </div>
+                    )}
+                    <div className='updateTime'>更新时间:{item.updateTime}</div>
+                  </div>
+                )
+              })}
+              <Pagination
+                total={total}
+                simple
+                defaultPageSize={20}
+                defaultCurrent={1}
+                current={1}
+                onChange={(page, size) => getListFu(page, size)}
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const MemoLeft = React.memo(Left)
+
+export default MemoLeft

+ 46 - 0
展示端/src/pages/components/MapTab/index.module.scss

@@ -0,0 +1,46 @@
+.MapTab {
+  width: 250px;
+  height: 250px;
+  position: absolute;
+  left: 50%;
+     transform: translate(-145%, 29%);
+  top: 50%;
+  background: url('../../../assets/img/searchResultBg.png');
+  background-size: 100% 100%;
+  border-radius: 15px;
+  padding: 0 20px;
+
+  :global {
+    .ant-tabs-extra-content {
+      & > img {
+        width: 30px;
+        height: 30px;
+        object-fit: contain;
+        cursor: pointer;
+      }
+    }
+    .ant-tabs-tab,
+    .ant-tabs-tab-active {
+      color: #e65252 !important;
+      font-weight: 600;
+      &>div{
+        color: #e65252 !important;
+      }
+    }
+    .ant-tabs-ink-bar {
+      background-color: #e65252 !important;
+    }
+    .ant-tabs-content-holder {
+      height: 180px;
+      overflow: auto;
+      &::-webkit-scrollbar {
+        width: 0;
+      }
+      .ant-tabs-tabpane{
+        &>div{
+          cursor: pointer;
+        }
+      }
+    }
+  }
+}

+ 62 - 0
展示端/src/pages/components/MapTab/index.tsx

@@ -0,0 +1,62 @@
+import React, { useMemo, useCallback } from 'react'
+import styles from './index.module.scss'
+import { Tabs } from 'antd'
+import type { TabsProps } from 'antd'
+import { ClueItem, RelativeItem, MartyrItem } from '@/types'
+import { cityIdToName } from '@/pages/A1home/util'
+
+type MapTabProps = {
+  cityId: string
+  setCityId: (cityId: string) => void
+  clueList: Record<string, ClueItem[]>
+  relativeList: Record<string, RelativeItem[]>
+  martyrListByCity: Record<string, MartyrItem[]>
+  handleItemClick: (cityId: string, name: string, martyrId: number) => void
+}
+function MapTab(props: MapTabProps) {
+
+
+  const cityName = useMemo(() => cityIdToName[props.cityId], [props.cityId])
+
+  const items: TabsProps['items'] = [
+    {
+      key: '1',
+      label: '烈士',
+      children: props.martyrListByCity[cityName].map((item, index) => <div className='text' onClick={() => props.handleItemClick(props.cityId, item.name, item.id)} key={index}>{item.name}</div>)
+    },
+    {
+      key: '2',
+      label: '亲属',
+      children: props.relativeList[cityName]?.map((item, index) => <div className='text' onClick={() => props.handleItemClick(props.cityId, item.martyrName, item.martyrId)} key={index}>{item.name}</div>) || <div>暂无</div>
+    },
+    {
+      key: '3',
+      label: '线索',
+      children: props.clueList[cityName]?.map((item, index) => <div className='text' onClick={() => props.handleItemClick(props.cityId, item.martyrName, item.martyrId)} key={index}>{item.remark}</div>) || <div>暂无</div>
+    }
+  ]
+
+  const operations = (
+    <img
+      onClick={e => {
+        e.stopPropagation()
+        props.setCityId('')
+      }}
+      src={require('@/assets/img/close.png')}
+      alt=''
+    />
+  )
+  return (
+    <div className={styles.MapTab}>
+      <Tabs
+        defaultActiveKey='1'
+        tabBarExtraContent={operations}
+        items={items}
+      />
+    </div>
+  )
+}
+
+const MemoMapTab = React.memo(MapTab)
+
+export default MemoMapTab

+ 115 - 0
展示端/src/pages/components/Right/index.module.scss

@@ -0,0 +1,115 @@
+.Right {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 23%;
+  height: 100%;
+  padding: 20px;
+  left: auto;
+  right: 0;
+  transition: all 0.5s ease-in-out;
+  :global {
+    .infoContainner {
+      width: 100%;
+      height: 100%;
+      background: url('../../../assets/img/mainInfoList.png');
+      padding: 20px;
+      border-radius: 20px;
+      .mainInfoList {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        border-radius: 10px;
+
+        gap: 10px;
+        overflow: auto;
+        &::-webkit-scrollbar {
+          width: 0;
+        }
+
+        .infoItem {
+          width: 100%;
+          height: fit-content;
+          border-radius: 5px;
+          background: url('../../../assets/img/infoItem.png');
+          background-size: 100% 100%;
+          padding: 16px 16px 26px 40px;
+          display: flex;
+          flex-direction: column;
+          gap: 10px;
+          font-size: 14px;
+          color: #000;
+          cursor: pointer;
+          .mainInfo {
+            display: flex;
+            align-items: center;
+            width: 100%;
+            height: 135px;
+            .left {
+              width: 67%;
+              height: 100%;
+              display: flex;
+              flex-direction: column;
+              gap: 10px;
+              .name {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                .Name {
+                  font-weight: 600;
+                  font-size: 20px;
+                }
+                .other {
+                  align-self: end;
+                }
+              }
+              .home {
+                height: fit-content;
+                max-height: 35px;
+                display: -webkit-box;
+                -webkit-box-orient: vertical;
+                -webkit-line-clamp: 2;
+                line-clamp: 2;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                line-height: 1.4em;
+              }
+            }
+            .right {
+              width: 33%;
+              height: 100%;
+              img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                border-radius: 10px;
+              }
+            }
+          }
+          .tag {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+            .label {
+              width: 85px;
+              height: 25px;
+              text-align: center;
+              line-height: 25px;
+              border-radius: 10px;
+              background: linear-gradient(to bottom, #fcaf80, #b15013);
+              color: #f5eeed;
+              font-size: 11px;
+              font-weight: 600;
+            }
+            & > div {
+              overflow: hidden;
+              white-space: nowrap;
+              text-overflow: ellipsis;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 75 - 0
展示端/src/pages/components/Right/index.tsx

@@ -0,0 +1,75 @@
+import React, { useEffect, useCallback, useState } from 'react'
+import styles from './index.module.scss'
+import classNames from 'classnames'
+
+import ImageLazy from '@/components/ImageLazy'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+
+import { MartyrItem } from '@/types/api/martyr'
+
+type RightProps = {
+  classN: string
+  handleItemClick: (cityId: string, name: string, martyrId: number) => void
+}
+function Right(props: RightProps) {
+
+  const { list: listAll } = useSelector((state: RootState) => state.Martyr.tableInfo)
+  console.log(listAll)
+
+
+  // const handleItemClick = useCallback(
+  //   (cityId: string, name: string) => {
+  //     props.setIsAddClassName(true)
+  //     props.focusOnCity(cityId)
+  //     props.changeTooltipText(cityId, name)
+  //     props.setEnableTooltipEvents(false)
+  //     props.drawArrows('tap-jinan', ['tap-weifang', 'tap-linyi', 'tap-binzhou', 'tap-dezhou', 'tap-heze', 'tap-jining'])
+  //   },
+  //   [props]
+  // )
+  return (
+    <div className={classNames(styles.Right, props.classN)}>
+      <div className='infoContainner'>
+        <div
+          className='mainInfoList'
+
+        >
+
+          {listAll.map((item: MartyrItem, index: number) => {
+            return (
+              <div className='infoItem' onClick={() => props.handleItemClick(item.cityId, item.name, item.id)} key={index}>
+                <div className='mainInfo'>
+                  <div className='left'>
+                    <div className='name'>
+                      <div className='Name'>{item.name}</div>
+                      <div className='other'>{item.dictPanName}</div>
+                    </div>
+                    <div className='gender'>{item.gender}|{item.nation}</div>
+                    <div className='birth'>{item.dateStart} - {item.dateEnd}</div>
+                    <div className='home'>{`籍贯:${item.nativeProvince}-${item.nativeCity}-${item.nativeRegion}`}</div>
+                  </div>
+
+                  <div className='right' onClick={e => e.stopPropagation()}>
+                    <ImageLazy width={'100%'} height={'100%'} src={item.thumb} srcBig={item.thumbPc} />
+                  </div>
+                </div>
+                <div className='tag'>
+                  <div className='label'>{item.clueType}</div>
+                  <div>{item.clueRemark}</div>
+                </div>
+                {/* <div className='tag'>
+                  <div className='label'>线索类型名称</div>
+                  <div>这是一段线索说明</div>
+                </div> */}
+              </div>)
+          })}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const MemoRight = React.memo(Right)
+
+export default MemoRight

+ 0 - 30
展示端/src/store/action/A1list.ts

@@ -1,30 +0,0 @@
-/**
- * 展馆预约记录-列表
- */
-
-import http from '@/utils/http'
-
-// export const A1_APIgetList = (data: any): any => {
-//   return async (dispatch: AppDispatch) => {
-//     const res = await http.post('cms/applyExhibition/pageList', data)
-//     if (res.code === 0) {
-//       const arr: A1tableType[] = res.data.records
-//       arr.forEach(v => {
-//         v.cityStr = v.province + '-' + v.city
-//       })
-
-//       const obj = {
-//         list: arr,
-//         total: res.data.total
-//       }
-//       dispatch({ type: 'A1/getList', payload: obj })
-//     }
-//   }
-// }
-
-/**
- * 获取全部访问量
- */
-export const A1_APIgetNumList = () => {
-  return http.get('visit/getList')
-}

+ 41 - 0
展示端/src/store/action/clue.ts

@@ -0,0 +1,41 @@
+/**
+ * 线索记录-列表
+ */
+
+import http from '@/utils/http'
+import { AppDispatch } from '@/store'
+import { ClueItem } from '@/types/api/clue';
+
+/**
+ * 获取线索记录列表
+ */
+type Clue_APIgetClueListParams = {
+  martyrId?: number,
+	pageNum: number,
+	pageSize: number,
+	searchKey?: string,
+	status?: number,
+	type?: string
+}
+export const Clue_APIgetClueList = (data: Clue_APIgetClueListParams): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('show/clue/pageList', data)
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.records.map((item: ClueItem) => ({
+              ...item,
+              status: {
+                0: '待确定',
+                1: '跟进中',
+                2: '已找到',
+                3: '未找到',
+                4: '作废',
+              }[item.status] as string
+            })),
+        total: res.data.total
+      }
+
+      dispatch({ type: 'Clue_APIgetClueList', payload: obj })
+    }
+  }
+}

+ 89 - 0
展示端/src/store/action/martyr.ts

@@ -0,0 +1,89 @@
+/**
+ * 烈士-列表
+ */
+
+import http from '@/utils/http'
+import { AppDispatch } from '@/store'
+import { MartyrItem } from '@/types/api/martyr'
+
+/**
+ * 获取烈士列表
+ */
+type Martyr_APIgetListParams = {
+  nativeProvince: string
+}
+export const Martyr_APIgetList = (data: Martyr_APIgetListParams): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get('show/martyr/getList', { params: data })
+    if (res.code === 0) {
+      const cityMap: Record<string, string> = {
+        东营市: 'tap-dongying',
+        济南市: 'tap-jinan',
+        青岛市: 'tap-qingdao',
+        淄博市: 'tap-zhob',
+        潍坊市: 'tap-weifang',
+        烟台市: 'tap-yantai',
+        威海市: 'tap-weihai',
+        日照市: 'tap-rizhisi',
+        临沂市: 'tap-linyi',
+        枣庄市: 'tap-zaozhuang',
+        聊城市: 'tap-chaoyang',
+        滨州市: 'tap-binzhou',
+        菏泽市: 'tap-herz',
+        德州市: 'tap-dezhou',
+        泰安市: 'tap-taian',
+        济宁市: 'tap-jining'
+      }
+      const obj = {
+        list: res.data.map((item: MartyrItem) => ({
+          ...item,
+          gender: item.gender === 1 ? '男' : '女',
+          cityId: cityMap[item.nativeCity] || ''
+        })),
+        total: res.data.total
+      }
+
+      dispatch({ type: 'Martyr_APIgetList', payload: obj })
+    }
+  }
+}
+
+/**
+ * 根据省份获取亲属列表
+ */
+type Martyr_APIgetRelativeListParams = {
+  nativeProvince: string
+}
+export const Martyr_APIgetRelativeList = (data: Martyr_APIgetRelativeListParams): any => {
+  return http.get('show/kinship/findByProvince', { params: data })
+}
+
+/**
+ * 根据省份获取线索列表
+ */
+type Martyr_APIgetClueListParams = {
+  nativeProvince: string
+}
+export const Martyr_APIgetClueList = (data: Martyr_APIgetClueListParams): any => {
+  return http.get('show/clue/findByProvince', { params: data })
+}
+
+/**
+ * 获取烈士详情
+ */
+type Martyr_APIgetDetailParams = {
+  id: number
+}
+export const Martyr_APIgetDetail = (data: Martyr_APIgetDetailParams): any => {
+  return http.get(`show/martyr/detail/${data.id}`)
+}
+
+/**
+ * 获取人物关系
+ */
+type Martyr_APIgetRelationListParams = {
+  martyrId: number
+}
+export const Martyr_APIgetRelationList = (data: Martyr_APIgetRelationListParams): any => {
+  return http.get(`show/relation/${data.martyrId}`)
+}

+ 6 - 4
展示端/src/store/reducer/A1list.ts

@@ -1,23 +1,25 @@
 // 初始化状态
+import { ClueItem } from '@/types/api/clue'
+
 const initState = {
   // 列表数据
   tableInfo: {
-    list: [] as any[],
+    list: [] as ClueItem[],
     total: 0
   }
 }
 
 // 定义 action 类型
 type Props = {
-  type: 'A1/getList'
-  payload: { list: any[]; total: number }
+  type: 'Clue_APIgetClueList'
+  payload: { list: ClueItem[]; total: number }
 }
 
 // reducer
 export default function Reducer(state = initState, action: Props) {
   switch (action.type) {
     // 获取列表数据
-    case 'A1/getList':
+    case 'Clue_APIgetClueList':
       return { ...state, tableInfo: action.payload }
 
     default:

+ 4 - 2
展示端/src/store/reducer/index.ts

@@ -3,12 +3,14 @@ import { combineReducers } from 'redux'
 
 // 导入 登录 模块的 reducer
 import A0Layout from './layout'
-import A1list from './A1list'
+import Clue from './clue'
+import Martyr from './martyr'
 
 // 合并 reducer
 const rootReducer = combineReducers({
   A0Layout,
-  A1list
+  Clue,
+  Martyr
 })
 
 // 默认导出

+ 28 - 0
展示端/src/store/reducer/martyr.ts

@@ -0,0 +1,28 @@
+// 初始化状态
+import { MartyrItem } from '@/types/api/martyr'
+
+const initState = {
+  // 列表数据
+  tableInfo: {
+    list: [] as MartyrItem[],
+    total: 0
+  }
+}
+
+// 定义 action 类型
+type Props = {
+  type: 'Martyr_APIgetList'
+  payload: { list: MartyrItem[]; total: number }
+}
+
+// reducer
+export default function Reducer(state = initState, action: Props) {
+  switch (action.type) {
+    // 获取列表数据
+    case 'Martyr_APIgetList':
+      return { ...state, tableInfo: action.payload }
+
+    default:
+      return state
+  }
+}

+ 61 - 0
展示端/src/types/api/clue.d.ts

@@ -0,0 +1,61 @@
+export interface ClueItem {
+  address: string
+  city: string
+  createTime: string
+  creatorId: number
+  creatorName: string
+  id: number
+  img: {
+    createTime: string
+    creatorId: number
+    creatorName: string
+    description: string
+    fileName: string
+    filePath: string
+    id: number
+    moduleId: null
+    moduleName: string
+    parentId: null
+    thumb: string
+    type: string
+    updateTime: null
+  }[]
+  imgIds: string
+  martyrId: number
+  martyrName: string
+  num: string
+  province: string
+  region: string
+  remark: string
+  result: string
+  // 0:待确认 | 1:跟进中 | 2:已找到 | 3:未找到 | 4:作废
+  status: number | string
+  supply: string
+  //亲属提供/社会征集/档案资料/现场调查/口述史/媒体报道/部队反馈/其他
+  type:
+    | '亲属提供'
+    | '社会征集'
+    | '档案资料'
+    | '现场调查'
+    | '口述史'
+    | '媒体报道'
+    | '部队反馈'
+    | '其他'
+  updateTime: string
+  video:  {
+    createTime: string
+    creatorId: number
+    creatorName: string
+    description: string
+    fileName: string
+    filePath: string
+    id: number
+    moduleId: null
+    moduleName: string
+    parentId: null
+    thumb: string
+    type: string
+    updateTime: string
+  }[]
+  videoIds: string
+}

+ 77 - 0
展示端/src/types/api/martyr.d.ts

@@ -0,0 +1,77 @@
+ export interface MartyrItem {
+      clue: any ,
+      clueRemark: string,
+      clueType: string,
+      createTime: string,
+      creatorId: number,
+      creatorName: string,
+      dateEnd: string,
+      dateStart: string,
+      dictPanId: number,
+      dictPanName: string,
+      gender: number,
+      id: number,
+      img: any,
+      imgIds: string,
+      intro: string,
+      kinship: any,
+      life: any,
+      link: string,
+      lossAddress: string,
+      lossCity: string,
+      lossProvince: string,
+      lossRegion: string,
+      name: string,
+      nation: string,
+      nativeAddress: string,
+      nativeCity: string,
+      nativeProvince: string,
+      nativeRegion: string, 
+      num: string,  
+      relic: any,
+      remark: string,
+      thumb: string,
+      thumbPc: string,
+      updateTime: string,
+      video: any,
+      videoIds: string,
+      cityId: string,
+    }
+
+  export interface RelativeItem{
+      address: string,
+      city: string,
+      contact: string,
+      createTime: string,
+      creatorId: number,
+      creatorName: string,
+      id: number,
+      img: any,
+      imgIds: string,
+      intro: string,
+      martyrId: number,
+      martyrName: string,
+      name: string,
+      province: string,
+      region: string,
+      relation: null,
+      remark: string,
+      updateTime: string,
+      video: null,
+      videoIds: string
+  }
+
+  export interface RelationShipItem{
+    createTime: string,
+    creatorId: number,
+    creatorName: string,
+    id: number,
+    martyrId: number,
+    martyrName: string,
+    moduleId: number,
+    moduleName: string,
+    relationMartyr: string,
+    relationModule: string,
+    type: string,
+    updateTime: string,
+  }

+ 0 - 2
展示端/src/types/declaration.d.ts

@@ -7,5 +7,3 @@ declare module '*.gif'
 declare module '*.svg'
 declare module 'js-export-excel'
 declare module 'braft-utils'
-
-declare const infoTemo

+ 2 - 0
展示端/src/types/index.d.ts

@@ -1 +1,3 @@
 export * from './api/layot'
+export * from './api/clue'
+export * from './api/martyr'

+ 4 - 29
展示端/src/utils/history.ts

@@ -2,34 +2,9 @@ import { createHashHistory } from 'history'
 const history = createHashHistory()
 export default history
 
-// 判断是手机端还是pc端
-export const isMobileFu = () => {
-  if (
-    window.navigator.userAgent.match(
-      /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
-    )
-  ) {
-    return true
-  } else return false
+type Option = {
+  value: string
+  label: string
+  children?: Option[]
 }
 
-export type A1listType = {
-  id: number
-  name: string
-  partOf: string
-  link: string
-  code: string
-  oldNum: number
-  newNum: number
-  changeSta: boolean
-  loc: 1
-  isSW?: boolean
-  type?: 'x1' | 'x2'
-}
-
-type MyInfoType = {
-  serverUrl: string
-  swArr: A1listType[]
-}
-
-export const myInfo: MyInfoType = infoTemo

+ 5 - 15
展示端/src/utils/http.ts

@@ -1,5 +1,4 @@
 import axios from 'axios'
-import history from './history'
 import { getTokenInfo, removeTokenInfo } from './storage'
 import store from '@/store'
 import { MessageFu } from './message'
@@ -7,16 +6,13 @@ import { domShowFu } from './domShow'
 
 const envFlag = process.env.NODE_ENV === 'development'
 
-const baseUrlTemp = 'https://sit-cctvyunzhan.4dage.com' // 测试环境
-// const baseUrlTemp = 'http://192.168.20.61:8097' // 线下环境
+// const baseUrlTemp = 'https://sit-jinanlsly.4dage.com/' // 测试环境
+const baseUrlTemp = envFlag ? 'http://192.168.20.61:8104/' : 'https://sit-jinanlsly.4dage.com/' // 线下环境
 
-const baseFlag = baseUrlTemp.includes('https://')
+// const baseFlag = baseUrlTemp.includes('https://')
 
 // 请求基地址
-export const baseURL = envFlag
-  ? `${baseUrlTemp}${baseFlag ? '' : '/api/'}`
-  : // ''
-    baseUrlTemp
+export const baseURL = baseUrlTemp
 
 // 处理  类型“AxiosResponse<any, any>”上不存在属性“code”
 declare module 'axios' {
@@ -29,7 +25,7 @@ declare module 'axios' {
 
 // 创建 axios 实例
 const http = axios.create({
-  baseURL: `${baseURL}${baseFlag ? '/api/' : ''}`,
+  baseURL: `${baseURL}api/`,
   timeout: 10000
 })
 
@@ -64,7 +60,6 @@ http.interceptors.response.use(
     }
     if (response.data.code === 5001 || response.data.code === 5002) {
       removeTokenInfo()
-      history.replace('/login')
       clearTimeout(timeId)
       timeId = window.setTimeout(() => {
         MessageFu.warning('登录失效!')
@@ -92,11 +87,6 @@ http.interceptors.response.use(
           err.response.data.msg.length < 30
         ) {
           MessageFu.error(err.response.data.msg)
-          // 没有权限
-          if (err.response.data.code === 5003) {
-            removeTokenInfo()
-            history.replace('/login')
-          }
         } else MessageFu.error('响应错误,请联系管理员!')
       }
     }, 100)

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 8511 - 8405
展示端/yarn.lock