lanxin преди 2 седмици
родител
ревизия
f73f317662
променени са 100 файла, в които са добавени 37135 реда и са изтрити 0 реда
  1. 12 0
      .editorconfig
  2. 11 0
      .prettierrc.js
  3. 2 0
      README.md
  4. 29 0
      config-overrides.js
  5. BIN
      dage-events-1.0.1.tgz
  6. BIN
      dage-krpano-3.0.0.tgz
  7. BIN
      dage-pc-components-1.3.11.tgz
  8. BIN
      dage-service-1.0.5.tgz
  9. BIN
      dage-utils-1.1.1.tgz
  10. 30738 0
      package-lock.json
  11. 71 0
      package.json
  12. 8 0
      path.tsconfig.json
  13. 30 0
      public/index.html
  14. 7 0
      public/krpano.js
  15. 128 0
      public/myData/data.js
  16. BIN
      public/myData/img/intro1.png
  17. BIN
      public/myData/img/intro1_thumb.png
  18. BIN
      public/myData/img/intro2.png
  19. BIN
      public/myData/img/intro2_thumb.png
  20. BIN
      public/myData/img/ore1.png
  21. BIN
      public/myData/img/ore2.png
  22. BIN
      public/myData/intro/intro1.png
  23. BIN
      public/myData/intro/intro1_thumb.png
  24. BIN
      public/myData/intro/intro2.png
  25. BIN
      public/myData/intro/intro2_thumb.png
  26. BIN
      public/myData/three/intro1.png
  27. BIN
      public/myData/three/intro1_thumb.png
  28. 1247 0
      public/tour.xml
  29. 897 0
      public/webvr.xml
  30. 85 0
      src/App.tsx
  31. BIN
      src/assets/img/IMGerror.png
  32. BIN
      src/assets/img/back.png
  33. BIN
      src/assets/img/btn_bg.png
  34. BIN
      src/assets/img/close.png
  35. BIN
      src/assets/img/detailBackBtn.png
  36. BIN
      src/assets/img/dot.png
  37. BIN
      src/assets/img/dot_bg.png
  38. BIN
      src/assets/img/foldBtn.png
  39. BIN
      src/assets/img/full.png
  40. BIN
      src/assets/img/fullx.png
  41. BIN
      src/assets/img/home_bg.png
  42. BIN
      src/assets/img/home_bg_M.png
  43. BIN
      src/assets/img/intro_bg.png
  44. BIN
      src/assets/img/intro_bg_M.png
  45. BIN
      src/assets/img/landtip.png
  46. BIN
      src/assets/img/loading.gif
  47. BIN
      src/assets/img/logo.png
  48. BIN
      src/assets/img/logo_M.png
  49. BIN
      src/assets/img/lookBtnBg.png
  50. BIN
      src/assets/img/lookBtnBg_ac.png
  51. BIN
      src/assets/img/lookItem_bg1.png
  52. BIN
      src/assets/img/lookItem_bg2.png
  53. BIN
      src/assets/img/lookItem_bg3.png
  54. BIN
      src/assets/img/look_bg.png
  55. BIN
      src/assets/img/look_bg_M.png
  56. BIN
      src/assets/img/look_title.png
  57. BIN
      src/assets/img/panoBottom_bg.png
  58. BIN
      src/assets/img/tabBtnBg.png
  59. BIN
      src/assets/img/tabBtnBg_ac.png
  60. BIN
      src/assets/img/title.png
  61. BIN
      src/assets/img/title_M.png
  62. BIN
      src/assets/img/unfoldBtn.png
  63. 222 0
      src/assets/styles/base.css
  64. 341 0
      src/assets/styles/base.less
  65. 21 0
      src/components/AsyncSpinLoding/index.module.scss
  66. 15 0
      src/components/AsyncSpinLoding/index.tsx
  67. 21 0
      src/components/AuthRoute/index.tsx
  68. 51 0
      src/components/ImageLazy/index.module.scss
  69. 63 0
      src/components/ImageLazy/index.tsx
  70. 65 0
      src/components/LookDom/index.module.scss
  71. 52 0
      src/components/LookDom/index.tsx
  72. 29 0
      src/components/Message/index.tsx
  73. 48 0
      src/components/MyPopconfirm.tsx
  74. 21 0
      src/components/MyTable/index.module.scss
  75. 171 0
      src/components/MyTable/index.tsx
  76. 26 0
      src/components/NotFound/index.tsx
  77. 10 0
      src/components/SpinLoding/index.module.scss
  78. 13 0
      src/components/SpinLoding/index.tsx
  79. 43 0
      src/components/UpAsyncLoding/index.module.scss
  80. 38 0
      src/components/UpAsyncLoding/index.tsx
  81. 195 0
      src/components/ZRichTexts/index.module.scss
  82. 429 0
      src/components/ZRichTexts/index.tsx
  83. 63 0
      src/components/ZupAudio/index.module.scss
  84. 135 0
      src/components/ZupAudio/index.tsx
  85. 101 0
      src/components/ZupOne/index.module.scss
  86. 295 0
      src/components/ZupOne/index.tsx
  87. 215 0
      src/components/ZupTypes/index.module.scss
  88. 585 0
      src/components/ZupTypes/index.tsx
  89. 38 0
      src/index.tsx
  90. 48 0
      src/pages/A1home/index.module.scss
  91. 21 0
      src/pages/A1home/index.tsx
  92. 61 0
      src/pages/A2layout/index.module.scss
  93. 65 0
      src/pages/A2layout/index.tsx
  94. 134 0
      src/pages/A3Pano/index.module.scss
  95. 126 0
      src/pages/A3Pano/index.tsx
  96. 40 0
      src/pages/A4Scene/index.module.scss
  97. 16 0
      src/pages/A4Scene/index.tsx
  98. 33 0
      src/pages/components/Dot/index.module.scss
  99. 20 0
      src/pages/components/Dot/index.tsx
  100. 0 0
      src/pages/components/Intro/index.module.scss

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+root = true # 控制配置文件 .editorconfig 是否生效的字段
+ 
+[**] # 匹配全部文件
+indent_style = space # 缩进风格,可选space|tab
+indent_size = 2 # 缩进的空格数
+charset = utf-8 # 设置字符集
+trim_trailing_whitespace = true # 删除一行中的前后空格
+insert_final_newline = true # 设为true表示使文件以一个空白行结尾
+end_of_line = lf
+ 
+[**.md] # 匹配md文件
+trim_trailing_whitespace = false

+ 11 - 0
.prettierrc.js

@@ -0,0 +1,11 @@
+module.exports = {
+  printWidth: 90, // 一行的字符数,如果超过会进行换行
+  tabWidth: 2, // 一个tab代表几个空格数,默认就是2
+  useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
+  semi: false, // 行尾是否使用分号,默认为true
+  singleQuote: true, // 字符串是否使用单引号
+  trailingComma: 'none', // 对象或数组末尾是否添加逗号 none| es5| all
+  jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
+  bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
+  arrowParens: 'avoid' // 箭头函数如果只有一个参数则省略括号
+}

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+1.使用 yarn。 npm有问题
+

+ 29 - 0
config-overrides.js

@@ -0,0 +1,29 @@
+const path = require('path')
+const { override, addWebpackAlias, adjustStyleLoaders } = require('customize-cra')
+
+// 添加 @ 别名
+const webpackAlias = addWebpackAlias({
+  '@': path.resolve(__dirname, 'src')
+})
+
+// 调整 sass-loader 配置,使用现代 API
+const fixSassDeprecation = adjustStyleLoaders(rule => {
+  if (rule.test.toString().includes('scss')) {
+    // 找到 sass-loader 并修改选项
+    rule.use
+      .filter(({ loader }) => loader === 'sass-loader')
+      .forEach(loader => {
+        loader.options = {
+          ...loader.options,
+          // 指定使用现代 API
+          sassOptions: {
+            ...loader.options?.sassOptions,
+            api: 'modern'
+          }
+        }
+      })
+  }
+})
+
+// 导出组合配置
+module.exports = override(webpackAlias, fixSassDeprecation)

BIN
dage-events-1.0.1.tgz


BIN
dage-krpano-3.0.0.tgz


BIN
dage-pc-components-1.3.11.tgz


BIN
dage-service-1.0.5.tgz


BIN
dage-utils-1.1.1.tgz


Файловите разлики са ограничени, защото са твърде много
+ 30738 - 0
package-lock.json


+ 71 - 0
package.json

@@ -0,0 +1,71 @@
+{
+  "name": "demo2",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
+    "@ant-design/cssinjs": "^1.5.6",
+    "@dage/krpano": "./dage-krpano-3.0.0.tgz",
+    "@dage/pc-components": "./dage-pc-components-1.3.11.tgz",
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^13.5.0",
+    "@types/jest": "^27.5.2",
+    "@types/node": "^16.18.3",
+    "@types/react": "^18.0.24",
+    "@types/react-dom": "^18.0.8",
+    "@zxing/library": "^0.21.3",
+    "antd": "^5.8.3",
+    "antd-mobile": "^5.30.0",
+    "axios": "^1.1.3",
+    "braft-editor": "^2.3.9",
+    "braft-utils": "^3.0.12",
+    "dayjs": "^1.11.10",
+    "js-base64": "^3.7.3",
+    "react": "^18.2.0",
+    "react-countup": "^6.5.3",
+    "react-dom": "^18.2.0",
+    "react-redux": "^8.0.4",
+    "react-router-dom": "^6.22.0",
+    "react-scripts": "5.0.1",
+    "react-sortablejs": "^6.1.4",
+    "redux": "^4.2.0",
+    "redux-devtools-extension": "^2.13.9",
+    "redux-thunk": "^2.4.1",
+    "sass": "^1.69.5",
+    "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",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@types/history": "^5.0.0",
+    "@types/react-router-dom": "^5.3.3",
+    "customize-cra": "^1.0.0",
+    "query-string": "^8.1.0",
+    "react-app-rewired": "^2.2.1"
+  },
+  "homepage": "."
+}

+ 8 - 0
path.tsconfig.json

@@ -0,0 +1,8 @@
+{
+    "compilerOptions": {
+      "baseUrl": "./",
+      "paths": {
+        "@/*": ["src/*"]
+      }
+    }
+  }

+ 30 - 0
public/index.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="zh">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta name="description" content="Web site created using create-react-app" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
+    />
+    <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>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+  </body>
+  <script src="./krpano.js"></script>
+
+  <!-- <script>
+    
+    window.onload = function () {
+      console.log(我在onload)
+    }
+  </script> -->
+</html>

Файловите разлики са ограничени, защото са твърде много
+ 7 - 0
public/krpano.js


+ 128 - 0
public/myData/data.js

@@ -0,0 +1,128 @@
+const BaseUrl = 'https://houseoss.4dkankan.com/project/jining/static/'
+
+const infoTemo = {
+  // 展馆简介
+  introInfo: [
+    {
+      // 大图
+      pic: './myData/intro/intro1.png',
+      // 缩略图
+      thumb: './myData/intro/intro1_thumb.png',
+      text: `集宁战役纪念馆(Tsining War
+              Memorial)位于内蒙古自治区乌兰察布市集宁区乌兰察布老虎山生态公园内。纪念馆的建成,对于缅怀革命先烈、发扬革命传统、弘扬爱国精神、教育激励后人具有重要意义。
+              2021年6月19日,被中央宣传部命名为“全国爱国主义教育示范基地”。`
+    },
+    {
+      pic: './myData/intro/intro2.png',
+      thumb: './myData/intro/intro2_thumb.png',
+      text: `集宁战役纪念馆(Tsining War Memorial)。`
+    }
+  ],
+  // 馆藏鉴赏
+  oreInfo: {
+    // 三维
+    three: [
+      {
+        // 封面图
+        pic: '',
+        // 标题
+        title: '1948石碑',
+        // 文字介绍
+        text: '1949年10月24日,集宁战役开始,100多名革命先烈被派往集宁。',
+        // 三维链接
+        link: 'https://4dscene.4dage.com/culturalrelics/demo2/Model.html?m=JN-1948shibei',
+        // 文字介绍下面的多张图
+        otherPic: []
+      },
+      {
+        pic: '',
+        title: '人民英雄纪念碑',
+        text: '1949年10月24日,集宁战役开始,100多名革命先烈被派往集宁。',
+        link: 'https://4dscene.4dage.com/culturalrelics/demo2/Model.html?m=JN-RMYXjinianbei',
+        otherPic: []
+      },
+      {
+        pic: '',
+        title: '英雄像',
+        text: '1949年10月24日,集宁战役开始,100多名革命先烈被派往集宁。',
+        link: 'https://4dscene.4dage.com/culturalrelics/demo2/Model.html?m=JN-YXxiang',
+        otherPic: []
+      }
+    ],
+    two: [
+      // {
+      //   pic: 'ore/ore2.png',
+      //   title: '1949年10月25日',
+      //   text: '1949年10月25日,集宁战役结束,100多名革命先烈被派往集宁。',
+      //   link: '',
+      //   otherPic: ['./myData/intro/intro1_thumb.png']
+      // },
+      // {
+      //   pic: './myData/intro/intro1_thumb.png',
+      //   title: '1949年10月24日',
+      //   text: '1949年10月24日,集宁战役开始,100多名革命先烈被派往集宁。',
+      //   link: '',
+      //   otherPic: ['./myData/intro/intro1_thumb.png']
+      // }
+    ]
+  },
+  // 全景信息
+  panoramicInfo: {
+    dotname: '',
+    sky: [
+      { sceneName: 'p1', thumbUrl: 'panos/s1.tiles/thumb.jpg' },
+      { sceneName: 'p2', thumbUrl: 'panos/s2.tiles/thumb.jpg' },
+      { sceneName: 'p3', thumbUrl: 'panos/s3.tiles/thumb.jpg' },
+      { sceneName: 'p4', thumbUrl: 'panos/s4.tiles/thumb.jpg' },
+      { sceneName: 'p5', thumbUrl: 'panos/s5.tiles/thumb.jpg' },
+      { sceneName: 'p6', thumbUrl: 'panos/s6.tiles/thumb.jpg' },
+      { sceneName: 'p7', thumbUrl: 'panos/s7.tiles/thumb.jpg' },
+      { sceneName: 'p8', thumbUrl: 'panos/s8.tiles/thumb.jpg' },
+      { sceneName: 'p9', thumbUrl: 'panos/s9.tiles/thumb.jpg' },
+      { sceneName: 'p10', thumbUrl: 'panos/s10.tiles/thumb.jpg' },
+      { sceneName: 'p11', thumbUrl: 'panos/s11.tiles/thumb.jpg' },
+      { sceneName: 'p12', thumbUrl: 'panos/s12.tiles/thumb.jpg' },
+      { sceneName: 'p13', thumbUrl: 'panos/s13.tiles/thumb.jpg' },
+      { sceneName: 'p14', thumbUrl: 'panos/s14.tiles/thumb.jpg' },
+      { sceneName: 'p15', thumbUrl: 'panos/s15.tiles/thumb.jpg' },
+      { sceneName: 'p16', thumbUrl: 'panos/s16.tiles/thumb.jpg' },
+      { sceneName: 'p17', thumbUrl: 'panos/s17.tiles/thumb.jpg' },
+      { sceneName: 'p18', thumbUrl: 'panos/s18.tiles/thumb.jpg' },
+      { sceneName: 'p19', thumbUrl: 'panos/s19.tiles/thumb.jpg' }
+    ],
+    floor: [
+      { sceneName: '1', thumbUrl: 'panos/1.tiles/thumb.jpg' },
+      { sceneName: '2', thumbUrl: 'panos/2.tiles/thumb.jpg' },
+      { sceneName: '3', thumbUrl: 'panos/3.tiles/thumb.jpg' },
+      { sceneName: '4', thumbUrl: 'panos/4.tiles/thumb.jpg' },
+      { sceneName: '5', thumbUrl: 'panos/5.tiles/thumb.jpg' },
+      { sceneName: '6', thumbUrl: 'panos/6.tiles/thumb.jpg' },
+      { sceneName: '7', thumbUrl: 'panos/7.tiles/thumb.jpg' },
+      { sceneName: '8', thumbUrl: 'panos/8.tiles/thumb.jpg' },
+      { sceneName: '9', thumbUrl: 'panos/9.tiles/thumb.jpg' },
+      { sceneName: '10', thumbUrl: 'panos/10.tiles/thumb.jpg' },
+      { sceneName: '11', thumbUrl: 'panos/11.tiles/thumb.jpg' },
+      { sceneName: '12', thumbUrl: 'panos/12.tiles/thumb.jpg' },
+      { sceneName: '13', thumbUrl: 'panos/13.tiles/thumb.jpg' },
+      { sceneName: '14', thumbUrl: 'panos/14.tiles/thumb.jpg' },
+      { sceneName: '15', thumbUrl: 'panos/15.tiles/thumb.jpg' },
+      { sceneName: '16', thumbUrl: 'panos/16.tiles/thumb.jpg' },
+      { sceneName: '17', thumbUrl: 'panos/17.tiles/thumb.jpg' },
+      { sceneName: '18', thumbUrl: 'panos/18.tiles/thumb.jpg' },
+      { sceneName: '19', thumbUrl: 'panos/19.tiles/thumb.jpg' },
+      { sceneName: '20', thumbUrl: 'panos/20.tiles/thumb.jpg' },
+      { sceneName: '21', thumbUrl: 'panos/21.tiles/thumb.jpg' },
+      { sceneName: '22', thumbUrl: 'panos/22.tiles/thumb.jpg' },
+      { sceneName: '23', thumbUrl: 'panos/23.tiles/thumb.jpg' },
+      { sceneName: '24', thumbUrl: 'panos/24.tiles/thumb.jpg' },
+      { sceneName: '25', thumbUrl: 'panos/25.tiles/thumb.jpg' },
+      { sceneName: '26', thumbUrl: 'panos/26.tiles/thumb.jpg' },
+      { sceneName: '27', thumbUrl: 'panos/27.tiles/thumb.jpg' },
+      { sceneName: '28', thumbUrl: 'panos/28.tiles/thumb.jpg' },
+      { sceneName: '29', thumbUrl: 'panos/29.tiles/thumb.jpg' },
+      { sceneName: '30', thumbUrl: 'panos/30.tiles/thumb.jpg' }
+    ]
+  },
+  // 3D实景
+  sceneCode: 'SG-3wLtGOcwy3K'
+}

BIN
public/myData/img/intro1.png


BIN
public/myData/img/intro1_thumb.png


BIN
public/myData/img/intro2.png


BIN
public/myData/img/intro2_thumb.png


BIN
public/myData/img/ore1.png


BIN
public/myData/img/ore2.png


BIN
public/myData/intro/intro1.png


BIN
public/myData/intro/intro1_thumb.png


BIN
public/myData/intro/intro2.png


BIN
public/myData/intro/intro2_thumb.png


BIN
public/myData/three/intro1.png


BIN
public/myData/three/intro1_thumb.png


Файловите разлики са ограничени, защото са твърде много
+ 1247 - 0
public/tour.xml


+ 897 - 0
public/webvr.xml

@@ -0,0 +1,897 @@
+<krpano>
+	<!--
+		webvr.xml
+		- krpano 1.19
+	-->
+
+	<!-- load the WebVR plugin and assign it to a 'webvr' variable for easier usage -->
+	<plugin name="WebVR" devices="html5" keep="true"
+	        url="webvr.js"
+	        onloaded="copy(webvr, plugin[WebVR]);"
+	        mousespeed="0.00125"
+	        multireslock="true"
+	        fullscreen_mirroring="true"
+	        mobilevr_support="true"
+	        mobilevr_ipd="63.5"
+	        mobilevr_screensize="auto"
+	        mobilevr_lens_overlap="1.0"
+	        mobilevr_lens_fov="96"
+	        mobilevr_lens_dist="0.6"
+	        mobilevr_lens_dist2="1|0|0|0"
+	        mobilevr_lens_ca="0.0"
+	        mobilevr_lens_vign="100"
+	        mobilevr_wakelock="true"
+	        mobilevr_sensor_mode="3"
+	        mobilevr_autocalibration="false"
+	        mobilevr_touch_support="true"
+	        mobilevr_fake_support="false"
+	        vr_cursor="hotspot[vr_cursor]"
+	        vr_cursor_enabled="true"
+	        vr_cursor_onover="if(handcursor, tween(hotspot[vr_cursor].scale,0.4,0.1); vr_auto_click(get(vr_timeout)); );"
+	        vr_cursor_onout="tween(hotspot[vr_cursor].scale,0.3,0.1);"
+	        onavailable="webvr_onavailable();"
+	        onunavailable=""
+	        onunknowndevice="webvr_onunknowndevice();"
+	        onentervr="webvr_onentervr();"
+	        onexitvr="webvr_onexitvr();"
+	        />
+
+	
+	<!-- a custom xml data structure with the supported VR headsets -->
+	<vrheadsets>
+		<headset name="cb1" caption="Cardboard A"   overlap="1.10" fov="96.0"  dist="1.00" dist2="1|0|0|0" ca="0.000" vig="100" />
+		<headset name="cb2" caption="Cardboard B"   overlap="1.00" fov="96.0"  dist="0.60" dist2="1|0|0|0" ca="0.000" vig="100" />
+		<headset name="gvr" caption="GearVR"        overlap="1.00" fov="112.0" dist="0.95" dist2="1|0|0|0" ca="0.090" vig="100" />
+		<headset name="hom" caption="HOMiDO"        overlap="1.00" fov="101.0" dist="1.10" dist2="1|0|0|0" ca="0.075" vig="100" />
+		<headset name="one" caption="VR ONE"        overlap="1.00" fov="109.9" dist="0.00" dist2="1.139|0.093|0.018|0.207" ca="0.090" vig="35" />
+		<headset name="ccr" caption="ColorCross VR" overlap="1.00" fov="70.0"  dist="0.65" dist2="1|0|0|0" ca="0.000" vig="100" />
+		<headset name="nod" caption="No Distortion" overlap="1.00" fov="96.0"  dist="0.00" dist2="1|0|0|0" ca="0.000" vig="100" />
+	</vrheadsets>
+
+
+	<!-- the VR cursor hotspot -->
+	<hotspot name="vr_cursor" keep="true"
+	         url="webvr_cursor_80x80_17f.png"
+	         visible="false"
+	         enabled="false"
+	         distorted="true"
+	         crop="0|0|80|80"
+	         scale="0.3"
+	         depth="1000"
+	         />
+
+
+	<!-- vr_auto_click() - call this action in the onover event of a
+	     hotspot to trigger automatically a click after some time.  -->
+	<action name="vr_auto_click">
+		if(webvr.isenabled,
+			if(%1 != null, set(vr_aclk_timeout, %1), set(vr_aclk_timeout, 2000));
+			copy(vr_aclk_t1, timertick);
+			set(vr_aclk_waiting, true);
+			copy(vr_aclk_hotspot, name);
+			set(hotspot[vr_cursor].crop,'0|0|80|80');
+
+			asyncloop(vr_aclk_waiting AND vr_aclk_hotspot == name,
+				sub(dt, timertick,vr_aclk_t1);
+
+				if(!hovering,
+					set(vr_aclk_waiting, false);
+					set(hotspot[vr_cursor].crop,'0|0|80|80');
+				  ,
+					div(f, dt, vr_aclk_timeout);
+					mul(f, 16);
+					roundval(f);
+					Math.min(f, 16);
+					mul(f, 80);
+
+					txtadd(hotspot[vr_cursor].crop,get(f),'|0|80|80');
+
+					<!-- wait another 100ms delay after finishing the animation before doing the click -->
+					sub(dt, 100);
+					if(dt GT vr_aclk_timeout,
+						set(vr_aclk_waiting,false);
+						set(hotspot[vr_cursor].crop,'0|0|80|80');
+						<!-- call onclick -->
+						onclick();
+					  );
+				  );
+				);
+		  );
+	</action>
+
+
+	<!-- by pressing SPACE the Oculus Rift could be re-centered -->
+	<events name="webvr_events" devices="html5" keep="true"
+	        onkeydown="if(webvr AND webvr.isenabled AND keycode==32, webvr.resetSensor() );"
+	        onmousedown="if(webvr AND webvr.isenabled, webvr_showbuttons() );"
+	        />
+
+
+	<!-- when WebVR support is available show an EnterVR button -->
+	<action name="webvr_onavailable">
+		webvr.loadsettings();
+		delayedcall(0.5, tween(layer[webvr_enterbutton].alpha,1.0); );
+	</action>
+	
+	
+	<action name="webvr_onunknowndevice">
+		if(webvr.isfake AND device.desktop AND webvr.havesettings == false,
+			<!-- set the 'no distortion' headset for fake desktop usage -->
+			set(webvr.mobilevr_lens_overlap, 1.0);
+			set(webvr.mobilevr_lens_fov, 96.0);
+			set(webvr.mobilevr_lens_dist, 0.0);
+			set(webvr.mobilevr_lens_dist2, '1|0|0|0');
+			set(webvr.mobilevr_lens_ca, 0.0);
+			set(webvr.mobilevr_lens_vign, 100);
+		  ,
+			set(ask_user_for_screensize,true);
+		  );
+	</action>
+
+
+	<action name="webvr_onentervr">
+		tween(layer[webvr_enterbutton].alpha,0,0);
+
+		webvr_showbuttons();
+		webvr_hide_all_non_vr_layers();
+
+		<!-- when the screen size is unknown an no custom size is set, open the setup screen on entering the VR mode -->
+		if(webvr.ismobilevr == true AND !webvr.isfake AND ask_user_for_screensize == true AND webvr.mobilevr_screensize == 'auto',
+			set(ask_user_for_screensize, false);
+			vr_setup();
+		  );
+		if(webvr.isfake,
+			webvr_show_fakemode_info(true);
+		  );
+	</action>
+
+
+	<action name="webvr_onexitvr">
+		stopdelayedcall(vr_button_fadeout);
+
+		tween(layer[webvr_enterbutton].alpha,1);
+		tween(layer[webvr_exitbutton].alpha,0);
+		tween(layer[webvr_setupbutton].alpha,0);
+		
+		webvr_show_fakemode_info(false);
+
+		webvr_restore_layers();
+	</action>
+
+
+	<action name="webvr_hide_all_non_vr_layers">
+		for(set(i,0), i LT layer.count, inc(i),
+			copy(lr, layer[get(i)]);
+			if(lr.vr !== true,
+				copy(lr.vr_backup_visible, lr.visible);
+				set(lr.visible, false);
+			  );
+		  );
+	</action>
+
+	<action name="webvr_restore_layers">
+		for(set(i,0), i LT layer.count, inc(i),
+			copy(lr, layer[get(i)]);
+			if(lr.vr_backup_visible,
+				copy(lr.visible, lr.vr_backup_visible);
+				delete(lr.vr_backup_visible);
+			  );
+		  );
+	</action>
+	
+	<action name="webvr_show_fakemode_info">
+		if('%1' == 'true',
+			addlayer(webvr_fakemode_info);
+			set(layer[webvr_fakemode_info].url, '%SWFPATH%/plugins/textfield.swf');
+			set(layer[webvr_fakemode_info].keep, true);
+			set(layer[webvr_fakemode_info].align, 'bottom');
+			set(layer[webvr_fakemode_info].y, 80);
+			set(layer[webvr_fakemode_info].background, false);
+			set(layer[webvr_fakemode_info].css, 'color:#FFFFFF;text-align:center;');
+			set(layer[webvr_fakemode_info].html, '[i][u]Simulated WebVR Mode![/u][/i][br]For real WebVR with headset tracking, either use a [a href="http://webvr.info" target="_blank" style="color:#FFFFFF;"]WebVR-API-capable[/a] desktop browser or a mobile device and a VR headset.');
+		  ,
+			removelayer(webvr_fakemode_info);
+		  );
+	</action>
+	
+	
+	<!-- ensure the same scaling on mobiles (regardless if mobilescale is 0.5 or 1.0) -->
+	<krpano webvr_setup_scale="calc:(1.0 + 1.0*(device.mobile AND stagescale LT 1.0)) / (1.0 + 1.0*device.mobile)"
+	        webvr_button_scale.normal="1.0"
+	        webvr_button_scale.mobile="1.6"
+	        />
+
+	
+	<!-- the EnterVR/ExitVR and SetupVR buttons -->
+	<style name="webvr_button_style"
+	       url="%SWFPATH%/plugins/textfield.swf"
+	       backgroundcolor="0x000000"
+	       backgroundalpha="0.5"
+	       roundedge="calc:9*webvr_setup_scale*webvr_button_scale"
+	       css="calc:'color:#FFFFFF;font-size:' + 20*webvr_setup_scale*webvr_button_scale + 'px;'"
+	       padding="calc:6*webvr_setup_scale*webvr_button_scale + ' ' + 10*webvr_setup_scale*webvr_button_scale"
+	       />
+	
+	<layer name="webvr_enterbutton" keep="true" vr="true"
+	       style="webvr_button_style"
+	       html="Enter VR"
+	       align="top" y="24"
+	       autoalpha="true" alpha="0.0"
+	       onclick="webvr.enterVR();"
+	       />
+
+	<layer name="webvr_exitbutton" keep="true" vr="true"
+	       style="webvr_button_style"
+	       html="Exit VR"
+	       align="top" y="24"
+	       autoalpha="true" alpha="0.0"
+	       onclick="webvr.exitVR();"
+	       />
+
+	<layer name="webvr_setupbutton" keep="true" vr="true"
+	       style="webvr_button_style"
+	       html="VR Setup"
+	       align="bottom" y="24"
+	       autoalpha="true" alpha="0.0"
+	       onclick="vr_setup()"
+	       />
+
+
+	<action name="webvr_showbuttons">
+		stopdelayedcall(vr_button_fadeout);
+		if(webvr.ismobilevr,
+			tween(layer[webvr_exitbutton].alpha|layer[webvr_setupbutton].alpha, 1.0|1.0, 0.25);
+			delayedcall(vr_button_fadeout, 3.0, tween(layer[webvr_exitbutton].alpha|layer[webvr_setupbutton].alpha, 0.0|0.0, 1.0); );
+		  ,
+			tween(layer[webvr_exitbutton].alpha, 1.0, 0.25);
+			delayedcall(vr_button_fadeout, 3.0, tween(layer[webvr_exitbutton].alpha, 0.0, 1.0); );
+		  );
+	</action>
+
+
+
+	<!--
+		VR Setup
+	-->
+
+	<action name="vr_setup">
+		<!-- disable cursor -->
+		set(webvr.vr_cursor_enabled, false);
+		
+		<!-- hide VR buttons -->
+		tween(layer[webvr_exitbutton].alpha,0);
+		tween(layer[webvr_setupbutton].alpha,0);
+		
+		<!-- create background layer -->
+		addlayer(vr_setup_bg);
+		set(layer[vr_setup_bg].type, container);
+		set(layer[vr_setup_bg].bgcolor, 0x000000);
+		set(layer[vr_setup_bg].bgalpha, 0.5);
+		set(layer[vr_setup_bg].bgcapture, true);
+		set(layer[vr_setup_bg].handcursor, false);
+		set(layer[vr_setup_bg].align, lefttop);
+		set(layer[vr_setup_bg].width, 100%);
+		set(layer[vr_setup_bg].height, 100%);
+		set(layer[vr_setup_bg].zorder, 99999);
+		
+		<!-- get and prepare device infos and settings -->
+		copy(i_screensize, webvr.mobilevr_screensize);
+		if(i_screensize == 'auto', copy(i_screensize, webvr.devicesize));
+		if(i_screensize LE 0, set(i_screensize, 5.0));
+		roundval(i_screensize, 1);
+		txtadd(i_screensize, ' inch');
+
+		copy(i_ipd, webvr.mobilevr_ipd);
+		roundval(i_ipd, 1);
+		txtadd(i_ipd, ' mm');
+
+		copy(i_fov, webvr.mobilevr_lens_fov);
+		roundval(i_fov, 1);
+
+		copy(i_dist, webvr.mobilevr_lens_dist);
+		roundval(i_dist, 2);
+		
+		copy(i_dist2, webvr.mobilevr_lens_dist2);
+		txtsplit(i_dist2, '|', i_dist2_k1, i_dist2_k2, i_dist2_k3, i_dist2_k4);
+		mul(i_dist2_k1,1);
+		mul(i_dist2_k2,10);
+		mul(i_dist2_k3,10);
+		mul(i_dist2_k4,10);
+		roundval(i_dist2_k1,2);
+		roundval(i_dist2_k2,2);
+		roundval(i_dist2_k3,2);
+		roundval(i_dist2_k4,2);
+
+		copy(i_vig, webvr.mobilevr_lens_vign);
+		roundval(i_vig, 0);
+		
+		copy(i_overlap, webvr.mobilevr_lens_overlap);
+		roundval(i_overlap, 2);
+		
+		copy(i_ca, webvr.mobilevr_lens_ca);
+		roundval(i_ca, 3);
+
+		set(i_headset, 'Custom');
+		for(set(i,0), i LT vrheadsets.headset.count, inc(i),
+			copy(hs, vrheadsets.headset[get(i)]);
+			if(i_overlap == hs.overlap AND i_fov == hs.fov AND i_dist == hs.dist AND i_dist2 == hs.dist2 AND i_ca == hs.ca AND i_vig == hs.vig , copy(i_headset, hs.caption));
+		   );
+
+		<!-- when the screen size is unknown, mark it red -->
+		set(known_size, true);
+		set(sizcol, #FFFFFF);
+		copy(i_devicename, webvr.devicename);
+		if(i_devicename == 'Unknown',
+			if(webvr.mobilevr_screensize == 'auto',
+				set(sizcol, #AA0000);
+				set(known_size, false);
+			  ,
+				set(i_devicename, 'Custom');
+			  );
+		  );
+
+		
+		<!-- create layer for the main menu -->
+		addlayer(vr_setup_m1);
+		set(layer[vr_setup_m1].type, container);
+		set(layer[vr_setup_m1].parent, vr_setup_bg);
+		set(layer[vr_setup_m1].align, lefttop);
+		set(layer[vr_setup_m1].width, 100%);
+		set(layer[vr_setup_m1].height, 100%);
+		
+		<!-- create layer for the headset customization menu -->
+		addlayer(vr_setup_m3);
+		set(layer[vr_setup_m3].type, container);
+		set(layer[vr_setup_m3].parent, vr_setup_bg);
+		set(layer[vr_setup_m3].align, lefttop);
+		set(layer[vr_setup_m3].width, 100%);
+		set(layer[vr_setup_m3].height, 100%);
+		set(layer[vr_setup_m3].visible, false);
+		
+		<!-- create layer for the calibration menu -->
+		addlayer(vr_setup_m2);
+		set(layer[vr_setup_m2].type, container);
+		set(layer[vr_setup_m2].parent, vr_setup_bg);
+		set(layer[vr_setup_m2].align, lefttop);
+		set(layer[vr_setup_m2].width, 100%);
+		set(layer[vr_setup_m2].height, 100%);
+		set(layer[vr_setup_m2].visible, false);
+		
+		<!-- create the text elements -->
+		set(vr_setup_text_parent, 'vr_setup_m1');
+		vr_setup_createtext(vr_setup_title, 'MOBILE VR SETUP',       center, center, 0, -225, #FFFFFF,     false);
+
+		vr_setup_createtext(vr_setup_dvn1, 'Device:',         center, right,  0, -145, #FFFFFF,     true, vr_setup_select('screen') );
+		vr_setup_createtext(vr_setup_dvn2, get(i_devicename), center, left,   0, -145, get(sizcol), true, vr_setup_select('screen') );
+		vr_setup_createtext(vr_setup_siz1, 'Screensize:',     center, right,  0, -105, #FFFFFF,     true, vr_setup_select('screen') );
+		vr_setup_createtext(vr_setup_siz2, get(i_screensize), center, left,   0, -105, get(sizcol), true, vr_setup_select('screen') );
+
+		vr_setup_createtext(vr_setup_ipd1, 'IPD:',            center, right,  0,  -35, #FFFFFF,     true, vr_setup_select('ipd') );
+		vr_setup_createtext(vr_setup_ipd2, get(i_ipd),        center, left,   0,  -35, #FFFFFF,     true, vr_setup_select('ipd') );
+
+		vr_setup_createtext(vr_setup_hmd1, 'VR Headset:',     center, right,  0,  +35, #FFFFFF,     true, vr_setup_select('headset') );
+		vr_setup_createtext(vr_setup_hmd2, get(i_headset),    center, left,   0,  +35, #FFFFFF,     true, vr_setup_select('headset') );
+		
+		vr_setup_createtext(vr_setup_hmd3, 'Customize',       center, center, 0,  +75, #FFFFFF,     true, set(background,true), set(background,false), vr_setup_customize_headset() );
+
+		
+
+		if(webvr.iswebvr == false,
+			vr_setup_createtext(vr_setup_cal, 'Calibrate Gyroscope',   center, center,    0, +145, #FFFFFF,     true, set(background,true), set(background,false), vr_setup_calibration() );
+		  );
+
+		vr_setup_createtext(vr_setup_sav, 'SAVE',          center, center, -200, +225, #FFFFFF,     true, set(background,true), set(background,false), vr_setup_save() );
+		vr_setup_createtext(vr_setup_rst, 'RESET',         center, center,    0, +225, #FFFFFF,     true, set(background,true), set(background,false), vr_setup_reset() );
+		vr_setup_createtext(vr_setup_cls, 'CLOSE',         center, center, +200, +225, #FFFFFF,     true, set(background,true), set(background,false), vr_setup_close() );
+		
+		<!-- and the adjusting buttons -->
+		vr_setup_createbutton(vr_setup_btn1, '&#60;', left,  left,  5%, -35, #FFFFFF, true, null);
+		vr_setup_createbutton(vr_setup_btn2, '&#62;', right, right, 5%, -35, #FFFFFF, true, null);
+		
+		
+		<!-- create the customize_headset text elements -->
+		set(vr_setup_text_parent, 'vr_setup_m3');
+		vr_setup_createtext(vr_setup_m31, 'VR HEADSET', center, center, 0, -225, #FFFFFF, false);
+		
+		vr_setup_createtext(vr_setup_fov1,  'FOV:',           center, right,  0,  -80,  #FFFFFF,    true, vr_setup_select('fov') );
+		vr_setup_createtext(vr_setup_fov2, get(i_fov),        center, left,   0,  -80,  #FFFFFF,    true, vr_setup_select('fov') );
+		vr_setup_createtext(vr_setup_dst1, 'Distortion:',     center, right,  0,  -32,  #FFFFFF,    true, vr_setup_select('dist') );
+		vr_setup_createtext(vr_setup_dst2, get(i_dist),       center, left,   0,  -32,  #FFFFFF,    true, vr_setup_select('dist') );
+		vr_setup_createtext(vr_setup_d2tx, 'Dist2:',          center, right,  0,  +16,  #FFFFFF,    true, vr_setup_select('dist2k1') );
+		vr_setup_createtext(vr_setup_d2k1, get(i_dist2_k1),   center, left,   0,  +16,  #FFFFFF,    true, vr_setup_select('dist2k1') );
+		vr_setup_createtext(vr_setup_d2k2, get(i_dist2_k2),   center, left, +100, +16,  #FFFFFF,    true, vr_setup_select('dist2k2') );
+		vr_setup_createtext(vr_setup_d2k3, get(i_dist2_k3),   center, left, +200, +16,  #FFFFFF,    true, vr_setup_select('dist2k3') );
+		vr_setup_createtext(vr_setup_d2k4, get(i_dist2_k4),   center, left, +300, +16,  #FFFFFF,    true, vr_setup_select('dist2k4') );
+		
+		
+		vr_setup_createtext(vr_setup_cac1, 'CA Corr:',        center, right,  0,  +64,  #FFFFFF,    true, vr_setup_select('ca') );
+		vr_setup_createtext(vr_setup_cac2, get(i_ca),         center, left,   0,  +64,  #FFFFFF,    true, vr_setup_select('ca') );
+		vr_setup_createtext(vr_setup_vig1, 'Vignette:',       center, right,  0, +112,  #FFFFFF,    true, vr_setup_select('vignette') );
+		vr_setup_createtext(vr_setup_vig2, get(i_vig),        center, left,   0, +112,  #FFFFFF,    true, vr_setup_select('vignette') );
+		vr_setup_createtext(vr_setup_olp1, 'Overlap:',        center, right,  0, +160,  #FFFFFF,    true, vr_setup_select('overlap') );
+		vr_setup_createtext(vr_setup_olp2, get(i_overlap),    center, left,   0, +160,  #FFFFFF,    true, vr_setup_select('overlap') );
+				
+		vr_setup_createtext(vr_setup_m35, 'CLOSE',       center, center, 0, +225, #FFFFFF, true, set(background,true), set(background,false), vr_setup_close_sub_menus() );
+		
+		
+		<!-- create the calibration text elements -->
+		set(vr_setup_text_parent, 'vr_setup_m2');
+		vr_setup_createtext(vr_setup_cb1, 'GYROSCOPE', center, center, 0, -225, #FFFFFF, false);
+		vr_setup_createtext(vr_setup_cb2, 'Place the device on a flat and[br]stable surface and tab calibrate[br]to correct a gyroscope drifting.', center, center, 0, -95, #FFFFFF, false, vr_setup_select('screen') );
+		vr_setup_createtext(vr_setup_cb3, 'CALIBRATE',   center, center, 0,  +55, #FFFFFF, true, set(background,true), set(background,false), vr_setup_do_calibration() );
+		vr_setup_createtext(vr_setup_cb4, 'RESET',       center, center, 0, +125, #FFFFFF, true, set(background,true), set(background,false), webvr.resetcalibration() );
+		vr_setup_createtext(vr_setup_cb5, 'CLOSE',       center, center, 0, +225, #FFFFFF, true, set(background,true), set(background,false), vr_setup_close_sub_menus() );
+		
+		vr_setup_createtext(vr_setup_cb6, 'Calibrating...',      bottom, center, 0, 40, #FFFFFF, false, null );
+		vr_setup_createtext(vr_setup_cb7, 'Calibration okay.',   bottom, center, 0, 40, #FFFFFF, false, null );
+		vr_setup_createtext(vr_setup_cb8, 'Calibration failed!', bottom, center, 0, 40, #FFFFFF, false, null );
+		set(layer[vr_setup_cb6].autoalpha, true);
+		set(layer[vr_setup_cb7].autoalpha, true);
+		set(layer[vr_setup_cb8].autoalpha, true);
+		set(layer[vr_setup_cb6].alpha, 0.0);
+		set(layer[vr_setup_cb7].alpha, 0.0);
+		set(layer[vr_setup_cb8].alpha, 0.0);
+		
+		
+		<!-- pre-select the screen size for adjusting when it is unknown, otherwise the IPD -->
+		if(known_size == false,
+			vr_setup_select('screen', true);
+		  ,
+			vr_setup_select('ipd', true);
+		  );
+	</action>
+
+
+	<action name="vr_setup_createtext">
+		<!--
+			%1 = name
+			%2 = text
+			%3 = align
+			%4 = edge
+			%5 = x
+			%6 = y
+			%7 = color
+			%8 = enabled
+			%9 = ondown
+			%10 = onup
+			%11 = onclick
+		-->
+		addlayer(%1);
+		set(layer[%1].parent, get(vr_setup_text_parent));
+		set(layer[%1].url, '%SWFPATH%/plugins/textfield.swf');
+		set(layer[%1].css, calc('text-align:%3;color:%7;font-size:'+40*webvr_setup_scale+'px;font-weight:bold;'));
+		set(layer[%1].padding, calc(0 + ' ' + 8*webvr_setup_scale));
+		set(layer[%1].roundedge, calc(8*webvr_setup_scale));
+		set(layer[%1].background, false);
+		set(layer[%1].backgroundcolor, 0xFFFFFF);
+		set(layer[%1].backgroundalpha, 0.25);
+		set(layer[%1].align, %3);
+		set(layer[%1].edge, %4);
+		set(layer[%1].x, calc(%5 * webvr_setup_scale));
+		set(layer[%1].y, calc(%6 * webvr_setup_scale));
+		set(layer[%1].html, %2);
+		set(layer[%1].enabled, %8);
+		set(layer[%1].ondown, %9);
+		set(layer[%1].onup, %10);
+		set(layer[%1].onclick, %11);
+	</action>
+
+
+	<action name="vr_setup_createbutton">
+		vr_setup_createtext(%1,%2,%3,%4,%5,%6,%7,%8,%9);
+		set(layer[%1].css, calc('vertical-align:middle;text-align:center;color:%7;font-size:'+60*webvr_setup_scale+'px;font-weight:bold;'));
+		set(layer[%1].background, true);
+		set(layer[%1].padding, 0);
+		set(layer[%1].roundedge, calc(40 * webvr_setup_scale));
+		set(layer[%1].width, calc(70 * webvr_setup_scale));
+		set(layer[%1].height, calc(70 * webvr_setup_scale));
+		set(layer[%1].vcenter, true);
+	</action>
+
+
+	<action name="vr_setup_reset">
+		<!-- reset to the defaults -->
+		set(webvr.mobilevr_screensize, 'auto');
+		copy(i_screensize, webvr.devicesize);
+		if(i_screensize LE 0, set(i_screensize, 5.0); );
+		roundval(i_screensize, 1);
+		set(layer[vr_setup_dvn2].html, get(webvr.devicename));
+		txtadd(layer[vr_setup_siz2].html, get(i_screensize), ' inch');
+
+		set(webvr.mobilevr_ipd, 63.5);
+		copy(i_ipd, webvr.mobilevr_ipd);
+		roundval(i_ipd, 1);
+		txtadd(layer[vr_setup_ipd2].html, get(i_ipd), ' mm');
+
+		<!-- set fake custom lens settings and call 'next' headset to switch to the default 'Cardboard' settings -->
+		set(webvr.mobilevr_lens_fov, 100);
+		set(webvr.mobilevr_lens_dist, 0.5);
+		set(webvr.mobilevr_lens_dist2, '1|0|0|0');
+		set(webvr.mobilevr_lens_vign, 100);
+		set(webvr.mobilevr_lens_overlap, 1.0);
+		set(webvr.mobilevr_lens_ca, 0.0);
+		
+		if(webvr.isfake AND device.desktop,
+			<!-- select 'no distortion' headset for fake desktop usage -->
+			vr_setup_change_headset(-1);
+		  ,
+			<!-- select 'Cardboard A' headset for Mobile-VR usage -->
+			vr_setup_change_headset(+1);	
+		  );
+
+		vr_setup_select(get(selected_var));
+	</action>
+
+
+	<action name="vr_setup_close">
+		<!-- 2. parameter == true => remove children elements too -->
+		removelayer(vr_setup_bg, true);
+		
+		<!-- enable cursor -->
+		set(webvr.vr_cursor_enabled, true);
+	</action>
+
+
+	<action name="vr_setup_save">
+		webvr.saveSettings();
+		vr_setup_close();
+	</action>
+	
+	
+	<action name="vr_setup_customize_headset">
+		set(layer[vr_setup_bg].bgalpha, 0.1);
+		
+		set(layer[vr_setup_m1].visible,false);
+		set(layer[vr_setup_m2].visible,false);
+		set(layer[vr_setup_m3].visible,true);
+		
+		set(layer[vr_setup_hmd1].parent, vr_setup_m3);
+		set(layer[vr_setup_hmd2].parent, vr_setup_m3);
+		set(layer[vr_setup_btn1].parent, vr_setup_m3);
+		set(layer[vr_setup_btn2].parent, vr_setup_m3);
+		
+		set(layer[vr_setup_hmd1].y, calc(-145 * webvr_setup_scale));
+		set(layer[vr_setup_hmd2].y, calc(-145 * webvr_setup_scale));
+		
+		copy(old_selection, selected_var);
+		vr_setup_select('headset');
+	</action>
+	
+	
+
+	<action name="vr_setup_calibration">
+		set(layer[vr_setup_m1].visible,false);
+		set(layer[vr_setup_m2].visible,true);
+	</action>
+	
+	<action name="vr_setup_close_sub_menus">
+		set(layer[vr_setup_bg].bgalpha, 0.5);
+		
+		set(layer[vr_setup_m1].visible,true);
+		set(layer[vr_setup_m2].visible,false);
+		set(layer[vr_setup_m3].visible,false);
+		
+		set(layer[vr_setup_hmd1].parent, vr_setup_m1);
+		set(layer[vr_setup_hmd2].parent, vr_setup_m1);
+		set(layer[vr_setup_btn1].parent, vr_setup_m1);
+		set(layer[vr_setup_btn2].parent, vr_setup_m1);
+		
+		set(layer[vr_setup_hmd1].y, calc(+35 * webvr_setup_scale));
+		set(layer[vr_setup_hmd2].y, calc(+35 * webvr_setup_scale));
+		
+		if(old_selection,
+			vr_setup_select(get(old_selection));
+			delete(old_selection);
+		  );
+	</action>
+	
+	<action name="vr_setup_do_calibration">
+		if(!webvr.isfake,
+			tween(layer[vr_setup_cb6].alpha, 1.0, 0.1);
+			tween(layer[vr_setup_cb7].alpha, 0.0, 0.1);
+			tween(layer[vr_setup_cb8].alpha, 0.0, 0.1);
+			webvr.calibrate(
+				tween(layer[vr_setup_cb6].alpha, 0.0, 0.1);
+				tween(layer[vr_setup_cb7].alpha, 1.0, 0.1);
+				delayedcall(2.0, tween(layer[vr_setup_cb7].alpha, 0.0, 0.25) );
+			  ,
+				tween(layer[vr_setup_cb6].alpha, 0.0, 0.1);
+				tween(layer[vr_setup_cb8].alpha, 1.0, 0.1);
+				delayedcall(2.0, tween(layer[vr_setup_cb8].alpha, 0.0, 0.25) );
+			  );
+		  );
+	</action>
+
+	<action name="vr_setup_update_dist2">
+		txtadd(webvr.mobilevr_lens_dist2, get(i_dist2_k1), '|', calc(i_dist2_k2/10.0), '|', calc(i_dist2_k3/10.0), '|', calc(i_dist2_k4/10.0));
+		vr_setup_change_headset(0);
+	</action>
+
+	<action name="vr_setup_select">
+		<!-- select a setting for adjusting -->
+		set(layer[vr_setup_siz2].background, false);
+		set(layer[vr_setup_ipd2].background, false);
+		set(layer[vr_setup_hmd2].background, false);
+		set(layer[vr_setup_fov2].background, false);
+		set(layer[vr_setup_dst2].background, false);
+		set(layer[vr_setup_d2k1].background, false);
+		set(layer[vr_setup_d2k2].background, false);
+		set(layer[vr_setup_d2k3].background, false);
+		set(layer[vr_setup_d2k4].background, false);
+		set(layer[vr_setup_vig2].background, false);
+		set(layer[vr_setup_cac2].background, false);
+		set(layer[vr_setup_olp2].background, false);
+
+		set(selected_setting, null);
+		delete(selected_var_value);
+
+		set(layer[vr_setup_btn1].ondown, vr_setup_change_ondown(-1) );
+		set(layer[vr_setup_btn2].ondown, vr_setup_change_ondown(+1) );
+		set(selected_var_callback, null);
+
+		set(selected_var, %1);
+
+		if(selected_var == 'screen',
+			set(selected_setting,      vr_setup_siz2);
+			set(selected_var_name,     'webvr.mobilevr_screensize');
+			set(selected_var_postfix,  ' inch');
+			copy(selected_var_value,   get(selected_var_name));
+			if(selected_var_value == 'auto', copy(selected_var_value, webvr.devicesize));
+			if(selected_var_value LE 0, set(selected_var_value, 5.0));
+			set(selected_var_step,     0.1);
+			set(selected_var_min,      4);
+			set(selected_var_max,      10);
+			set(selected_var_round,    1);
+			set(selected_var_callback, vr_setup_change_screen() );
+		  );
+
+		if(selected_var == 'ipd',
+			set(selected_setting,      vr_setup_ipd2);
+			set(selected_var_name,     'webvr.mobilevr_ipd');
+			set(selected_var_postfix,  ' mm');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.1);
+			set(selected_var_min,      40);
+			set(selected_var_max,      80);
+			set(selected_var_round,    1);
+		  );
+
+		if(selected_var == 'headset',
+			set(selected_setting,      vr_setup_hmd2);
+			set(layer[vr_setup_btn1].ondown, vr_setup_change_headset(-1) );
+			set(layer[vr_setup_btn2].ondown, vr_setup_change_headset(+1) );
+		  );
+
+		if(selected_var == 'fov',
+			set(selected_setting,      vr_setup_fov2);
+			set(selected_var_name,     'webvr.mobilevr_lens_fov');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.1);
+			set(selected_var_min,      40);
+			set(selected_var_max,      179);
+			set(selected_var_round,    1);
+			set(selected_var_callback, vr_setup_change_headset(0) );
+		  );
+
+		if(selected_var == 'dist',
+			set(selected_setting,      vr_setup_dst2);
+			set(selected_var_name,     'webvr.mobilevr_lens_dist');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      0);
+			set(selected_var_max,      5);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_change_headset(0) );
+		  );
+		
+		if(selected_var == 'dist2k1',
+			set(selected_setting,      vr_setup_d2k1);
+			set(selected_var_name,     'i_dist2_k1');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      -9);
+			set(selected_var_max,      +9);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_update_dist2() );
+		  );
+		
+		if(selected_var == 'dist2k2',
+			set(selected_setting,      vr_setup_d2k2);
+			set(selected_var_name,     'i_dist2_k2');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      -9);
+			set(selected_var_max,      +9);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_update_dist2() );
+		  );
+		
+		if(selected_var == 'dist2k3',
+			set(selected_setting,      vr_setup_d2k3);
+			set(selected_var_name,     'i_dist2_k3');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      -9);
+			set(selected_var_max,      +9);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_update_dist2() );
+		  );
+
+		if(selected_var == 'dist2k4',
+			set(selected_setting,      vr_setup_d2k4);
+			set(selected_var_name,     'i_dist2_k4');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      -9);
+			set(selected_var_max,      +9);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_update_dist2() );
+		  );
+
+		if(selected_var == 'vignette',
+			set(selected_setting,      vr_setup_vig2);
+			set(selected_var_name,     'webvr.mobilevr_lens_vign');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     1);
+			set(selected_var_min,      10);
+			set(selected_var_max,      200);
+			set(selected_var_round,    0);
+			set(selected_var_callback, vr_setup_change_headset(0) );
+		  );
+
+		if(selected_var == 'ca',
+			set(selected_setting,      vr_setup_cac2);
+			set(selected_var_name,     'webvr.mobilevr_lens_ca');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      -1.0);
+			set(selected_var_max,      +1.0);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_change_headset(0) );
+		  );
+		  
+		if(selected_var == 'overlap',
+			set(selected_setting,      vr_setup_olp2);
+			set(selected_var_name,     'webvr.mobilevr_lens_overlap');
+			set(selected_var_postfix,  '');
+			copy(selected_var_value,   get(selected_var_name));
+			set(selected_var_step,     0.01);
+			set(selected_var_min,      0.5);
+			set(selected_var_max,      2.0);
+			set(selected_var_round,    2);
+			set(selected_var_callback, vr_setup_change_headset(0) );
+		  );
+
+		if(selected_setting != null,
+			set(layer[get(selected_setting)].background, true);
+			if(%2 == true,
+				set(layer[vr_setup_btn1].y, get(layer[get(selected_setting)].y));
+				set(layer[vr_setup_btn2].y, get(layer[get(selected_setting)].y));
+			  ,
+				tween(layer[vr_setup_btn1].y, get(layer[get(selected_setting)].y));
+				tween(layer[vr_setup_btn2].y, get(layer[get(selected_setting)].y));
+			  );
+		  );
+	</action>
+
+
+	<action name="vr_setup_change_screen">
+		set(layer[vr_setup_dvn2].html, 'Custom');
+		set(layer[vr_setup_dvn2].css, calc('color:#FFFFFF;font-size:'+40*webvr_setup_scale+'px;font-weight:bold;'));
+		set(layer[vr_setup_siz2].css, calc('color:#FFFFFF;font-size:'+40*webvr_setup_scale+'px;font-weight:bold;'));
+	</action>
+
+
+	<action name="vr_setup_change_ondown">
+		copy(t0,timertick);
+		set(t1,0);
+		asyncloop(pressed,
+			copy(t2,timertick);
+			sub(dt,t2,t1);
+			if(dt GT 100,
+				copy(t1,t2);
+				sub(dt,t1,t0);
+				div(dt,1000);
+				Math.max(dt,1);
+				mul(dt,%1);
+				vr_setup_adjust(get(dt));
+			  );
+		  );
+	</action>
+
+
+	<action name="vr_setup_adjust">
+		if(selected_setting != null,
+			mul(change, selected_var_step, %1);
+			add(selected_var_value, change);
+			Math.max(selected_var_value, selected_var_min);
+			Math.min(selected_var_value, selected_var_max);
+			roundval(selected_var_value, get(selected_var_round));
+			tween(get(selected_var_name), get(selected_var_value), 0.1);
+			txtadd(layer[get(selected_setting)].html, get(selected_var_value), get(selected_var_postfix));
+			if(selected_var_callback != null, selected_var_callback());
+		  );
+	</action>
+
+
+	<action name="vr_setup_change_headset">
+		set(i_headset, 'Custom');
+		if(%1 != 0,
+			copy(i_fov, webvr.mobilevr_lens_fov);
+			roundval(i_fov, 1);
+			copy(i_dist, webvr.mobilevr_lens_dist);
+			roundval(i_dist, 2);
+			copy(i_dist2, webvr.mobilevr_lens_dist2);
+			copy(i_vig, webvr.mobilevr_lens_vign);
+			roundval(i_vig, 0);
+			copy(i_ca, webvr.mobilevr_lens_ca);
+			roundval(i_ca, 3);
+			copy(i_overlap, webvr.mobilevr_lens_overlap);
+			roundval(i_overlap, 2);
+			set(i_hsindex, -1);
+			copy(i_hscount, vrheadsets.headset.count);
+			for(set(i,0), i LT i_hscount, inc(i),
+				copy(hs, vrheadsets.headset[get(i)]);
+				if(i_overlap == hs.overlap AND i_fov == hs.fov AND i_dist == hs.dist AND i_dist2 == hs.dist2 AND i_ca == hs.ca AND i_vig == hs.vig , copy(i_hsindex, i); copy(i_headset, hs.caption); );
+			   );
+
+			if(%1 GT 0,
+				<!-- loop right -->
+				add(i_hsindex, 1);
+				if(i_hsindex GE i_hscount, set(i_hsindex,0));
+			  ,
+				<!-- loop left -->
+				sub(i_hsindex, 1);
+				if(i_hsindex LT 0, sub(i_hsindex,i_hscount,1));
+			  );
+
+			copy(hs, vrheadsets.headset[get(i_hsindex)]);
+			copy(i_headset, hs.caption);
+			copy(i_overlap, hs.overlap);
+			copy(i_fov,     hs.fov);
+			copy(i_dist,    hs.dist);
+			copy(i_dist2,   hs.dist2);
+			copy(i_ca,      hs.ca);
+			copy(i_vig,     hs.vig);
+		  );
+
+		copy(layer[vr_setup_hmd2].html, i_headset);
+		if(%1 != 0,
+			copy(webvr.mobilevr_lens_overlap, i_overlap);
+			copy(webvr.mobilevr_lens_fov, i_fov);
+			copy(webvr.mobilevr_lens_dist, i_dist);
+			copy(webvr.mobilevr_lens_dist2, i_dist2);
+			copy(webvr.mobilevr_lens_ca, i_ca);
+			copy(webvr.mobilevr_lens_vign, i_vig);
+			copy(layer[vr_setup_olp2].html, i_overlap);
+			copy(layer[vr_setup_fov2].html, i_fov);
+			copy(layer[vr_setup_dst2].html, i_dist);
+			
+			txtsplit(i_dist2, '|', i_dist2_k1, i_dist2_k2, i_dist2_k3, i_dist2_k4);
+			mul(i_dist2_k1,1);
+			mul(i_dist2_k2,10);
+			mul(i_dist2_k3,10);
+			mul(i_dist2_k4,10);
+			roundval(i_dist2_k1,2);
+			roundval(i_dist2_k2,2);
+			roundval(i_dist2_k3,2);
+			roundval(i_dist2_k4,2);
+			copy(layer[vr_setup_d2k1].html, i_dist2_k1);
+			copy(layer[vr_setup_d2k2].html, i_dist2_k2);
+			copy(layer[vr_setup_d2k3].html, i_dist2_k3);
+			copy(layer[vr_setup_d2k4].html, i_dist2_k4);
+			
+			copy(layer[vr_setup_cac2].html, i_ca);
+			copy(layer[vr_setup_vig2].html, i_vig);
+		  );
+	</action>
+
+</krpano>

+ 85 - 0
src/App.tsx

@@ -0,0 +1,85 @@
+import '@/assets/styles/base.css'
+// 关于路由
+import React, { useCallback, useEffect } from 'react'
+import { HashRouter, Routes, Route } from 'react-router-dom'
+import { isMobileFu } from './utils/history'
+import SpinLoding from './components/SpinLoding'
+import AsyncSpinLoding from './components/AsyncSpinLoding'
+
+import UpAsyncLoding from './components/UpAsyncLoding'
+import MessageCom from './components/Message'
+
+import screenImg from '@/assets/img/landtip.png'
+
+const A1home = React.lazy(() => import('./pages/A1home'))
+const A2layout = React.lazy(() => import('./pages/A2layout'))
+const A3Pano = React.lazy(() => import('./pages/A3Pano'))
+const A4Scene = React.lazy(() => import('./pages/A4Scene'))
+
+export default function App() {
+  // 从仓库中获取查看图片的信息
+  // const lookBigImg = useSelector((state: RootState) => state.A0Layout.lookBigImg)
+
+  const rootDomFu = useCallback(() => {
+    const rootDom: HTMLDivElement = document.querySelector('#root')!
+    if (rootDom) {
+      rootDom.style.height = window.innerHeight + 'px'
+    }
+  }, [])
+
+  useEffect(() => {
+    window.addEventListener('resize', rootDomFu, true)
+    rootDomFu()
+  }, [rootDomFu])
+
+  return (
+    <>
+      {/* 关于路由 */}
+      <HashRouter>
+        <React.Suspense fallback={<SpinLoding />}>
+          <Routes>
+            <Route path='/' element={<A1home />} />
+            <Route path='/layout' element={<A2layout />} />
+            <Route path='/pano' element={<A3Pano />} />
+            <Route path='/scene' element={<A4Scene />} />
+          </Routes>
+        </React.Suspense>
+      </HashRouter>
+
+      {/* 发送请求的加载组件 */}
+      <AsyncSpinLoding />
+
+      {/* 所有图片点击预览查看大图 */}
+      {/* <Image
+        preview={{
+          visible: lookBigImg.show,
+          src: lookBigImg.url,
+          onVisibleChange: value => {
+            // 清除仓库信息
+            store.dispatch({
+              type: 'layout/lookBigImg',
+              payload: { url: '', show: false }
+            })
+          }
+        }}
+      /> */}
+
+      {/* 上传附件的进度条元素 */}
+      <UpAsyncLoding />
+
+      {/* 查看视频音频 */}
+      {/* <LookDom /> */}
+
+      {/* antd 轻提示 ---兼容360浏览器 */}
+      <MessageCom />
+
+      {/* 横屏提示 */}
+      {isMobileFu() ? (
+        <div id='ScreenChange'>
+          <img src={screenImg} alt='' />
+          <p>请在竖屏模式浏览</p>
+        </div>
+      ) : null}
+    </>
+  )
+}

BIN
src/assets/img/IMGerror.png


BIN
src/assets/img/back.png


BIN
src/assets/img/btn_bg.png


BIN
src/assets/img/close.png


BIN
src/assets/img/detailBackBtn.png


BIN
src/assets/img/dot.png


BIN
src/assets/img/dot_bg.png


BIN
src/assets/img/foldBtn.png


BIN
src/assets/img/full.png


BIN
src/assets/img/fullx.png


BIN
src/assets/img/home_bg.png


BIN
src/assets/img/home_bg_M.png


BIN
src/assets/img/intro_bg.png


BIN
src/assets/img/intro_bg_M.png


BIN
src/assets/img/landtip.png


BIN
src/assets/img/loading.gif


BIN
src/assets/img/logo.png


BIN
src/assets/img/logo_M.png


BIN
src/assets/img/lookBtnBg.png


BIN
src/assets/img/lookBtnBg_ac.png


BIN
src/assets/img/lookItem_bg1.png


BIN
src/assets/img/lookItem_bg2.png


BIN
src/assets/img/lookItem_bg3.png


BIN
src/assets/img/look_bg.png


BIN
src/assets/img/look_bg_M.png


BIN
src/assets/img/look_title.png


BIN
src/assets/img/panoBottom_bg.png


BIN
src/assets/img/tabBtnBg.png


BIN
src/assets/img/tabBtnBg_ac.png


BIN
src/assets/img/title.png


BIN
src/assets/img/title_M.png


BIN
src/assets/img/unfoldBtn.png


+ 222 - 0
src/assets/styles/base.css

@@ -0,0 +1,222 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+html {
+  height: 100%;
+  font-size: 14px;
+  user-select: none;
+}
+body {
+  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
+  height: 100%;
+  color: black;
+}
+i {
+  font-style: normal;
+}
+img {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: middle;
+}
+ul {
+  list-style: none;
+}
+body {
+  overflow: hidden;
+}
+/* 文本域取消下拉 */
+textarea {
+  resize: none !important;
+  min-height: 100px !important;
+}
+/* 主题色 */
+:root {
+  --themeColor: #042b8f;
+  --themeColor2: #69c690;
+}
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity 0.5s;
+}
+/* 兼容360浏览器的下拉框 */
+.ant-select-selector {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #d9d9d9;
+  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-popconfirm {
+  width: 240px;
+}
+#root {
+  margin: 0 auto;
+  width: 100vw;
+  height: 100vh;
+  overflow: auto;
+  overflow-y: overlay;
+  /* 普通文字按钮的颜色 */
+  /* 按钮的危险颜色 */
+  /* antd分页器样式 */
+  /* 表格的图片居中 */
+  /* antd图片预览组件 */
+  /* antd表格居中 */
+}
+#root > div {
+  width: 100%;
+  height: 100%;
+}
+#root a {
+  text-decoration: none;
+  color: black;
+  outline: none;
+}
+#root .iconHoverTit {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+#root .iconHoverTitTxt {
+  background-color: var(--themeColor);
+  color: #fff;
+  width: 16px;
+  height: 16px;
+  line-height: 16px;
+  text-align: center;
+  font-size: 12px;
+  border-radius: 50%;
+}
+#root .ant-btn-text {
+  color: var(--themeColor);
+}
+#root .ant-btn-text:disabled {
+  cursor: not-allowed;
+  color: rgba(0, 0, 0, 0.25);
+}
+#root .ant-btn-text.ant-btn-dangerous {
+  color: #ff4d4d;
+}
+#root .tableImgAuto {
+  display: flex;
+  justify-content: center;
+}
+#root .ant-image {
+  display: none;
+}
+#root .ant-table-cell {
+  text-align: center !important;
+}
+#root #A2Table3 .ant-table-row-expand-icon {
+  background-color: var(--themeColor);
+  color: #fff;
+}
+#root #A2Table3 .ant-table-cell-with-append {
+  display: flex;
+  justify-content: flex-start;
+}
+[hidden] {
+  display: none !important;
+}
+#upInput {
+  display: none;
+}
+#upInput2 {
+  display: none;
+}
+#upInputAudio {
+  display: none;
+}
+.pageTitle {
+  font-size: 18px;
+  font-weight: 700;
+  position: absolute;
+  z-index: 11;
+  top: -56px;
+  left: -18px;
+  padding-left: 40px;
+}
+.pageTitle::before {
+  position: absolute;
+  left: 20px;
+  top: 50%;
+  transform: translateY(-50%);
+  content: '';
+  width: 6px;
+  height: 20px;
+  background-color: var(--themeColor);
+}
+.mySorrl::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 5px;
+  /*高宽分别对应横竖滚动条的尺寸*/
+  height: 1px;
+}
+.mySorrl::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 10px;
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  background: var(--themeColor);
+}
+.mySorrl::-webkit-scrollbar-track {
+  /*滚动条里面轨道*/
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  border-radius: 10px;
+  background: transparent;
+}
+.ant-image-preview-operations {
+  background-color: rgba(0, 0, 0, 0.8) !important;
+}
+.ant-image-preview-mask {
+  z-index: 9999 !important;
+}
+.ant-image-preview-wrap {
+  z-index: 9999 !important;
+}
+.ant-image-preview-operations-wrapper {
+  z-index: 9999 !important;
+}
+.ant-notification-notice {
+  max-height: 500px !important;
+  overflow-y: auto !important;
+}
+.ant-picker-dropdown {
+  text-align: center;
+}
+.ant-picker-selection-item-remove {
+  display: none !important;
+}
+#ScreenChange {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 10000;
+  background-color: rgba(0, 0, 0, 0.8);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  opacity: 0;
+  pointer-events: none;
+  transition: all 0.5s;
+}
+#ScreenChange > img {
+  width: 200px;
+}
+#ScreenChange > p {
+  margin-top: 20px;
+  color: #fff;
+  font-size: 18px;
+  height: 40px;
+}
+/*横屏*/
+@media screen and (orientation: landscape) {
+  #ScreenChange {
+    opacity: 1;
+    pointer-events: auto;
+  }
+}

+ 341 - 0
src/assets/styles/base.less

@@ -0,0 +1,341 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html {
+  height: 100%;
+  font-size: 14px;
+  user-select: none;
+}
+
+body {
+  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB',
+    'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
+  height: 100%;
+  color: black;
+}
+
+i {
+  font-style: normal;
+}
+
+img {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: middle;
+  // object-fit: cover;
+}
+
+ul {
+  list-style: none;
+}
+
+body {
+  overflow: hidden;
+}
+
+/* 文本域取消下拉 */
+textarea {
+  resize: none !important;
+  min-height: 100px !important;
+}
+
+/* 主题色 */
+:root {
+  // --themeColor: #69c790;
+  --themeColor: #042b8f;
+  --themeColor2: #69c690;
+}
+
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity 0.5s;
+}
+
+/* 兼容360浏览器的下拉框 */
+.ant-select-selector {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #d9d9d9;
+  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+// 气泡框闪烁问题
+.ant-popconfirm {
+  width: 240px;
+}
+
+// 重置antd样式
+#root {
+  // max-width: 500px;
+  margin: 0 auto;
+  width: 100vw;
+  height: 100vh;
+  overflow: auto;
+  overflow-y: overlay;
+
+  & > div {
+    width: 100%;
+    height: 100%;
+  }
+
+  a {
+    text-decoration: none;
+    color: black;
+    outline: none;
+  }
+
+  // ?的提示
+  .iconHoverTit {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .iconHoverTitTxt {
+    background-color: var(--themeColor);
+    color: #fff;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+    font-size: 12px;
+    border-radius: 50%;
+  }
+
+  // a {
+  //   color: var(--themeColor);
+  // }
+
+  /* 普通文字按钮的颜色 */
+  .ant-btn-text {
+    color: var(--themeColor);
+  }
+
+  .ant-btn-text:disabled {
+    cursor: not-allowed;
+    color: rgba(0, 0, 0, 0.25);
+  }
+
+  /* 按钮的危险颜色 */
+  .ant-btn-text.ant-btn-dangerous {
+    color: #ff4d4d;
+  }
+
+  /* antd分页器样式 */
+  // .ant-pagination .ant-pagination-item {
+  //   border-radius: 50%;
+  //   border: 1px solid #999;
+  //   background-color: transparent !important;
+  // }
+
+  // .ant-pagination .ant-pagination-item-active {
+  //   background-color: var(--themeColor) !important;
+  // }
+
+  // .ant-pagination .ant-pagination-item-active a {
+  //   color: #fff !important;
+  // }
+
+  // .ant-pagination .ant-pagination-item:hover {
+  //   background-color: var(--themeColor) !important;
+  // }
+
+  // .ant-pagination .ant-pagination-item:hover a {
+  //   color: #fff !important;
+  // }
+
+  // .ant-pagination-prev {
+  //   border-radius: 50% !important;
+  //   border: 1px solid #999;
+  // }
+
+  // .ant-pagination-prev:hover {
+  //   background-color: var(--themeColor);
+  // }
+
+  // .ant-pagination-prev:hover button {
+  //   color: #fff;
+  // }
+
+  // .ant-pagination-next {
+  //   border-radius: 50% !important;
+  //   border: 1px solid #999;
+  // }
+
+  // .ant-pagination-next:hover {
+  //   background-color: var(--themeColor);
+  // }
+
+  // .ant-pagination-next:hover button {
+  //   color: #fff;
+  // }
+
+  // .ant-pagination-disabled {
+  //   border: 1px solid #ccc;
+  // }
+
+  // .ant-pagination-disabled:hover {
+  //   background-color: transparent;
+  // }
+
+  /* 表格的图片居中 */
+  .tableImgAuto {
+    display: flex;
+    justify-content: center;
+  }
+
+  /* antd图片预览组件 */
+  .ant-image {
+    display: none;
+  }
+
+  /* antd表格居中 */
+
+  .ant-table-cell {
+    text-align: center !important;
+  }
+
+  // 树型 表格 定制化
+  #A2Table3 {
+    .ant-table-row-expand-icon {
+      background-color: var(--themeColor);
+      color: #fff;
+    }
+
+    .ant-table-cell-with-append {
+      display: flex;
+      justify-content: flex-start;
+    }
+  }
+}
+
+[hidden] {
+  display: none !important;
+}
+
+#upInput {
+  display: none;
+}
+
+#upInput2 {
+  display: none;
+}
+
+#upInputAudio {
+  display: none;
+}
+
+// 页面标题
+.pageTitle {
+  font-size: 18px;
+  font-weight: 700;
+  position: absolute;
+  z-index: 11;
+  top: -56px;
+  left: -18px;
+  padding-left: 40px;
+
+  &::before {
+    position: absolute;
+    left: 20px;
+    top: 50%;
+    transform: translateY(-50%);
+    content: '';
+    width: 6px;
+    height: 20px;
+    background-color: var(--themeColor);
+  }
+}
+
+// 滚动条
+.mySorrl::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 5px;
+  /*高宽分别对应横竖滚动条的尺寸*/
+  height: 1px;
+}
+
+.mySorrl::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 10px;
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  background: var(--themeColor);
+}
+
+.mySorrl::-webkit-scrollbar-track {
+  /*滚动条里面轨道*/
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  border-radius: 10px;
+  background: transparent;
+}
+.ant-image-preview-operations {
+  background-color: rgba(0, 0, 0, 0.8) !important;
+}
+.ant-image-preview-mask {
+  z-index: 9999 !important;
+}
+.ant-image-preview-wrap {
+  z-index: 9999 !important;
+}
+.ant-image-preview-operations-wrapper {
+  z-index: 9999 !important;
+}
+
+.ant-notification-notice {
+  max-height: 500px !important;
+  overflow-y: auto !important;
+}
+
+// 多日期选择器居中 挡住 按钮(即必须失焦才能点击确定)
+.ant-picker-dropdown {
+  text-align: center;
+}
+.ant-picker-selection-item-remove {
+  display: none !important;
+}
+// .ant-picker-year-btn,
+// .ant-picker-header-super-prev-btn,
+// .ant-picker-header-super-next-btn {
+//   display: none;
+// }
+
+// 横屏 竖屏的切换
+#ScreenChange {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 10000;
+  background-color: rgba(0, 0, 0, 0.8);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  opacity: 0;
+  pointer-events: none;
+  transition: all 0.5s;
+
+  & > img {
+    width: 200px;
+  }
+
+  & > p {
+    margin-top: 20px;
+    color: #fff;
+    font-size: 18px;
+    height: 40px;
+  }
+}
+
+/*横屏*/
+@media screen and (orientation: landscape) {
+  #ScreenChange {
+    opacity: 1;
+    pointer-events: auto;
+  }
+}

+ 21 - 0
src/components/AsyncSpinLoding/index.module.scss

@@ -0,0 +1,21 @@
+.AsyncSpinLoding {
+  opacity: 0;
+  pointer-events: none;
+  transition: all .5s;
+  position: fixed;
+  z-index: 9998;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  // background-color: rgba(0, 0, 0, .6);
+  background-color: transparent;
+  :global{
+    .ant-spin-spinning{
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%,-50%);
+    }
+  }
+}

+ 15 - 0
src/components/AsyncSpinLoding/index.tsx

@@ -0,0 +1,15 @@
+import styles from "./index.module.scss";
+import { Spin } from "antd";
+import React from "react";
+
+function AsyncSpinLoding() {
+  return (
+    <div id="AsyncSpinLoding" className={styles.AsyncSpinLoding}>
+      <Spin size="large" />
+    </div>
+  );
+}
+
+const MemoAsyncSpinLoding = React.memo(AsyncSpinLoding);
+
+export default MemoAsyncSpinLoding;

+ 21 - 0
src/components/AuthRoute/index.tsx

@@ -0,0 +1,21 @@
+import { hasToken } from "@//utils/storage";
+import { MessageFu } from "@/utils/message";
+import React from "react";
+import { Navigate, useLocation } from "react-router-dom";  // v6 新增 API
+
+// 定义保护路由组件,接收需要保护的子组件作为 children
+type AuthRouteProps = {
+  children: React.ReactNode;
+};
+
+export default function AuthRoute({ children }: AuthRouteProps) {
+  const location = useLocation();  // 获取当前路由位置,用于重定向后返回原页面
+
+  if (hasToken()) {
+    return <>{children}</>;  // 已登录:渲染子组件
+  } else {
+    MessageFu.warning("登录失效!");
+    // 未登录:重定向到登录页,并记录当前位置(登录后可返回)
+    return <Navigate to="/login" state={{ from: location }} replace />;
+  }
+}

+ 51 - 0
src/components/ImageLazy/index.module.scss

@@ -0,0 +1,51 @@
+.ImageLazy {
+  position: relative;
+
+  :global {
+    .lazyBox {
+      width: 100%;
+      height: 100%;
+      position: relative;
+
+      .adm-image {
+        width: 100%;
+        height: 100%;
+
+        img {
+          width: 100%;
+          height: 100%;
+        }
+      }
+
+      .lookImg {
+        cursor: pointer;
+        transition: opacity .3s;
+        opacity: 0;
+        pointer-events: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 18px;
+        color: #fff;
+        background-color: rgba(0, 0, 0, .6);
+
+        &>div {
+          font-size: 14px;
+        }
+      }
+
+      &:hover {
+        .lookImg {
+          opacity: 1;
+          pointer-events: auto;
+        }
+      }
+    }
+  }
+
+}

+ 63 - 0
src/components/ImageLazy/index.tsx

@@ -0,0 +1,63 @@
+import React, { useCallback, useState } from 'react'
+import styles from './index.module.scss'
+import { baseURL } from '@/utils/http'
+import imgLoding from '@/assets/img/loading.gif'
+import imgErr from '@/assets/img/IMGerror.png'
+import { EyeOutlined } from '@ant-design/icons'
+import store from '@/store'
+import { Image } from 'antd-mobile'
+
+type Props = {
+  width?: number | string
+  height?: number | string
+  src: string
+  srcBig?: string
+  noLook?: boolean
+  offline?: boolean
+}
+
+function ImageLazy({ width = 100, height = 100, src, srcBig, noLook, offline = false }: Props) {
+  // 默认不能预览图片,加载成功之后能预览
+  const [lookImg, setLookImg] = useState(false)
+
+  // 图片加载完成
+  const onLoad = useCallback(() => {
+    setLookImg(true)
+  }, [])
+
+  // 点击预览图片
+  const lookBigImg = useCallback(() => {
+    store.dispatch({
+      type: 'layout/lookBigImg',
+      payload: { url: offline ? src : srcBig ? baseURL + srcBig : baseURL + src, show: true }
+    })
+  }, [offline, src, srcBig])
+
+  return (
+    <div className={styles.ImageLazy} style={{ width: width, height: height }}>
+      <div className='lazyBox'>
+        <Image
+          lazy
+          onLoad={onLoad}
+          src={src ? (offline ? src : baseURL + src) : ''}
+          placeholder={<img src={imgLoding} alt='' />}
+          fallback={<img src={imgErr} alt='' />}
+          fit='cover'
+        />
+
+        {/* 图片预览 */}
+        {noLook || !lookImg ? null : (
+          <div className='lookImg' onClick={lookBigImg}>
+            <EyeOutlined rev={undefined} />
+            &nbsp;
+            <div>预览</div>
+          </div>
+        )}
+      </div>
+    </div>
+  )
+}
+
+const MemoImageLazy = React.memo(ImageLazy)
+
+export default MemoImageLazy

+ 65 - 0
src/components/LookDom/index.module.scss

@@ -0,0 +1,65 @@
+.LookDom {
+  transition: opacity .3s;
+  position: fixed;
+  z-index: 9991;
+  opacity: 0;
+  pointer-events: none;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, .6);
+
+  :global {
+    .close {
+      color: #fff;
+      position: absolute;
+      right: 70px;
+      top: 70px;
+      font-size: 30px;
+      cursor: pointer;
+      z-index: 10;
+    }
+
+    .viedoBox {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 800px;
+      height: 500px;
+
+      video {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .audioBox {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 500px;
+      height: 60px;
+
+      audio {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .modelBox {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+
+      iframe {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+}

+ 52 - 0
src/components/LookDom/index.tsx

@@ -0,0 +1,52 @@
+/* eslint-disable jsx-a11y/iframe-has-title */
+import React from "react";
+import { CloseCircleOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+import { useSelector } from "react-redux";
+import store, { RootState } from "@/store";
+import { baseURL } from "@/utils/http";
+function LookDom() {
+  const { src, type, flag } = useSelector(
+    (state: RootState) => state.A0Layout.lookDom
+  );
+  return (
+    <div
+      className={styles.LookDom}
+      style={src ? { opacity: 1, pointerEvents: "auto" } : {}}
+    >
+      {src ? (
+        <>
+          {type === "video" ? (
+            <div className="viedoBox">
+              <video autoPlay controls src={flag ? src : baseURL + src}></video>
+            </div>
+          ) : type === "audio" ? (
+            <div className="audioBox">
+              <audio autoPlay controls src={flag ? src : baseURL + src}></audio>
+            </div>
+          ) : (
+            <div className="modelBox">
+              <iframe src={`model.html?m=${src}`}></iframe>
+            </div>
+          )}
+
+          <div
+            className="close"
+            onClick={() =>
+              store.dispatch({
+                type: "layout/lookDom",
+                payload: { src: "", type: "", flag: false },
+              })
+            }
+          >
+            <CloseCircleOutlined rev={undefined} />
+          </div>
+        </>
+      ) : null}
+    </div>
+  );
+}
+
+const MemoLookDom = React.memo(LookDom);
+
+export default MemoLookDom;

+ 29 - 0
src/components/Message/index.tsx

@@ -0,0 +1,29 @@
+import React, { useEffect } from "react";
+import { message } from "antd";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store";
+
+function MessageCom() {
+  // 从仓库中获取 antd 轻提示信息
+  const messageReducerInfo = useSelector(
+    (state: RootState) => state.A0Layout.message
+  );
+
+  const [messageApi, contextHolder] = message.useMessage();
+
+  useEffect(() => {
+    if (messageReducerInfo.txt) {
+      messageApi.open({
+        type: messageReducerInfo.type,
+        content: messageReducerInfo.txt,
+        duration: messageReducerInfo.duration,
+      });
+    }
+  }, [messageApi, messageReducerInfo]);
+
+  return <>{contextHolder}</>;
+}
+
+const MemoMessage = React.memo(MessageCom);
+
+export default MemoMessage;

+ 48 - 0
src/components/MyPopconfirm.tsx

@@ -0,0 +1,48 @@
+import React, { useMemo } from 'react'
+import { Button, Popconfirm } from 'antd'
+
+type Props = {
+  txtK: '删除' | '取消' | '重置密码' | '退出登录' | '清空' | '取消认证'
+  onConfirm: () => void
+  Dom?: React.ReactNode
+  loc?: 'bottom'
+}
+
+function MyPopconfirm({ txtK, onConfirm, Dom, loc }: Props) {
+  const txt = useMemo(() => {
+    const obj = {
+      删除: ['删除后无法恢复,是否删除?', '删除'],
+      取消: ['放弃编辑后,信息将不会保存!', '放弃'],
+      重置密码: ['密码重制后为123456,是否重置?', '重置'],
+      退出登录: ['确定退出吗?', '确定'],
+      清空: ['确定清空吗?', '确定'],
+      取消认证: ['是否移除该用户的团队认证资格及信息?', '移除']
+    }
+    return Reflect.get(obj, txtK) || ['', '']
+  }, [txtK])
+
+  return (
+    <Popconfirm
+      placement={loc}
+      title={txt[0]}
+      okText={txt[1]}
+      cancelText='取消'
+      onConfirm={onConfirm}
+      okButtonProps={{ loading: false }}
+    >
+      {Dom ? (
+        Dom
+      ) : txtK === '删除' ? (
+        <Button size='small' type='text' danger>
+          {txtK}
+        </Button>
+      ) : (
+        <Button>{txtK}</Button>
+      )}
+    </Popconfirm>
+  )
+}
+
+const MemoMyPopconfirm = React.memo(MyPopconfirm)
+
+export default MemoMyPopconfirm

+ 21 - 0
src/components/MyTable/index.module.scss

@@ -0,0 +1,21 @@
+.MyTable {
+  :global {
+    .ant-table-body {
+      overflow-y: auto !important;
+      overflow-y: overlay !important;
+
+      .ant-table-row {
+        .ant-table-cell {
+          padding: 10px;
+
+          a {
+            color: var(--themeColor) !important;
+          }
+        }
+      }
+      .tabx {
+        color: #ff4d4d;
+      }
+    }
+  }
+}

+ 171 - 0
src/components/MyTable/index.tsx

@@ -0,0 +1,171 @@
+import React, { useCallback, useEffect, useMemo } from 'react'
+import styles from './index.module.scss'
+import { Table } from 'antd'
+import ImageLazy from '../ImageLazy'
+
+type Props = {
+  yHeight?: number //设置表格的高度
+  list: any //表格数据
+  columnsTemp: any[][] //表格展示
+  total?: number //总数
+  pageNum?: number
+  pageSize?: number
+  pagingInfo?: any | boolean
+  onChange?: (pageNum: number, pageSize: number) => void
+  lastBtn?: any
+  classKey?: string //一个组件多次使用的时候要传递,分别设置style
+  // 表格简单的合并
+  merge?: { type: string; num: number; loc: 'rowSpan' | 'colSpan' }
+  // 定制化表头
+  myTitle?: { name: string; Com: React.ReactNode }
+  // 为空的定制字段
+  isNull?: string
+}
+
+// 表格内容定制化
+const tableComObj = (key: string, val: string[]) => {
+  const obj = {
+    A: (
+      <a href={val[1]} target='_blank' title={val[1]} rel='noreferrer'>
+        {val[0]}
+      </a>
+    )
+  }
+  return Reflect.get(obj, key)
+}
+
+function MyTable({
+  yHeight,
+  list,
+  columnsTemp,
+  total,
+  pageNum = 1,
+  pageSize = 10,
+  pagingInfo = {
+    showQuickJumper: true,
+    position: ['bottomCenter'],
+    showSizeChanger: true
+  },
+  onChange,
+  lastBtn = [],
+  classKey = '',
+  merge,
+  myTitle,
+  isNull = '(空)'
+}: Props) {
+  useEffect(() => {
+    const dom = document.querySelector(`.MyTable${classKey} .ant-table-body`) as HTMLDivElement
+
+    if (dom && yHeight) dom.style.height = yHeight + 'px'
+  }, [classKey, yHeight])
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      if (onChange) {
+        onChange(pageNum, pageSize)
+      }
+    },
+    [onChange]
+  )
+
+  const dataChangeFu = useCallback(
+    (v: any) => {
+      /**
+       * index:序号
+       * txt:正常数据
+       * img:图片
+       * txtChange:判断显示不同字段
+       * text:文字比较多的情况
+       */
+
+      const obj = {
+        index: (_: any, __: any, index: number) => index + 1 + (pageNum - 1) * pageSize,
+        txt: (item: any) =>
+          v[3] && !item[v[2]] ? (
+            <div dangerouslySetInnerHTML={{ __html: v[3] }}></div>
+          ) : (
+            item[v[2]] || isNull
+          ),
+        img: (item: any) =>
+          v[3] && !item[v[2]] ? (
+            <div dangerouslySetInnerHTML={{ __html: v[3] }}></div>
+          ) : (
+            <div className='tableImgAuto'>
+              <ImageLazy
+                width={60}
+                height={60}
+                srcBig={item.thumbPc}
+                src={item[v[2]] || item.thumb}
+                offline={(item[v[2]] || item.thumb).includes('http')}
+              />
+            </div>
+          ),
+        txtChange: (item: any) => Reflect.get(v[3], item[v[2]]) || v[4] || isNull,
+        text: (item: any) => {
+          let tempCom: any = item[v[2]] || isNull
+
+          if (tempCom.length >= v[3]) {
+            tempCom = tempCom.substring(0, v[3]) + '...'
+          }
+
+          if (v[4]) {
+            tempCom = tableComObj(v[4], [tempCom, item[v[2]]])
+          } else if (item[v[2]].length >= v[3]) {
+            tempCom = (
+              <span style={{ cursor: 'pointer' }} title={item[v[2]]}>
+                {tempCom}
+              </span>
+            )
+          }
+
+          return tempCom
+        }
+      }
+
+      return Reflect.get(obj, v[0])
+    },
+    [isNull, pageNum, pageSize]
+  )
+
+  const columns = useMemo(() => {
+    const arr: any = columnsTemp.map((v: any) => ({
+      title: myTitle && v.includes(myTitle.name) ? myTitle.Com : v[1],
+      render: dataChangeFu(v),
+      onCell:
+        merge && v.includes(merge.type)
+          ? // {rowSpan:3}
+            (item: any, index: number) => ({
+              rowSpan: index === 0 ? merge.num : 0
+            })
+          : ''
+    }))
+
+    return arr
+  }, [columnsTemp, dataChangeFu, merge, myTitle])
+
+  return (
+    <Table
+      className={`${styles.MyTable} MyTable${classKey}`}
+      scroll={{ y: yHeight ? yHeight : '' }}
+      dataSource={list}
+      columns={[...columns, ...lastBtn]}
+      rowKey='id'
+      pagination={
+        pagingInfo
+          ? {
+              ...pagingInfo,
+              current: pageNum,
+              pageSize: pageSize,
+              total: total,
+              onChange: paginationChange()
+            }
+          : false
+      }
+    />
+  )
+}
+
+const MemoMyTable = React.memo(MyTable)
+
+export default MemoMyTable

+ 26 - 0
src/components/NotFound/index.tsx

@@ -0,0 +1,26 @@
+import { Result } from "antd";
+import { useEffect, useRef } from "react";
+
+export default function NotFound() {
+  const timeRef = useRef(-1);
+
+  useEffect(() => {
+    timeRef.current = window.setTimeout(() => {
+      const dom: HTMLDivElement = document.querySelector(".noFindPage")!;
+      dom.style.opacity = "1";
+    }, 500);
+    return () => {
+      clearTimeout(timeRef.current);
+    };
+  }, []);
+
+  return (
+    <div className="noFindPage">
+      <Result
+        status="404"
+        title="404"
+        subTitle="找不到页面,或没有权限!"
+      />
+    </div>
+  );
+}

+ 10 - 0
src/components/SpinLoding/index.module.scss

@@ -0,0 +1,10 @@
+.SpinLoding {
+  position: relative;
+  z-index: 9999;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

+ 13 - 0
src/components/SpinLoding/index.tsx

@@ -0,0 +1,13 @@
+import styles from "./index.module.scss";
+import { Spin } from "antd";
+import React from "react";
+function SpinLoding() {
+  return (
+    <div className={styles.SpinLoding}>
+      <Spin size='large'/>
+    </div>
+  );
+}
+const MemoSpinLoding = React.memo(SpinLoding);
+
+export default MemoSpinLoding;

+ 43 - 0
src/components/UpAsyncLoding/index.module.scss

@@ -0,0 +1,43 @@
+.UpAsyncLoding {
+  opacity: 0;
+  pointer-events: none;
+  position: fixed;
+  z-index: 10000;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, .4);
+
+  :global {
+    .progressBox {
+      position: absolute;
+      top: 60%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 500px;
+      height: 6px;
+      border-radius: 3px;
+      border: 1px solid var(--themeColor);
+      overflow: hidden;
+
+      #progress {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 0%;
+        height: 100%;
+        background-color: var(--themeColor);
+      }
+
+    }
+
+    .closeUpBtn {
+      position: absolute;
+      top: 70%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+
+  }
+}

+ 38 - 0
src/components/UpAsyncLoding/index.tsx

@@ -0,0 +1,38 @@
+import store, { RootState } from "@/store";
+import { Button } from "antd";
+import React, { useCallback } from "react";
+import { useSelector } from "react-redux";
+import styles from "./index.module.scss";
+function UpAsyncLoding() {
+  // 从仓库中获取取消上传的函数
+  const closeUpFile = useSelector(
+    (state: RootState) => state.A0Layout.closeUpFile
+  );
+
+  const btnClose = useCallback(() => {
+    closeUpFile.fu();
+
+    setTimeout(() => {
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: () => {}, state: false },
+      });
+    }, 200);
+  }, [closeUpFile]);
+
+  return (
+    <div id="UpAsyncLoding" className={styles.UpAsyncLoding}>
+      <div className="progressBox">
+        <div id="progress"></div>
+      </div>
+      {/* 手动取消上传按钮 */}
+      <div className="closeUpBtn">
+        <Button onClick={btnClose}>取消上传</Button>
+      </div>
+    </div>
+  );
+}
+
+const MemoUpAsyncLoding = React.memo(UpAsyncLoding);
+
+export default MemoUpAsyncLoding;

+ 195 - 0
src/components/ZRichTexts/index.module.scss

@@ -0,0 +1,195 @@
+.ZRichTexts {
+  width: 1000px;
+
+  :global {
+    // 正文
+    .formRightZW {
+      width: 1000px;
+      top: -3px;
+      position: relative;
+      display: flex;
+      align-items: center;
+      // justify-content: space-between;
+      height: 32px;
+
+      .formRightZWRR {
+        display: flex;
+      }
+    }
+
+    // 从查看进来
+    .formRightZWLook {
+      .ant-checkbox-wrapper {
+        pointer-events: none;
+      }
+    }
+
+    .txtBox {
+      width: 100%;
+      position: relative;
+
+      a {
+        color: #fff !important;
+      }
+
+      // 隐藏媒体功能
+      .control-item.media {
+        display: none;
+      }
+
+      button {
+        &:nth-of-type(7) {
+          display: none !important;
+        }
+        &:nth-of-type(8) {
+          display: none !important;
+        }
+        &:nth-of-type(18) {
+          display: none !important;
+        }
+        &:nth-of-type(19) {
+          display: none !important;
+        }
+      }
+
+      .bf-container {
+        height: 100%;
+      }
+
+      .bf-content {
+        height: 300px;
+        padding-bottom: 0px;
+      }
+
+      .bf-controlbar {
+        position: relative;
+
+        .upImgBox {
+          position: absolute;
+          bottom: 13px;
+          right: 15px;
+          cursor: pointer;
+          color: var(--themeColor);
+          // display: none;
+        }
+
+        .upImgBoxNo {
+          display: none;
+        }
+      }
+
+      .zztxtRow {
+        margin-bottom: 20px;
+        border: 1px solid #ccc;
+
+        .zztxtRow1 {
+          padding: 0 20px;
+          height: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          background-color: #e8e8e8;
+
+          .zztxtRow1_1 {
+            display: flex;
+            align-items: center;
+
+            .zztxtRow1_1_1 {
+              font-weight: 700;
+              font-size: 16px;
+              margin-right: 20px;
+            }
+
+            .zztxtRow1_1_2 {
+              display: flex;
+              align-items: center;
+              width: 580px;
+              height: 32px;
+            }
+          }
+        }
+
+        .zztxtRow1_2 {
+          display: flex;
+          align-items: center;
+
+          .anticon {
+            cursor: pointer;
+            font-size: 18px;
+          }
+
+          .zztxtRow1_2Icon {
+            position: relative;
+            top: 2px;
+          }
+
+          .zztxtRow1_2IconNo {
+            pointer-events: none;
+            opacity: 0.2;
+          }
+        }
+      }
+
+      .zztxtRowErr {
+        border-color: #ff4d4f;
+      }
+    }
+
+    // 从查看进来
+    .txtBoxLook {
+      .button-remove {
+        display: none !important;
+      }
+      .bf-controlbar {
+        pointer-events: auto !important;
+        display: flex;
+        justify-content: flex-end;
+        button {
+          display: none;
+          &:last-child {
+            display: inline-block;
+          }
+        }
+        div {
+          display: none;
+        }
+        .separator-line {
+          display: none;
+        }
+      }
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top 0.2s;
+      color: #ff4d4f;
+      top: -20px;
+    }
+
+    .noUpThumbAc {
+      top: -10px;
+      opacity: 1;
+    }
+
+    .bf-media .bf-image {
+      float: initial !important;
+      display: block;
+      margin: 0px auto;
+      text-align: center;
+
+      // 不让拖动放大缩小图片(会报错)
+      .bf-csize-icon {
+        display: none !important;
+      }
+
+      img {
+        max-width: 500px;
+        max-height: 300px;
+      }
+    }
+
+    // .bf-video-wrap
+  }
+}

+ 429 - 0
src/components/ZRichTexts/index.tsx

@@ -0,0 +1,429 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+
+// 引入编辑器组件
+
+// 安装---npm install braft-editor --save --force
+// npm install braft-utils --save --force
+import { ContentUtils } from 'braft-utils'
+import BraftEditor from 'braft-editor'
+// 引入编辑器样式
+import 'braft-editor/dist/index.css'
+
+import classNames from 'classnames'
+import { MessageFu } from '@/utils/message'
+import { fileDomInitialFu } from '@/utils/domShow'
+import { baseURL } from '@/utils/http'
+
+import { forwardRef, useImperativeHandle } from 'react'
+import { API_upFile } from '@/store/action/layout'
+import ZupAudio, { ZupAudioType } from '../ZupAudio'
+import { Button, Checkbox, Input } from 'antd'
+import { ArrowDownOutlined, DeleteOutlined, ArrowUpOutlined } from '@ant-design/icons'
+import MyPopconfirm from '../MyPopconfirm'
+
+export type SectionArrType = {
+  id: number
+  name: string
+  txt: any
+  fileInfo: ZupAudioType
+}
+
+type Props = {
+  check: boolean //表单校验,为fasle表示不校验
+  dirCode: string //文件的code码
+  isLook: boolean //是否是查看进来
+  // ref: any //当前自己的ref,给父组件调用
+  myUrl: string //上传的api地址
+  isOne?: boolean //只显示单个富文本
+  upAudioBtnNone?: boolean //是否能上传无障碍音频
+  // 其他后端需要的配置项
+  otherArr?: { key: string; value: string }[]
+}
+
+function ZRichTexts(
+  {
+    check,
+    dirCode,
+    isLook,
+    myUrl,
+    isOne = false,
+    upAudioBtnNone = false,
+    otherArr
+  }: Props,
+  ref: any
+) {
+  const [sectionArr, setSectionArr] = useState<SectionArrType[]>([
+    {
+      id: Date.now(),
+      name: '',
+      txt: BraftEditor.createEditorState(''),
+      fileInfo: { fileName: '', filePath: '' }
+    }
+  ])
+
+  // 是否按章节发布
+  const [isSection, setIsSection] = useState(false)
+
+  // 当前上传 图片
+  const nowIndexRef = useRef(0)
+
+  // 判断 富文本是否为空
+  const isTxtFlag = useMemo(() => {
+    let flag = false
+
+    // 不是按章节发布,检查第一个富文本
+    if (!isSection) {
+      const txt = sectionArr[0].txt.toText()
+      const txtHtml = sectionArr[0].txt.toHTML()
+      const txtRes = txt.replaceAll('\n', '').replaceAll(' ', '')
+      if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
+    } else {
+      // 按章节发布  检查 所有的 标题 和富文本
+      sectionArr.forEach(v => {
+        if (!v.name) flag = true
+        const txt = v.txt.toText()
+        const txtHtml = sectionArr[0].txt.toHTML()
+        const txtRes = txt.replaceAll('\n', '').replaceAll(' ', '')
+        if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
+      })
+    }
+
+    return flag
+  }, [isSection, sectionArr])
+
+  const myInput = useRef<HTMLInputElement>(null)
+
+  // 上传图片
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0]
+
+        let type = ['image/jpeg', 'image/png']
+        let size = 5
+        let txt = '图片只支持png、jpg和jpeg格式!'
+        let txt2 = '图片最大支持5M!'
+
+        // 校验格式
+        if (!type.includes(filesInfo.type)) {
+          e.target.value = ''
+          return MessageFu.warning(txt)
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = ''
+          return MessageFu.warning(txt2)
+        }
+
+        // 创建FormData对象
+        const fd = new FormData()
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append('type', 'img')
+        fd.append('dirCode', dirCode)
+        fd.append('file', filesInfo)
+
+        if (otherArr) {
+          otherArr.forEach((v: any) => {
+            fd.append(v.key, v.value)
+          })
+        }
+
+        e.target.value = ''
+
+        try {
+          const res = await API_upFile(fd, myUrl)
+          if (res.code === 0) {
+            MessageFu.success('上传成功!')
+            // 在光标位置插入图片
+            const newTxt = ContentUtils.insertMedias(
+              sectionArr[nowIndexRef.current].txt,
+              [
+                {
+                  type: 'IMAGE',
+                  url: baseURL + res.data.filePath
+                }
+              ]
+            )
+            const arr = [...sectionArr]
+            arr[nowIndexRef.current].txt = newTxt
+            setSectionArr(arr)
+          }
+          fileDomInitialFu()
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, myUrl, otherArr, sectionArr]
+  )
+
+  // 让父组件调用的 回显 富文本
+  const ritxtShowFu = useCallback((val: any) => {
+    if (val) {
+      setIsSection(val.isSection || false)
+      if (val.txtArr) {
+        const arr = val.txtArr.map((v: any) => ({
+          ...v,
+          txt: BraftEditor.createEditorState(v.txt)
+        }))
+        setSectionArr(arr)
+      }
+    }
+  }, [])
+
+  // 让父组件调用的返回 富文本信息 和 表单校验 isTxtFlag为ture表示未通过校验
+  const fatherBtnOkFu = useCallback(() => {
+    const arr: any[] = []
+
+    sectionArr.forEach((v, i) => {
+      arr.push({
+        ...v,
+        txt: v.txt.toHTML()
+      })
+    })
+
+    const obj = {
+      isSection: isSection, //是否按章节发布
+      txtArr: arr,
+      isTxtFlag
+    }
+
+    return { val: obj, flag: isTxtFlag }
+  }, [isSection, isTxtFlag, sectionArr])
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    ritxtShowFu,
+    fatherBtnOkFu
+  }))
+
+  // 点击新增章节
+  const addSectionFu = useCallback(() => {
+    if (sectionArr.length >= 20) return MessageFu.warning('最多存在20个章节')
+    setSectionArr([
+      ...sectionArr,
+      {
+        id: Date.now(),
+        name: '',
+        txt: BraftEditor.createEditorState(''),
+        fileInfo: { fileName: '', filePath: '' }
+      }
+    ])
+  }, [sectionArr])
+
+  // 章节音频上传成功
+  const upSectionFu = useCallback(
+    (info: ZupAudioType, index: number) => {
+      const arr = [...sectionArr]
+      arr[index].fileInfo = info
+      setSectionArr(arr)
+    },
+    [sectionArr]
+  )
+
+  // 章节音频删除
+  const delSectionFu = useCallback(
+    (index: number) => {
+      // console.log("ppppppppp", index);
+
+      const arr = [...sectionArr]
+      arr[index].fileInfo = { fileName: '', filePath: '' }
+      setSectionArr(arr)
+    },
+    [sectionArr]
+  )
+
+  // 整个章节的删除
+  const delSectionAllFu = useCallback(
+    (id: number) => {
+      setSectionArr(sectionArr.filter(v => v.id !== id))
+    },
+    [sectionArr]
+  )
+
+  // 整个章节的位移
+  const moveSectionFu = useCallback(
+    (index: number, num: number) => {
+      const arr = [...sectionArr]
+      const temp = arr[index]
+      arr[index] = arr[index + num]
+      arr[index + num] = temp
+      setSectionArr(arr)
+    },
+    [sectionArr]
+  )
+
+  // 单个富文本是否输入完整
+  const isOneTxtFlag = useCallback(
+    (name: string, txt: any) => {
+      let flag = false
+      if (!name && isSection) flag = true
+      const txt2 = txt.toText()
+      const txtHtml = txt.toHTML()
+      const txtRes = txt2.replaceAll('\n', '').replaceAll(' ', '')
+      if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
+      return flag
+    },
+    [isSection]
+  )
+
+  return (
+    <div className={styles.ZRichTexts}>
+      <input
+        id='upInput'
+        type='file'
+        accept='.png,.jpg,.jpeg'
+        ref={myInput}
+        onChange={e => handeUpPhoto(e)}
+      />
+
+      <div className={classNames('formRightZW', isLook ? 'formRightZWLook' : '')}>
+        {isOne ? (
+          <div></div>
+        ) : (
+          <Checkbox checked={isSection} onChange={e => setIsSection(e.target.checked)}>
+            按章节发布
+          </Checkbox>
+        )}
+
+        {isSection ? (
+          <Button hidden={isLook} type='primary' onClick={addSectionFu}>
+            新增章节
+          </Button>
+        ) : (
+          <div className='formRightZWRR'>
+            {upAudioBtnNone ? null : (
+              <ZupAudio
+                fileInfo={sectionArr[0].fileInfo}
+                upDataFu={info => upSectionFu(info, 0)}
+                delFu={() => delSectionFu(0)}
+                dirCode={dirCode}
+                myUrl={myUrl}
+                isLook={isLook}
+              />
+            )}
+
+            <div hidden={isLook}>
+              <Button
+                onClick={() => {
+                  nowIndexRef.current = 0
+                  myInput.current?.click()
+                }}
+              >
+                上传图片
+              </Button>
+            </div>
+          </div>
+        )}
+      </div>
+
+      <div className={classNames('txtBox', isLook ? 'txtBoxLook' : '')}>
+        {sectionArr.map((item, index) => (
+          <div
+            className={classNames(
+              'zztxtRow',
+              isOneTxtFlag(item.name, item.txt) && check ? 'zztxtRowErr' : ''
+            )}
+            key={item.id}
+            hidden={!isSection && index > 0}
+          >
+            {/* 顶部 */}
+            <div className='zztxtRow1' hidden={!isSection && index === 0}>
+              <div className='zztxtRow1_1'>
+                <div className='zztxtRow1_1_1'>章节 {index + 1}</div>
+                <div className='zztxtRow1_1_2'>
+                  标题:
+                  <Input
+                    readOnly={isLook}
+                    value={item.name}
+                    placeholder='请输入内容'
+                    maxLength={100}
+                    showCount
+                    style={{ width: 400 }}
+                    onChange={e => {
+                      const arr = [...sectionArr]
+                      arr[index].name = e.target.value.replace(/\s+/g, '')
+                      setSectionArr(arr)
+                    }}
+                  />
+                  &emsp;
+                  <Button
+                    hidden={isLook}
+                    onClick={() => {
+                      nowIndexRef.current = index
+                      myInput.current?.click()
+                    }}
+                  >
+                    上传图片
+                  </Button>
+                </div>
+              </div>
+              <div className='zztxtRow1_2'>
+                <ZupAudio
+                  fileInfo={item.fileInfo}
+                  upDataFu={info => upSectionFu(info, index)}
+                  delFu={() => delSectionFu(index)}
+                  dirCode={dirCode}
+                  myUrl={myUrl}
+                  isLook={isLook}
+                />
+                &emsp;
+                <div
+                  hidden={isLook}
+                  className={classNames(
+                    'zztxtRow1_2Icon',
+                    index === 0 ? 'zztxtRow1_2IconNo' : ''
+                  )}
+                  onClick={() => moveSectionFu(index, -1)}
+                >
+                  <ArrowUpOutlined title='上移' rev={undefined} />
+                </div>
+                &emsp;
+                <div
+                  hidden={isLook}
+                  className={classNames(
+                    'zztxtRow1_2Icon',
+                    index === sectionArr.length - 1 ? 'zztxtRow1_2IconNo' : ''
+                  )}
+                  onClick={() => moveSectionFu(index, 1)}
+                >
+                  <ArrowDownOutlined title='下移' rev={undefined} />
+                </div>
+                &emsp;
+                {isLook || sectionArr.length <= 1 ? null : (
+                  <MyPopconfirm
+                    txtK='删除'
+                    onConfirm={() => delSectionAllFu(item.id)}
+                    Dom={
+                      <DeleteOutlined title='删除' className='ZTbox2X' rev={undefined} />
+                    }
+                  />
+                )}
+              </div>
+            </div>
+            {/* 主体 */}
+            <BraftEditor
+              readOnly={isLook}
+              placeholder='请输入内容'
+              value={item.txt}
+              onChange={e => {
+                const arr = [...sectionArr]
+                arr[index].txt = e
+                setSectionArr(arr)
+              }}
+              imageControls={['remove']}
+            />
+          </div>
+        ))}
+      </div>
+      <div className={classNames('noUpThumb', check && isTxtFlag ? 'noUpThumbAc' : '')}>
+        {`请完整输入${isSection ? '标题/' : ''}正文!`}
+      </div>
+    </div>
+  )
+}
+
+export default forwardRef(ZRichTexts)

+ 63 - 0
src/components/ZupAudio/index.module.scss

@@ -0,0 +1,63 @@
+// 上传无障碍音频的样式
+.ZupAudio {
+  margin-left: 20px;
+  width: 180px;
+  height: 32px;
+  // position: relative;
+  // top: -4px;
+  border: 1px solid #ccc;
+  border-radius: 16px;
+  background-color: #fff;
+
+  :global {
+
+    .ZupAudio1 {
+      cursor: pointer;
+
+      display: flex;
+      justify-content: center;
+
+      .anticon-upload {
+        font-size: 20px;
+      }
+
+      .ZupAudio1_1 {
+        line-height: 15px;
+        color: #aaa5cb;
+        margin-left: 10px;
+
+        &>p {
+          &:nth-of-type(2) {
+            font-size: 10px;
+            // color: red;
+            opacity: .7;
+          }
+        }
+      }
+    }
+
+    .ZupAudio2 {
+      padding: 0 10px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+
+      .anticon {
+        font-size: 18px;
+        cursor: pointer;
+
+      }
+
+      &>div {
+        height: 100%;
+        line-height: 30px;
+        width: calc(100% - 35px);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+    }
+
+
+  }
+}

+ 135 - 0
src/components/ZupAudio/index.tsx

@@ -0,0 +1,135 @@
+import React, { useCallback, useRef } from 'react'
+import styles from './index.module.scss'
+import { MessageFu } from '@/utils/message'
+import { API_upFile } from '@/store/action/layout'
+import { fileDomInitialFu } from '@/utils/domShow'
+import { UploadOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'
+import store from '@/store'
+import MyPopconfirm from '../MyPopconfirm'
+
+export type ZupAudioType = {
+  fileName: string
+  filePath: string
+}
+
+type Props = {
+  fileInfo: ZupAudioType
+  upDataFu: (info: ZupAudioType) => void
+  delFu: () => void
+  dirCode: string
+  myUrl: string
+  size?: number
+  isLook?: boolean
+}
+
+function ZupAudio({
+  size = 10,
+  isLook = false,
+  fileInfo,
+  upDataFu,
+  delFu,
+  dirCode,
+  myUrl
+}: Props) {
+  // 上传 无障碍音频的 点击
+  const handeUpAudio = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0]
+        // console.log("-----", filesInfo);
+
+        // 校验格式
+        const type = ['audio/mpeg']
+        if (!type.includes(filesInfo.type)) {
+          e.target.value = ''
+          return MessageFu.warning(`只支持.mp3格式!`)
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = ''
+          return MessageFu.warning(`最大支持${size}M!`)
+        }
+        // 创建FormData对象
+        const fd = new FormData()
+
+        fd.append('type', 'audio')
+        fd.append('dirCode', dirCode)
+        fd.append('file', filesInfo)
+        e.target.value = ''
+
+        try {
+          const res = await API_upFile(fd, myUrl)
+          if (res.code === 0) {
+            MessageFu.success('上传成功!')
+            // console.log(res);
+            upDataFu(res.data)
+          }
+          fileDomInitialFu()
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, myUrl, size, upDataFu]
+  )
+
+  // 上传附近的ref
+  const myInput = useRef<HTMLInputElement>(null)
+
+  return (
+    <div
+      className={styles.ZupAudio}
+      id='upInputAudioBox'
+      hidden={isLook && !fileInfo.filePath}
+    >
+      {/* 上传无障碍音频 */}
+      <input
+        id='upInputAudio'
+        type='file'
+        accept='.mp3'
+        onChange={e => handeUpAudio(e)}
+        ref={myInput}
+      />
+      {fileInfo.filePath ? (
+        <div className='ZupAudio2'>
+          <div title={fileInfo.fileName}>{fileInfo.fileName}</div>
+          <EyeOutlined
+            rev={undefined}
+            title='预览'
+            onClick={() =>
+              store.dispatch({
+                type: 'layout/lookDom',
+                payload: { src: fileInfo.filePath, type: 'audio' }
+              })
+            }
+          />
+
+          {isLook ? null : (
+            <>
+              &nbsp;
+              <MyPopconfirm
+                txtK='删除'
+                onConfirm={delFu}
+                Dom={<DeleteOutlined title='删除' className='ZTbox2X' rev={undefined} />}
+              />
+            </>
+          )}
+        </div>
+      ) : (
+        <div className='ZupAudio1' onClick={() => myInput.current?.click()}>
+          <UploadOutlined rev={undefined} />
+          <div className='ZupAudio1_1'>
+            <p>上传无障碍音频</p>
+            <p>支持{size}MB以下mp3格式</p>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
+
+const MemoZupAudio = React.memo(ZupAudio)
+
+export default MemoZupAudio

+ 101 - 0
src/components/ZupOne/index.module.scss

@@ -0,0 +1,101 @@
+.ZupOne {
+  width: 100%;
+  height: 100%;
+  position: relative;
+
+  :global {
+
+    .file_upIcon {
+      color: #a6a6a6;
+      border-radius: 3px;
+      cursor: pointer;
+      font-size: 30px;
+      width: 100px;
+      height: 100px;
+      border: 1px dashed #797979;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+
+    }
+
+    .file_img {
+      width: 100px;
+      height: 126px;
+      position: relative;
+      margin-top: 10px;
+
+      .file_closeBox {
+        position: absolute;
+        right: -10px;
+        top: -10px;
+        z-index: 99;
+        background-color: rgba(0, 0, 0, 0.8);
+        width: 20px;
+        height: 20px;
+        border-radius: 50%;
+        font-size: 16px;
+        color: #fff;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+
+
+
+      .file_lookBox {
+        width: 100%;
+        background-color: rgba(0, 0, 0, .6);
+        color: #fff;
+        display: flex;
+        justify-content: space-around;
+
+        &>a {
+          color: #fff !important;
+        }
+
+        font-size: 16px;
+      }
+    }
+
+    .fileInfo {
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+
+      .clearCover {
+        margin-left: 20px;
+        cursor: pointer;
+        font-size: 16px;
+      }
+
+      &>a {
+        color: black;
+      }
+    }
+
+    .fileBoxRow_r_tit {
+      height: 46px;
+      margin-top: 5px;
+      font-size: 14px;
+      color: rgb(126, 124, 124);
+
+
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top .2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+  }
+}

+ 295 - 0
src/components/ZupOne/index.tsx

@@ -0,0 +1,295 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import ImageLazy from '@/components/ImageLazy'
+import {
+  PlusOutlined,
+  EyeOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  UploadOutlined
+} from '@ant-design/icons'
+import store from '@/store'
+import { baseURL } from '@/utils/http'
+import classNames from 'classnames'
+import { Button } from 'antd'
+import { MessageFu } from '@/utils/message'
+import { fileDomInitialFu } from '@/utils/domShow'
+import { API_upFile } from '@/store/action/layout'
+import { forwardRef, useImperativeHandle } from 'react'
+import MyPopconfirm from '../MyPopconfirm'
+
+type MyTypeType = 'thumb' | 'video' | 'audio' | 'model' | 'pdf' | 'epub'
+
+// 这个组件 只处理 上传 一张图片或者 视频 音频 模型 pdf 的情况
+
+type Props = {
+  fileCheck: boolean //有没有点击过确定
+  size: number //上传附件大小(M)
+  dirCode: string //文件的code码
+  myUrl: string //请求地址
+  format: string[] //上传格式 ["image/jpeg", "image/png"] ["video/mp4"] ,application/pdf
+  formatTxt: string //上传图片提示
+  checkTxt: string
+  upTxt: string
+  myType: MyTypeType
+  isLook?: boolean //是不是查看
+  // ref: any //当前自己的ref,给父组件调用
+  // 其他后端需要的配置项
+  otherArr?: { key: string; value: string }[]
+}
+
+function ZupOne(
+  {
+    fileCheck,
+    size,
+    dirCode,
+    myUrl,
+    format,
+    formatTxt,
+    checkTxt,
+    upTxt,
+    myType,
+    isLook = false,
+    otherArr
+  }: Props,
+  ref: any
+) {
+  const [fileUrl, setFileUrl] = useState({
+    fileName: '',
+    filePath: '',
+    thumb: '' //压缩图
+  })
+
+  const myInput = useRef<HTMLInputElement>(null)
+
+  // 上传封面图
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0]
+        // console.log("-----", filesInfo.type);
+
+        // 校验格式
+        const type = format
+
+        if (myType === 'pdf') {
+          if (!filesInfo.type.includes('pdf')) {
+            e.target.value = ''
+            return MessageFu.warning(`只支持${formatTxt}格式!`)
+          }
+        } else if (myType === 'epub') {
+          if (!filesInfo.name.endsWith('.epub')) {
+            e.target.value = ''
+            return MessageFu.warning(`只支持${formatTxt}格式!`)
+          }
+        } else {
+          if (!type.includes(filesInfo.type)) {
+            e.target.value = ''
+            return MessageFu.warning(`只支持${formatTxt}格式!`)
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = ''
+          return MessageFu.warning(`最大支持${size}M!`)
+        }
+        // 创建FormData对象
+        const fd = new FormData()
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        let myTypeRes: string = myType
+        if (['pdf', 'epub'].includes(myTypeRes)) myTypeRes = 'doc'
+        fd.append('type', myTypeRes)
+        fd.append('dirCode', dirCode)
+        fd.append('file', filesInfo)
+
+        if (otherArr) {
+          otherArr.forEach((v: any) => {
+            fd.append(v.key, v.value)
+          })
+        }
+
+        if (filesInfo.size > 1 * 1024 * 1024) {
+          // 开启压缩图片
+          fd.append('isCompress', 'true')
+        }
+
+        e.target.value = ''
+
+        try {
+          const res = await API_upFile(fd, myUrl)
+          if (res.code === 0) {
+            MessageFu.success('上传成功!')
+            setFileUrl(res.data)
+          }
+          fileDomInitialFu()
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, format, formatTxt, myType, myUrl, otherArr, size]
+  )
+
+  // 让父组件调用的 回显 附件 地址
+  const setFileComFileFu = useCallback(
+    (valObj: { fileName: string; filePath: string; thumb: string }) => {
+      setFileUrl(valObj)
+    },
+    []
+  )
+
+  // 让父组件调用的返回 附件 名字和路径
+  const fileComFileResFu = useCallback(() => {
+    return fileUrl
+  }, [fileUrl])
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    setFileComFileFu,
+    fileComFileResFu
+  }))
+
+  const acceptRes = useMemo(() => {
+    let accept = '.png,.jpg,.jpeg'
+    if (myType === 'video') accept = '.mp4'
+    else if (myType === 'audio') accept = '.mp3'
+    else if (myType === 'model') accept = '.4dage'
+    else if (myType === 'pdf') accept = '.pdf'
+    else if (myType === 'epub') accept = '.epub'
+    return accept
+  }, [myType])
+
+  // 点击 预览(除了图片)
+  const lookFileNoImgFu = useCallback(
+    (type: MyTypeType) => {
+      if (type === 'pdf' || type === 'thumb') {
+        // 新窗口打开
+        window.open(baseURL + fileUrl.filePath)
+      } else if (type !== 'epub') {
+        store.dispatch({
+          type: 'layout/lookDom',
+          payload: { src: fileUrl.filePath, type }
+        })
+      }
+
+      // if (type === "pdf") {
+      // } else {
+      // }
+    },
+    [fileUrl.filePath]
+  )
+
+  return (
+    <div className={styles.ZupOne}>
+      <input
+        id='upInput'
+        type='file'
+        accept={acceptRes}
+        ref={myInput}
+        onChange={e => handeUpPhoto(e)}
+      />
+      {myType === 'thumb' ? (
+        <div
+          hidden={fileUrl.filePath !== ''}
+          className='file_upIcon'
+          onClick={() => myInput.current?.click()}
+        >
+          <PlusOutlined rev={undefined} />
+        </div>
+      ) : (
+        <Button
+          hidden={fileUrl.filePath !== ''}
+          onClick={() => myInput.current?.click()}
+          icon={<UploadOutlined rev={undefined} />}
+        >
+          上传
+        </Button>
+      )}
+
+      {/* 为图片的情况-------------- */}
+      {myType === 'thumb' ? (
+        <div className='file_img' hidden={fileUrl.filePath === ''}>
+          {fileUrl ? (
+            <ImageLazy
+              width={100}
+              height={100}
+              srcBig={fileUrl.filePath}
+              src={fileUrl.thumb}
+              noLook
+            />
+          ) : null}
+
+          {/* 删除 */}
+          <div className='file_closeBox' hidden={isLook}>
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => setFileUrl({ fileName: '', filePath: '', thumb: '' })}
+              Dom={<CloseOutlined rev={undefined} />}
+            />
+          </div>
+
+          {/* 预览 下载 */}
+          <div className='file_lookBox'>
+            <EyeOutlined
+              onClick={() =>
+                store.dispatch({
+                  type: 'layout/lookBigImg',
+                  payload: { url: baseURL + fileUrl.filePath, show: true }
+                })
+              }
+              rev={undefined}
+            />
+            <a href={baseURL + fileUrl.filePath} download target='_blank' rel='noreferrer'>
+              <DownloadOutlined rev={undefined} />
+            </a>
+          </div>
+        </div>
+      ) : fileUrl.filePath ? (
+        <div className='fileInfo'>
+          <div className='upSuccTxt'>{fileUrl.fileName}</div>
+          {/* 视频预览 */}
+          <div
+            className='clearCover'
+            hidden={!fileUrl.filePath || myType === 'epub'}
+            onClick={() => lookFileNoImgFu(myType)}
+          >
+            <EyeOutlined rev={undefined} />
+          </div>
+          {/* 视频下载 */}
+          <a
+            href={baseURL + fileUrl.filePath}
+            download
+            target='_blank'
+            className='clearCover'
+            rel='noreferrer'
+          >
+            <DownloadOutlined rev={undefined} />
+          </a>
+          {/* 视频删除 */}
+
+          {isLook ? null : (
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => setFileUrl({ fileName: '', filePath: '', thumb: '' })}
+              Dom={<CloseOutlined className='clearCover' rev={undefined} />}
+            />
+          )}
+        </div>
+      ) : null}
+
+      <div className='fileBoxRow_r_tit' hidden={isLook}>
+        格式要求:支持{formatTxt}格式;最大支持{size}M。{upTxt}
+        <br />
+        <div
+          className={classNames('noUpThumb', !fileUrl.filePath && fileCheck ? 'noUpThumbAc' : '')}
+        >
+          {checkTxt}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default forwardRef(ZupOne)

+ 215 - 0
src/components/ZupTypes/index.module.scss

@@ -0,0 +1,215 @@
+.ZupTypes {
+  padding-top: 3px;
+
+  :global {
+    .ZTboxTit {
+      margin-left: 10px;
+      font-size: 14px;
+      color: rgb(126, 124, 124);
+      position: relative;
+      top: 4px;
+    }
+
+    .ZTbox {
+      margin-top: 20px;
+      display: flex;
+
+      .ZTbox1 {
+        position: relative;
+        top: 3px;
+        width: 60px;
+
+        & > span {
+          color: #ff4d4f;
+        }
+      }
+
+      .ZTbox2 {
+        width: calc(100% - 60px);
+        margin-left: 5px;
+        display: flex;
+        font-size: 16px;
+        align-items: center;
+
+        .ZTbox2Look {
+          margin-left: 20px;
+          cursor: pointer;
+        }
+
+        .ZTbox2Down {
+          margin-left: 15px;
+          color: black;
+        }
+
+        .ZTbox2X {
+          cursor: pointer;
+          margin-left: 15px;
+        }
+      }
+    }
+
+    // 图片
+    .ZTboxImgMain {
+      margin-top: 20px;
+
+      .ZTboxImgBox {
+        display: flex;
+
+        .ZTbox1 {
+          position: relative;
+          top: 3px;
+          width: 60px;
+
+          & > span {
+            color: #ff4d4f;
+          }
+        }
+
+        .ZTbox1Img {
+          width: calc(100% - 60px);
+          display: flex;
+          flex-wrap: wrap;
+
+          .ZTbox1ImgIcon {
+            margin-right: 20px;
+            color: #a6a6a6;
+            border-radius: 3px;
+            cursor: pointer;
+            font-size: 30px;
+            width: 100px;
+            height: 100px;
+            border: 1px dashed #797979;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+          }
+
+          .ZTbox1ImgRow {
+            // content-visibility:auto;
+            margin-right: 20px;
+            width: 100px;
+            height: 125px;
+            position: relative;
+            margin-bottom: 20px;
+            cursor: move;
+            position: relative;
+
+            // 第一张作为封面
+            .ZTbox1ImgRowCover {
+              font-size: 12px;
+              line-height: 22px;
+              position: absolute;
+              left: 0;
+              top: 0;
+              width: 100%;
+              height: 24px;
+              background-color: rgba(0, 0, 0, 0.8);
+              color: #fff;
+              text-align: center;
+              pointer-events: none;
+            }
+
+            // 修改图片名字
+            .ZTbox1ImgRowName {
+              font-size: 12px;
+              line-height: 22px;
+              position: absolute;
+              left: 0;
+              bottom: 25px;
+              width: 100%;
+              cursor: pointer;
+              height: 24px;
+              padding: 0 3px;
+              background-color: rgba(0, 0, 0, 0.8);
+              color: #fff;
+              text-align: center;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+
+              &:hover {
+                color: var(--themeColor);
+              }
+            }
+
+            .ZTbox1ImgRowIcon {
+              width: 100%;
+              background-color: rgba(0, 0, 0, 0.6);
+              color: #fff;
+              display: flex;
+              justify-content: space-around;
+              font-size: 16px;
+
+              a {
+                color: #fff !important;
+              }
+            }
+
+            .ZTbox1ImgRowX {
+              cursor: pointer;
+              position: absolute;
+              right: -10px;
+              top: -10px;
+              z-index: 99;
+              background-color: rgba(0, 0, 0, 0.8);
+              width: 20px;
+              height: 20px;
+              border-radius: 50%;
+              font-size: 16px;
+              color: #fff;
+              display: flex;
+              justify-content: center;
+              align-items: center;
+            }
+          }
+        }
+      }
+    }
+
+    .ZcheckTxt {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top 0.2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .ZcheckTxtAc {
+      top: 2px;
+      opacity: 1;
+    }
+  }
+}
+
+// 查看情况
+.ZupTypesLook {
+  :global {
+    .ZTbox1ImgRowName {
+      cursor: default !important;
+      color: #fff !important;
+    }
+
+    .ZTbox1ImgRow {
+      cursor: default !important;
+    }
+
+    .ZTbox1ImgRowX {
+      display: none !important;
+    }
+  }
+}
+
+// 修改图片名字的弹窗
+.ZupTypesMo {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .ZupTypesMoBtn {
+      margin-top: 24px;
+      text-align: center;
+    }
+  }
+}

+ 585 - 0
src/components/ZupTypes/index.tsx

@@ -0,0 +1,585 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Checkbox, Input, Modal } from 'antd'
+import { forwardRef, useImperativeHandle } from 'react'
+import { baseURL } from '@/utils/http'
+import {
+  PlusOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  EyeOutlined
+} from '@ant-design/icons'
+import { MessageFu } from '@/utils/message'
+import { API_upFile } from '@/store/action/layout'
+import { fileDomInitialFu } from '@/utils/domShow'
+import store from '@/store'
+import ImageLazy from '../ImageLazy'
+import classNames from 'classnames'
+import MyPopconfirm from '../MyPopconfirm'
+// import { A2_APIchangeImgName } from "@/store/action/A2exhibition";
+
+export type FileListType = {
+  fileName: string
+  thumb?: string
+  filePath: string
+  id: number
+  type: 'model' | 'img' | 'audio' | 'video'
+  imgName: string
+}
+
+type Props = {
+  // ref: any //当前自己的ref,给父组件调用
+  selecFlag: string //筛选的字符串 模型/图片/音频/视频
+  fileCheck: boolean //有没有点击过确定
+  dirCode: string //文件的code码
+  myUrl: string //请求地址
+  isLook?: boolean //是不是查看
+  modelSize?: number //模型文件大小限制
+  imgSize?: number //图片大小限制
+  imgLength?: number //图片数量限制
+  audioSize?: number //音频大小限制
+  videoSize?: number //视频大小限制
+  videoTit?: string //视频上传的提示语
+  isTypeShow?: boolean //默认就选中(只有一个类型的时候)
+  isUpName?: boolean //是否能修改图片名字
+  lastImgTxt?: string //加载最后面的上传提示
+  oneIsCover?: boolean //是否将第一张作为封面
+}
+
+function ZupTypes(
+  {
+    selecFlag,
+    fileCheck,
+    dirCode,
+    myUrl,
+    isLook = false,
+    modelSize = 500,
+    imgSize = 5,
+    imgLength = 9,
+    audioSize = 10,
+    videoSize = 500,
+    videoTit = '',
+    isTypeShow = false,
+    isUpName = false,
+    lastImgTxt = '',
+    oneIsCover = false
+  }: Props,
+  ref: any
+) {
+  // 筛选
+  const [typeCheck, setTypeCheck] = useState<string[]>([])
+
+  // 筛选数组
+  const typeCheckArr = useMemo(() => {
+    const arr = [
+      { label: '模型', value: 'model' },
+      { label: '图片', value: 'img' },
+      { label: '音频', value: 'audio' },
+      { label: '视频', value: 'video' }
+    ]
+
+    const arrRes = arr.filter(v => selecFlag.includes(v.label))
+    if (arrRes.length <= 1 && isTypeShow) {
+      setTypeCheck([arrRes[0].value])
+      // 默认就选中(只有一个类型的时候)
+    }
+
+    return arrRes
+  }, [isTypeShow, selecFlag])
+
+  // 上传附件的信息
+  const [fileList, setFileList] = useState({
+    model: {} as FileListType,
+    img: [] as FileListType[],
+    audio: {} as FileListType,
+    video: {} as FileListType
+  })
+
+  // 附件信息的校验,不满足返回 true
+  const fileCheckFu = useMemo(() => {
+    let flag = false
+    if (typeCheck.length === 0) flag = true
+    if (typeCheck.includes('model') && !fileList.model.id) flag = true
+    if (typeCheck.includes('img') && fileList.img.length === 0) flag = true
+    if (typeCheck.includes('audio') && !fileList.audio.id) flag = true
+    if (typeCheck.includes('video') && !fileList.video.id) flag = true
+    return flag
+  }, [fileList, typeCheck])
+
+  // 点击上传附件按钮
+  const myInput = useRef<HTMLInputElement>(null)
+
+  const [fileOneType, setFileOneType] = useState('')
+
+  useEffect(() => {
+    if (fileOneType) myInput.current?.click()
+  }, [fileOneType])
+
+  const upFileFu = useCallback((type: string) => {
+    setFileOneType('')
+    window.setTimeout(() => {
+      setFileOneType(type)
+    }, 100)
+  }, [])
+
+  // 上传附件的处理函数
+  const handeUpPhoto2 = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0]
+
+        let anType = ['image/jpeg', 'image/png']
+        let anTit1 = '只支持png、jpg格式!'
+        let anTit2 = `最大支持${imgSize}M!`
+        let anSize = imgSize * 1024 * 1024
+
+        if (fileOneType === 'audio') {
+          anType = ['audio/mpeg']
+          anTit1 = '只支持mp3格式!'
+          anTit2 = `最大支持${audioSize}M!`
+          anSize = audioSize * 1024 * 1024
+        } else if (fileOneType === 'video') {
+          anType = ['video/mp4']
+          anTit1 = '只支持mp4格式!'
+          anTit2 = `最大支持${videoSize}M!`
+          anSize = videoSize * 1024 * 1024
+        } else if (fileOneType === 'model') {
+          anType = ['']
+          anTit1 = '只支持4dage格式!'
+          anTit2 = `最大支持${modelSize}M!`
+          anSize = modelSize * 1024 * 1024
+        }
+
+        // 校验格式
+        if (fileOneType !== 'model') {
+          if (!anType.includes(filesInfo.type)) {
+            e.target.value = ''
+            return MessageFu.warning(anTit1)
+          }
+        } else {
+          if (!filesInfo.name.includes('.4dage')) {
+            e.target.value = ''
+            return MessageFu.warning(anTit1)
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = ''
+          return MessageFu.warning(anTit2)
+        }
+        // 创建FormData对象
+        const fd = new FormData()
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append('type', fileOneType)
+        fd.append('dirCode', dirCode)
+        fd.append('isDb', 'true')
+
+        //初始图片 fileName为:未命名
+        if (isUpName) {
+          fd.append('isDefaultName', 'false')
+        }
+
+        fd.append('file', filesInfo)
+
+        if (fileOneType === 'img' && filesInfo.size > 1 * 1024 * 1024) {
+          // 开启压缩图片
+          fd.append('isCompress', 'true')
+        }
+
+        e.target.value = ''
+
+        const res = await API_upFile(fd, myUrl)
+
+        try {
+          if (res.code === 0) {
+            MessageFu.success('上传成功!')
+            if (fileOneType === 'img')
+              setFileList({
+                ...fileList,
+                img: [...fileList.img, { ...res.data, imgName: '未命名' }]
+              })
+            else setFileList({ ...fileList, [fileOneType]: res.data })
+          }
+          fileDomInitialFu()
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [audioSize, dirCode, fileList, fileOneType, imgSize, isUpName, modelSize, myUrl, videoSize]
+  )
+
+  // 附件图片的拖动
+  const [dragImg, setDragImg] = useState<any>(null)
+
+  const handleDragOver = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
+      if (isLook) return
+      e.dataTransfer.dropEffect = 'move'
+    },
+    [isLook]
+  )
+
+  const handleDragEnter = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
+      if (isLook) return
+
+      e.dataTransfer.effectAllowed = 'move'
+      if (item === dragImg) return
+      const newItems = [...fileList.img] //拷贝一份数据进行交换操作。
+      const src = newItems.indexOf(dragImg) //获取数组下标
+      const dst = newItems.indexOf(item)
+      newItems.splice(dst, 0, ...newItems.splice(src, 1)) //交换位置
+      setFileList({ ...fileList, img: newItems })
+    },
+    [dragImg, fileList, isLook]
+  )
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: number) => {
+      const newItems = fileList.img.filter(v => v.id !== id)
+      setFileList({ ...fileList, img: newItems })
+    },
+    [fileList]
+  )
+
+  // 模型 音频 视频 的 dom
+  const resOneDivDom = useCallback(
+    (type: 'model' | 'audio' | 'video') => {
+      const dom = (
+        <div className='ZTbox' hidden={!typeCheck.includes(type)}>
+          <div className='ZTbox1'>
+            <span> </span>
+            {type === 'model' ? '模型' : type === 'audio' ? '音频' : '视频'}:
+          </div>
+          {fileList[type].id ? (
+            <div className='ZTbox2'>
+              <div className='ZTbox2Name'>{fileList[type].fileName}</div>
+
+              <div
+                className='ZTbox2Look'
+                onClick={() =>
+                  store.dispatch({
+                    type: 'layout/lookDom',
+                    payload: { src: fileList[type].filePath, type }
+                  })
+                }
+              >
+                <EyeOutlined rev={undefined} />
+              </div>
+
+              <a
+                href={baseURL + fileList[type].filePath}
+                download
+                target='_blank'
+                className='ZTbox2Down'
+                rel='noreferrer'
+              >
+                <DownloadOutlined rev={undefined} />
+              </a>
+
+              <MyPopconfirm
+                txtK='删除'
+                onConfirm={() => setFileList({ ...fileList, [type]: {} as FileListType })}
+                Dom={<CloseCircleOutlined className='ZTbox2X' rev={undefined} />}
+              />
+            </div>
+          ) : (
+            <>
+              <Button onClick={() => upFileFu(type)} icon={<UploadOutlined rev={undefined} />}>
+                上传
+              </Button>
+
+              <div className='ZTboxTit'>
+                {type === 'model'
+                  ? `仅支持4dage格式的模型文件,大小不能超过${modelSize}M。`
+                  : type === 'audio'
+                    ? `仅支持mp3格式的音频文件,大小不得超过${audioSize}MB。`
+                    : `仅支持mp4格式的视频文件,大小不得超过${videoSize}MB。${videoTit}`}
+              </div>
+            </>
+          )}
+        </div>
+      )
+      return dom
+    },
+    [audioSize, fileList, modelSize, typeCheck, upFileFu, videoSize, videoTit]
+  )
+
+  // ------------让父组件调用的 回显
+  const setFileComFileFu = useCallback((info: any) => {
+    if (info.type) setTypeCheck(info.type.split(','))
+
+    if (info.fileList && info.fileList.length) {
+      const data: FileListType[] = info.fileList
+      const obj = {
+        model: {} as FileListType,
+        img: [] as FileListType[],
+        audio: {} as FileListType,
+        video: {} as FileListType
+      }
+
+      data.forEach(v => {
+        if (v.type === 'img') {
+          obj.img.push({ ...v, imgName: v.fileName })
+        } else obj[v.type!] = v
+      })
+      setFileList(obj)
+    }
+  }, [])
+
+  // --------------让父组件调用的返回 附件 信息
+  const fileComFileResFu = useCallback(() => {
+    let coverUrl = ''
+    const fileIds = []
+    if (fileList.model.id && typeCheck.includes('model')) fileIds.push(fileList.model.id)
+    if (fileList.audio.id && typeCheck.includes('audio')) fileIds.push(fileList.audio.id)
+    if (fileList.video.id && typeCheck.includes('video')) fileIds.push(fileList.video.id)
+    if (typeCheck.includes('img')) {
+      fileList.img.forEach((v, i) => {
+        if (v.id) {
+          fileIds.push(v.id)
+          if (oneIsCover && i === 0) {
+            // 返回 第一张图的url 作为封面
+            coverUrl = v.thumb || v.filePath
+          }
+        }
+      })
+    }
+    return {
+      sonType: typeCheck,
+      sonFileIds: fileIds,
+      sonIsOk: fileCheckFu,
+      coverUrl
+    }
+  }, [
+    fileCheckFu,
+    fileList.audio.id,
+    fileList.img,
+    fileList.model.id,
+    fileList.video.id,
+    oneIsCover,
+    typeCheck
+  ])
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    setFileComFileFu,
+    fileComFileResFu
+  }))
+
+  // 修改图片名称
+  const [isNameChange, setIsNameChange] = useState({
+    id: 0,
+    oldName: '',
+    newName: ''
+  })
+
+  // 关闭弹窗
+  const isNameChangeXFu = useCallback(() => {
+    setIsNameChange({ id: 0, oldName: '', newName: '' })
+  }, [])
+
+  // 点击图片名字-出来弹窗
+  const isNameChangeFu = useCallback(
+    (item: FileListType) => {
+      if (isLook) return
+      setIsNameChange({ id: item.id, oldName: item.imgName, newName: '' })
+    },
+    [isLook]
+  )
+
+  // 修改完这点击 确定修改
+  const isNameChangeOkFu = useCallback(async () => {
+    // if (!isNameChange.newName) return MessageFu.warning("图片名不能为空!");
+    // const res = await A2_APIchangeImgName({
+    //   id: isNameChange.id,
+    //   fileName: isNameChange.newName,
+    // });
+    // if (res.code === 0) {
+    //   MessageFu.success("修改图片名成功!");
+    //   setFileList({
+    //     ...fileList,
+    //     img: fileList.img.map((v) => ({
+    //       ...v,
+    //       imgName: v.id === isNameChange.id ? isNameChange.newName : v.imgName,
+    //     })),
+    //   });
+    //   isNameChangeXFu();
+    // }
+  }, [])
+  //
+
+  return (
+    <div className={classNames(styles.ZupTypes, isLook ? styles.ZupTypesLook : '')}>
+      <input
+        id='upInput'
+        type='file'
+        accept={
+          fileOneType === 'img'
+            ? '.png,.jpg,.jpeg'
+            : fileOneType === 'audio'
+              ? '.mp3'
+              : fileOneType === 'model'
+                ? '.4dage'
+                : '.mp4'
+        }
+        ref={myInput}
+        onChange={e => handeUpPhoto2(e)}
+      />
+      <div hidden={isTypeShow}>
+        <Checkbox.Group
+          options={typeCheckArr}
+          value={typeCheck}
+          onChange={e => setTypeCheck(e as string[])}
+        />
+      </div>
+
+      {/* -----------模型 */}
+      {resOneDivDom('model')}
+
+      {/* -----------图片 */}
+      <div className='ZTboxImgMain' hidden={!typeCheck.includes('img')}>
+        <div className='ZTboxImgBox'>
+          <div className='ZTbox1' hidden={isTypeShow}>
+            <span> </span> 图片:
+          </div>
+
+          <div className='ZTbox1Img' style={{ width: isTypeShow ? '100%' : '' }}>
+            <div
+              hidden={(!!fileList.img.length && fileList.img.length >= imgLength) || isLook}
+              className='ZTbox1ImgIcon'
+              onClick={() => upFileFu('img')}
+            >
+              <PlusOutlined rev={undefined} />
+            </div>
+            {fileList.img.map((v, i) => (
+              <div
+                className='ZTbox1ImgRow'
+                key={v.id}
+                draggable='true'
+                onDragStart={() => setDragImg(v)}
+                onDragOver={e => handleDragOver(e, v)}
+                onDragEnter={e => handleDragEnter(e, v)}
+                onDragEnd={() => setDragImg(null)}
+              >
+                {v.thumb || v.filePath ? (
+                  <ImageLazy noLook={true} width={100} height={100} src={v.thumb || v.filePath} />
+                ) : null}
+
+                {oneIsCover && i === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
+
+                {/* 修改图片名字 */}
+                {isUpName ? (
+                  <div
+                    title={v.imgName}
+                    className='ZTbox1ImgRowName'
+                    onClick={() => isNameChangeFu(v)}
+                  >
+                    {v.imgName}
+                  </div>
+                ) : null}
+
+                <div className='ZTbox1ImgRowIcon'>
+                  <EyeOutlined
+                    onClick={() =>
+                      store.dispatch({
+                        type: 'layout/lookBigImg',
+                        payload: {
+                          url: baseURL + v.filePath,
+                          show: true
+                        }
+                      })
+                    }
+                    rev={undefined}
+                  />
+                  <a href={baseURL + v.filePath} download target='_blank' rel='noreferrer'>
+                    <DownloadOutlined rev={undefined} />
+                  </a>
+                </div>
+
+                <MyPopconfirm
+                  txtK='删除'
+                  onConfirm={() => delImgListFu(v.id!)}
+                  Dom={<CloseOutlined className='ZTbox1ImgRowX' rev={undefined} />}
+                />
+              </div>
+            ))}
+          </div>
+        </div>
+
+        <div className='ZTboxTit' hidden={isLook}>
+          {fileList.img.length && fileList.img.length >= 2 ? (
+            <>
+              按住鼠标可拖动图片调整顺序。
+              <br />
+            </>
+          ) : null}
+          支持png、jpg的图片格式;最大支持5M;最多支持{imgLength}张。
+          {lastImgTxt}
+        </div>
+      </div>
+
+      {/* -----------音频 */}
+      {resOneDivDom('audio')}
+
+      {/* -----------视频 */}
+      {resOneDivDom('video')}
+
+      {/* 最后的提示 */}
+      <div className={classNames('ZcheckTxt', fileCheck && fileCheckFu ? 'ZcheckTxtAc' : '')}>
+        请最少上传一张图片!
+      </div>
+
+      {/* 点击修改名字出来的弹窗 */}
+      {isNameChange.id ? (
+        <Modal
+          wrapClassName={styles.ZupTypesMo}
+          open={true}
+          title='修改展品图片名称'
+          footer={
+            [] // 设置footer为空,去掉 取消 确定默认按钮
+          }
+        >
+          <br />
+          <div className='ZupTypesMoRow'>
+            <strong>当前名:</strong>
+            {isNameChange.oldName}
+          </div>
+          <div className='ZupTypesMoRow'>
+            <br />
+            <strong>修改为:</strong>
+            <Input
+              style={{ width: 400 }}
+              placeholder='请输入图片名'
+              maxLength={50}
+              showCount
+              value={isNameChange.newName}
+              onChange={e => {
+                setIsNameChange({
+                  ...isNameChange,
+                  newName: e.target.value.replace(/\s+/g, '')
+                })
+              }}
+            />
+          </div>
+
+          <div className='ZupTypesMoBtn'>
+            <Button onClick={isNameChangeXFu}>取消</Button>
+            &emsp;
+            <Button type='primary' onClick={isNameChangeOkFu}>
+              修改
+            </Button>
+          </div>
+        </Modal>
+      ) : null}
+    </div>
+  )
+}
+
+export default forwardRef(ZupTypes)

+ 38 - 0
src/index.tsx

@@ -0,0 +1,38 @@
+// import 'default-passive-events';
+
+import App from './App'
+import store from './store/index'
+
+import { Provider } from 'react-redux'
+import { createRoot } from 'react-dom/client'
+
+import { ConfigProvider } from 'antd'
+
+// 兼容360浏览器
+import { StyleProvider, legacyLogicalPropertiesTransformer } from '@ant-design/cssinjs'
+
+import 'dayjs/locale/zh-cn'
+import locale from 'antd/locale/zh_CN'
+
+const container = document.getElementById('root') as HTMLElement
+const root = createRoot(container)
+
+root.render(
+  <ConfigProvider
+    locale={locale}
+    theme={{
+      token: {
+        colorPrimary: '#042b8f'
+      }
+    }}
+  >
+    <Provider store={store}>
+      <StyleProvider
+        hashPriority='high'
+        transformers={[legacyLogicalPropertiesTransformer]}
+      >
+        <App />
+      </StyleProvider>
+    </Provider>
+  </ConfigProvider>
+)

+ 48 - 0
src/pages/A1home/index.module.scss

@@ -0,0 +1,48 @@
+.A1home {
+  position: relative;
+  background: url('../../assets/img/home_bg.png') no-repeat center center;
+  background-size: 100% 100%;
+  :global {
+    .title {
+      position: absolute;
+      top: 20%;
+      left: 50%;
+      transform: translate(-50%, -25%);
+      width: 60%;
+      height: 200px;
+      background: url('../../assets/img/title.png') no-repeat center center;
+      background-size: 100% 100%;
+    }
+    .btn {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, 410%);
+      width: 187px;
+      height: 72px;
+      background: url('../../assets/img/btn_bg.png') no-repeat center center;
+      background-size: 100% 100%;
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .A1home {
+    background: url('../../assets/img/home_bg_M.png') no-repeat center center;
+    background-size: 100% 100%;
+    :global {
+      .title {
+        top: 30%;
+        width: 90%;
+        height: 100px;
+        background: url('../../assets/img/title.png') no-repeat center center;
+        background-size: 100% 100%;
+      }
+      .btn {
+        width: 135px;
+        height: 50px;
+        transform: translate(-50%, 460%);
+      }
+    }
+  }
+}

+ 21 - 0
src/pages/A1home/index.tsx

@@ -0,0 +1,21 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { useNavigate } from 'react-router-dom'
+
+
+
+function A1home() {
+  const navigate = useNavigate()
+  return (
+    <div className={styles.A1home} >
+      <div className="title"></div>
+      <div className="btn" onClick={() => {
+        navigate('/layout')
+      }}></div>
+    </div>
+  )
+}
+
+const MemoA1home = React.memo(A1home)
+
+export default MemoA1home

+ 61 - 0
src/pages/A2layout/index.module.scss

@@ -0,0 +1,61 @@
+.A2layout {
+  width: 100%;
+  height: 100%;
+  :global {
+    .bg {
+      width: 100%;
+      height: 100%;
+      background-color: #ccc;
+    }
+    .tabBar {
+      position: fixed;
+      bottom: 0;
+      left: 50%;
+      transform: translate(-50%, 0);
+      width: 750px;
+      height: 100px;
+      margin: 0 auto;
+      padding: 0 30px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      .tabBtn {
+        width: 24%;
+        height: 50px;
+        font-size: 18px;
+        font-weight: bold;
+        letter-spacing: 3px;
+        line-height: 46px;
+        text-align: center;
+        color: #831209;
+        cursor: pointer;
+        background: url('../../assets/img/tabBtnBg.png') no-repeat center center;
+        background-size: 100% 100%;
+        &.active {
+          background: url('../../assets/img/tabBtnBg_ac.png') no-repeat center center;
+          background-size: 100% 100%;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .A2layout {
+    :global {
+      .tabBar {
+        width: 100%;
+        gap: 6px;
+        justify-content: center;
+        .tabBtn {
+          height: 70px;
+          padding: 10px;
+          width: 62px;
+          padding: 10px;
+          line-height: 19px;
+          font-size: 17px;
+        }
+      }
+    }
+  }
+}

+ 65 - 0
src/pages/A2layout/index.tsx

@@ -0,0 +1,65 @@
+import React, { useEffect, useState } from 'react'
+import styles from './index.module.scss'
+import Intro from '../components/Intro'
+import Look from '../components/Look'
+import isMobile from '../../utils/isMobile'
+import IntroM from '../components/IntroM'
+import Panoramic from '../components/Panoramic'
+
+import { useNavigate } from 'react-router-dom'
+
+function A2layout() {
+  const [currentActive, setCurrentActive] = useState('')
+  const [currentScene, setCurrentScene] = useState('p1')
+  const navigate = useNavigate()
+  return (
+    <div className={styles.A2layout}>
+      <div className='bg'>
+        <Panoramic currentScene={currentScene} setCurrentScene={setCurrentScene} />
+      </div>
+      <div className='tabBar'>
+        <div
+          className={`tabBtn ${currentActive === 'intro' ? 'active' : ''}`}
+          onClick={() => setCurrentActive('intro')}
+        >
+          展馆简介
+        </div>
+        {/* <div
+          className={`tabBtn ${currentActive === 'pano' ? 'active' : ''}`}
+          onClick={() => {
+            setCurrentActive('pano')
+          }}
+        >
+          三维实景
+        </div> */}
+        <div
+          className={`tabBtn ${currentActive === 'scene' ? 'active' : ''}`}
+          onClick={() => {
+            setCurrentActive('scene')
+            navigate('/scene')
+          }}
+        >
+          展厅漫游
+        </div>
+        <div
+          className={`tabBtn ${currentActive === 'look' ? 'active' : ''}`}
+          onClick={() => setCurrentActive('look')}
+        >
+          馆藏鉴赏
+        </div>
+      </div>
+
+      {currentActive === 'intro' &&
+        (isMobile() ? (
+          <IntroM setCurrentActive={setCurrentActive} />
+        ) : (
+          <Intro setCurrentActive={setCurrentActive} />
+        ))}
+      {currentActive === 'look' && <Look setCurrentActive={setCurrentActive} />}
+    </div>
+  )
+}
+
+const MemoA2layout = React.memo(A2layout)
+
+export default MemoA2layout

+ 134 - 0
src/pages/A3Pano/index.module.scss

@@ -0,0 +1,134 @@
+.A3Pano {
+  width: 100%;
+  height: 100%;
+  background-color: #ccc;
+  :global {
+    .back {
+      position: fixed;
+      top: 20px;
+      left: 20px;
+      width: 70px;
+      height: 70px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      & > img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+    .bottom {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      width: 100%;
+      height: 270px;
+      transition: all 0.3s ease-in-out;
+      .fold {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: end;
+        flex-direction: column;
+        gap: 20px;
+        .foldBtn {
+          height: 55px;
+          width: 110px;
+          cursor: pointer;
+          & > img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+        .foldContainner {
+          width: 100%;
+          height: 190px;
+          background: url('../../assets/img/panoBottom_bg.png') no-repeat center center;
+          background-size: 100% 100%;
+          display: flex;
+          align-items: center;
+          .opt {
+            width: 250px;
+            height: 100%;
+            padding: 50px 30px 15px 30px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            flex-direction: column;
+            font-size: 18px;
+            font-weight: bold;
+            letter-spacing: 3px;
+            text-align: center;
+            line-height: 46px;
+            color: rgba(147, 29, 33, 1);
+            gap: 10px;
+            cursor: pointer;
+            .hangpai {
+              width: 100%;
+              height: 50%;
+              background: url('../../assets/img/lookBtnBg.png') no-repeat center center;
+              background-size: 100% 100%;
+            }
+            .dimian {
+              width: 100%;
+              height: 50%;
+              background: url('../../assets/img/lookBtnBg.png') no-repeat center center;
+              background-size: 100% 100%;
+            }
+            .active {
+              color: rgba(255, 255, 255, 1);
+              background: url('../../assets/img/lookBtnBg_ac.png') no-repeat center center;
+              background-size: 100% 100%;
+            }
+          }
+          .content {
+            width: calc(100% - 250px);
+            height: 100%;
+            padding: 23px 10px 15px 0;
+            overflow: auto;
+            &::-webkit-scrollbar {
+              display: none;
+            }
+            .scroll {
+              width: fit-content;
+              height: 100%;
+              display: flex;
+              align-items: center;
+              justify-content: left;
+              gap: 16px;
+              .panoItem {
+                height: 100%;
+                width: 240px;
+                background-color: #ccc;
+                position: relative;
+                cursor: pointer;
+                & > img {
+                  width: 100%;
+                  height: 100%;
+                  object-fit: cover;
+                }
+                .mood {
+                  position: absolute;
+                  bottom: 0;
+                  left: 0;
+                  height: 20%;
+                  width: 100%;
+                  background-color: rgba(0, 0, 0, 1);
+                }
+              }
+            }
+          }
+        }
+      }
+      & > img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+  }
+}

+ 126 - 0
src/pages/A3Pano/index.tsx

@@ -0,0 +1,126 @@
+import React, { useState, useRef, useEffect } from 'react'
+import styles from './index.module.scss'
+import { useNavigate } from 'react-router-dom'
+
+import Panoramic from '../components/Panoramic'
+
+function A3Pano() {
+  const navigate = useNavigate()
+  const [currentActive, setCurrentActive] = useState<'sky' | 'floor'>('sky')
+  // 切换全景
+  const [currentScene, setCurrentScene] = useState('p1')
+  const [isDragging, setIsDragging] = useState(false)
+  const [isFold, setIsFold] = useState(true)
+  const bottomRef = useRef<HTMLDivElement>(null)
+  const scrollRef = useRef<HTMLDivElement>(null)
+
+  useEffect(() => {
+    const search = new URLSearchParams(window.location.hash.split('?')[1] || '')
+    const sceneName = search.get('name')
+    if (sceneName) {
+      setCurrentScene(sceneName)
+    }
+  }, [])
+
+  // 切换折叠状态
+  const toggleFold = () => {
+    setIsFold(!isFold)
+    if (bottomRef.current) {
+      bottomRef.current.style.transform = !isFold ? 'translateY(0)' : 'translateY(190px)'
+    }
+  }
+
+  const onWheel = (e: React.WheelEvent) => {
+    // 滚动横向滚动
+    if (scrollRef.current) {
+      console.log(e.deltaY)
+      scrollRef.current.scrollLeft += e.deltaY
+    }
+  }
+
+  const handleMouseMove = (e: React.MouseEvent) => {
+    if (!isDragging) return
+    if (scrollRef.current) {
+      scrollRef.current.scrollLeft += e.movementX * -1
+    }
+  }
+
+  useEffect(() => {
+    const handleMouseUp = (e: MouseEvent) => {
+      setIsDragging(false)
+    }
+    const handleMouseDown = (e: MouseEvent) => {
+      const scrollContent = document.getElementById('scroll_content')
+      if (e.target && scrollContent && scrollContent.contains(e.target as Node)) {
+        setIsDragging(true)
+      }
+    }
+    document.addEventListener('mousedown', handleMouseDown)
+    document.addEventListener('mouseup', handleMouseUp)
+    return () => {
+      document.removeEventListener('mousedown', handleMouseDown)
+      document.removeEventListener('mouseup', handleMouseUp)
+    }
+  }, [])
+
+  return (
+    <div className={styles.A3Pano}>
+      <div className='pano'>
+        <Panoramic currentScene={currentScene} setCurrentScene={setCurrentScene} />
+      </div>
+      <div className='back' onClick={() => navigate('/layout')}>
+        <img src={require('@/assets/img/back.png')} alt='' />
+      </div>
+      <div className='bottom' ref={bottomRef}>
+        <div className='fold'>
+          <div className='foldBtn' onClick={toggleFold}>
+            <img
+              src={require(`@/assets/img/${isFold ? 'foldBtn' : 'unfoldBtn'}.png`)}
+              alt=''
+            />
+          </div>
+          <div className='foldContainner'>
+            <div className='opt'>
+              <div
+                className={`hangpai ${currentActive === 'sky' ? 'active' : ''}`}
+                onClick={() => setCurrentActive('sky')}
+              >
+                航拍全景
+              </div>
+              <div
+                className={`dimian ${currentActive === 'floor' ? 'active' : ''}`}
+                onClick={() => setCurrentActive('floor')}
+              >
+                地面全景
+              </div>
+            </div>
+            <div
+              id='scroll_content'
+              className='content'
+              ref={scrollRef}
+              onWheel={onWheel}
+              onMouseMove={handleMouseMove}
+            >
+              <div className='scroll'>
+                {infoTemo.panoramicInfo?.[currentActive]?.map((item, index) => (
+                  <div
+                    className='panoItem'
+                    onClick={() => setCurrentScene(item.sceneName)}
+                    key={index}
+                  >
+                    <img draggable={false} src={item.thumbUrl} alt='' />
+                    <div className='mood'></div>
+                  </div>
+                ))}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const MemoA3Pano = React.memo(A3Pano)
+
+export default MemoA3Pano

+ 40 - 0
src/pages/A4Scene/index.module.scss

@@ -0,0 +1,40 @@
+.A4Scene {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  :global {
+    .back {
+      position: fixed;
+      top: 20px;
+      left: 20px;
+      width: 70px;
+      height: 70px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      & > img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+    iframe {
+      width: 100%;
+      height: 100%;
+      border: none;
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .A4Scene {
+    :global {
+      .back {
+        left: 4px;
+        width: 55px;
+        height: 55px;
+      }
+    }
+  }
+}

+ 16 - 0
src/pages/A4Scene/index.tsx

@@ -0,0 +1,16 @@
+import React from "react";
+import styles from "./index.module.scss";
+import { useNavigate } from 'react-router-dom'
+function A4Scene() {
+  const navigate = useNavigate()
+  return (
+    <div className={styles.A4Scene}>
+      <div className="back" onClick={() => navigate('/layout')}> <img src={require('@/assets/img/back.png')} alt='' /></div>
+      <iframe title='3dScene' src={`https://houseoss.4dkankan.com/project/jining/scene/index.html#/?m=${infoTemo.sceneCode}`}></iframe>
+    </div>
+  )
+}
+
+const MemoA4Scene = React.memo(A4Scene);
+
+export default MemoA4Scene;

+ 33 - 0
src/pages/components/Dot/index.module.scss

@@ -0,0 +1,33 @@
+.dot {
+  pointer-events: auto;
+  width: 260px;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: end;
+  background: url('../../../assets/img/dot_bg.png') no-repeat center center;
+  background-size: 100% 100%;
+  gap: 5px;
+  cursor: pointer;
+  & > img {
+    width: 13%;
+    height: 60%;
+    vertical-align: right;
+    padding-bottom: 5px;
+    align-self: flex-end;
+    object-fit: contain;
+  }
+  :global {
+    .dotName {
+      width: 68%;
+      height: 82%;
+      padding-bottom: 5px;
+      align-self: flex-end;
+      font-size: 22px;
+      font-weight: 500;
+      letter-spacing: 3px;
+      line-height: 45px;
+      color: rgba(255, 255, 255, 1);
+    }
+  }
+}

+ 20 - 0
src/pages/components/Dot/index.tsx

@@ -0,0 +1,20 @@
+import React from 'react'
+import styles from './index.module.scss'
+type DotType = {
+  name?: string
+}
+function Dot({ name = '点位名称' }: DotType) {
+  return (
+    <div
+      className={styles.dot}
+
+    >
+      <img src={require('@/assets/img/dot.png')} alt='' />
+      <div className='dotName'>{name}</div>
+    </div>
+  )
+}
+
+const MemoDot = React.memo(Dot)
+
+export default MemoDot

+ 0 - 0
src/pages/components/Intro/index.module.scss


Някои файлове не бяха показани, защото твърде много файлове са промени