Browse Source

fix: 制作现场图编辑器

bill 2 years ago
parent
commit
1597aa4376
58 changed files with 6671 additions and 1929 deletions
  1. 4 0
      package.json
  2. 2089 1884
      pnpm-lock.yaml
  3. 539 0
      public/css/iconfont/demo.css
  4. 1637 0
      public/css/iconfont/demo_index.html
  5. 267 0
      public/css/iconfont/iconfont.css
  6. 1 0
      public/css/iconfont/iconfont.js
  7. 450 0
      public/css/iconfont/iconfont.json
  8. BIN
      public/css/iconfont/iconfont.ttf
  9. BIN
      public/css/iconfont/iconfont.woff
  10. BIN
      public/css/iconfont/iconfont.woff2
  11. 146 0
      src/api/board.ts
  12. 2 1
      src/api/index.ts
  13. 4 0
      src/assets/svg/arrow.svg
  14. 3 0
      src/assets/svg/brokenLine.svg
  15. 7 0
      src/assets/svg/cigarette.svg
  16. 3 0
      src/assets/svg/circular.svg
  17. 4 0
      src/assets/svg/corpse.svg
  18. 6 0
      src/assets/svg/fingerPrint.svg
  19. 3 0
      src/assets/svg/fireoint.svg
  20. 8 0
      src/assets/svg/footPrint.svg
  21. 8 0
      src/assets/svg/footPrintRever.svg
  22. 4 0
      src/assets/svg/icon.svg
  23. 11 0
      src/assets/svg/n-compass.svg
  24. 3 0
      src/assets/svg/rect.svg
  25. 4 0
      src/assets/svg/shoePrint.svg
  26. 4 0
      src/assets/svg/shoePrintRever.svg
  27. 7 0
      src/assets/svg/table.svg
  28. 4 0
      src/assets/svg/text.svg
  29. 5 0
      src/assets/svg/theBlood.svg
  30. 5 0
      src/constant/api.ts
  31. 3 1
      src/constant/index.ts
  32. 2 1
      src/constant/route.ts
  33. 2 2
      src/constant/scene.ts
  34. 5 0
      src/public.scss
  35. 8 4
      src/router/config.ts
  36. 22 2
      src/router/index.tsx
  37. 10 0
      src/setupProxy.js
  38. 91 0
      src/store/board.ts
  39. 2 0
      src/store/files.ts
  40. 5 2
      src/store/index.tsx
  41. 60 0
      src/utils/base.ts
  42. 170 0
      src/utils/file-serve.ts
  43. 3 1
      src/utils/index.ts
  44. 3 3
      src/utils/route.ts
  45. 48 0
      src/views/draw-file/board/index.d.ts
  46. 43 0
      src/views/draw-file/board/index.js
  47. 74 0
      src/views/draw-file/board/shape.js
  48. 77 0
      src/views/draw-file/header.tsx
  49. 95 0
      src/views/draw-file/index.tsx
  50. 199 0
      src/views/draw-file/modal.tsx
  51. 76 0
      src/views/draw-file/slider.tsx
  52. 175 0
      src/views/draw-file/style.module.scss
  53. 4 9
      src/views/example/index.tsx
  54. 47 0
      src/views/files/columns.tsx
  55. 100 0
      src/views/files/index.tsx
  56. 3 0
      src/views/files/style.module.scss
  57. 116 0
      src/views/files/upload.tsx
  58. 0 19
      src/views/test/index.tsx

+ 4 - 0
package.json

@@ -3,12 +3,14 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@ant-design/icons": "^4.7.0",
     "@craco/craco": "^6.4.5",
     "@reduxjs/toolkit": "^1.8.3",
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^13.3.0",
     "@testing-library/user-event": "^13.5.0",
+    "@types/dom-to-image": "^2.6.4",
     "@types/jest": "^27.5.2",
     "@types/lodash": "^4.14.182",
     "@types/node": "^16.11.45",
@@ -19,6 +21,8 @@
     "canvas-nest.js": "^2.0.4",
     "classnames": "^2.3.1",
     "craco-less": "^2.0.0",
+    "dom-to-image": "^2.6.0",
+    "html2canvas": "^1.4.1",
     "icons": "link:@types/@ant-design/icons",
     "js-base64": "^3.7.2",
     "lodash": "^4.17.21",

File diff suppressed because it is too large
+ 2089 - 1884
pnpm-lock.yaml


+ 539 - 0
public/css/iconfont/demo.css

@@ -0,0 +1,539 @@
+/* Logo 字体 */
+@font-face {
+  font-family: "iconfont logo";
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: "iconfont logo";
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown>p,
+.markdown>blockquote,
+.markdown>.highlight,
+.markdown>ol,
+.markdown>ul {
+  width: 80%;
+}
+
+.markdown ul>li {
+  list-style: circle;
+}
+
+.markdown>ul li,
+.markdown blockquote ul>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown>ul li p,
+.markdown>ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol>li {
+  list-style: decimal;
+}
+
+.markdown>ol li,
+.markdown blockquote ol>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown>table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown>table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown>table th,
+.markdown>table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown>table th {
+  background: #F7F7F7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown>br,
+.markdown>p>br {
+  clear: both;
+}
+
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*="language-"],
+pre[class*="language-"] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection,
+pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection,
+code[class*="language-"] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection,
+pre[class*="language-"] ::selection,
+code[class*="language-"]::selection,
+code[class*="language-"] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+
+  code[class*="language-"],
+  pre[class*="language-"] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+:not(pre)>code[class*="language-"],
+pre[class*="language-"] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre)>code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

File diff suppressed because it is too large
+ 1637 - 0
public/css/iconfont/demo_index.html


+ 267 - 0
public/css/iconfont/iconfont.css

@@ -0,0 +1,267 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 3549513 */
+  src: url('iconfont.woff2?t=1670490368835') format('woff2'),
+       url('iconfont.woff?t=1670490368835') format('woff'),
+       url('iconfont.ttf?t=1670490368835') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-broken_l:before {
+  content: "\e6fd";
+}
+
+.icon-arrows:before {
+  content: "\e6fe";
+}
+
+.icon-blood:before {
+  content: "\e6ff";
+}
+
+.icon-circle:before {
+  content: "\e700";
+}
+
+.icon-cigarette_e:before {
+  content: "\e701";
+}
+
+.icon-corpse:before {
+  content: "\e702";
+}
+
+.icon-icon_n:before {
+  content: "\e703";
+}
+
+.icon-form:before {
+  content: "\e704";
+}
+
+.icon-footprint_l:before {
+  content: "\e705";
+}
+
+.icon-footprint_r:before {
+  content: "\e706";
+}
+
+.icon-fire_p:before {
+  content: "\e707";
+}
+
+.icon-rectangle:before {
+  content: "\e708";
+}
+
+.icon-shoeprints_l:before {
+  content: "\e709";
+}
+
+.icon-text:before {
+  content: "\e70a";
+}
+
+.icon-fingerprint:before {
+  content: "\e70b";
+}
+
+.icon-shoeprints_r:before {
+  content: "\e70c";
+}
+
+.icon-nav-setup:before {
+  content: "\e64b";
+}
+
+.icon-a-film:before {
+  content: "\e6e8";
+}
+
+.icon-nav-edit:before {
+  content: "\e642";
+}
+
+.icon-pic:before {
+  content: "\e648";
+}
+
+.icon-list-scene:before {
+  content: "\e6e4";
+}
+
+.icon-list-file:before {
+  content: "\e6e5";
+}
+
+.icon-list-record:before {
+  content: "\e6e6";
+}
+
+.icon-list-view:before {
+  content: "\e6e7";
+}
+
+.icon-video1:before {
+  content: "\e63b";
+}
+
+.icon-order:before {
+  content: "\e6dd";
+}
+
+.icon-pin1:before {
+  content: "\e6e3";
+}
+
+.icon-nav-measure:before {
+  content: "\e64a";
+}
+
+.icon-v-l:before {
+  content: "\e66f";
+}
+
+.icon-h-r:before {
+  content: "\e670";
+}
+
+.icon-f-l:before {
+  content: "\e673";
+}
+
+.icon-search:before {
+  content: "\e64c";
+}
+
+.icon-left1:before {
+  content: "\e6ae";
+}
+
+.icon-right:before {
+  content: "\e6af";
+}
+
+.icon-state_e:before {
+  content: "\e624";
+}
+
+.icon-state_f:before {
+  content: "\e625";
+}
+
+.icon-state_s:before {
+  content: "\e626";
+}
+
+.icon-eye-n:before {
+  content: "\e621";
+}
+
+.icon-eye-s:before {
+  content: "\e622";
+}
+
+.icon-more:before {
+  content: "\e600";
+}
+
+.icon-element:before {
+  content: "\e666";
+}
+
+.icon-extend:before {
+  content: "\e690";
+}
+
+.icon-shrink:before {
+  content: "\e691";
+}
+
+.icon-pause:before {
+  content: "\e636";
+}
+
+.icon-preview:before {
+  content: "\e63a";
+}
+
+.icon-clear:before {
+  content: "\e63f";
+}
+
+.icon-play_stop:before {
+  content: "\e6b4";
+}
+
+.icon-transparency:before {
+  content: "\e6d7";
+}
+
+.icon-pull-down:before {
+  content: "\e61d";
+}
+
+.icon-pull-up:before {
+  content: "\e61e";
+}
+
+.icon-add:before {
+  content: "\e631";
+}
+
+.icon-close:before {
+  content: "\e633";
+}
+
+.icon-pin:before {
+  content: "\e67c";
+}
+
+.icon-flip:before {
+  content: "\e67e";
+}
+
+.icon-move:before {
+  content: "\e680";
+}
+
+.icon-del:before {
+  content: "\e632";
+}
+
+.icon-checkbox:before {
+  content: "\e649";
+}
+
+.icon-nor:before {
+  content: "\e696";
+}
+
+.icon-joint:before {
+  content: "\e6e0";
+}
+
+.icon-path:before {
+  content: "\e6e1";
+}
+
+.icon-label:before {
+  content: "\e6e2";
+}
+
+.icon-case:before {
+  content: "\e6da";
+}
+
+.icon-scene:before {
+  content: "\e6db";
+}
+

File diff suppressed because it is too large
+ 1 - 0
public/css/iconfont/iconfont.js


+ 450 - 0
public/css/iconfont/iconfont.json

@@ -0,0 +1,450 @@
+{
+  "id": "3549513",
+  "name": "融合平台",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "33292114",
+      "name": "broken_l",
+      "font_class": "broken_l",
+      "unicode": "e6fd",
+      "unicode_decimal": 59133
+    },
+    {
+      "icon_id": "33292115",
+      "name": "arrows",
+      "font_class": "arrows",
+      "unicode": "e6fe",
+      "unicode_decimal": 59134
+    },
+    {
+      "icon_id": "33292116",
+      "name": "blood",
+      "font_class": "blood",
+      "unicode": "e6ff",
+      "unicode_decimal": 59135
+    },
+    {
+      "icon_id": "33292117",
+      "name": "circle",
+      "font_class": "circle",
+      "unicode": "e700",
+      "unicode_decimal": 59136
+    },
+    {
+      "icon_id": "33292118",
+      "name": "cigarette_e",
+      "font_class": "cigarette_e",
+      "unicode": "e701",
+      "unicode_decimal": 59137
+    },
+    {
+      "icon_id": "33292119",
+      "name": "corpse",
+      "font_class": "corpse",
+      "unicode": "e702",
+      "unicode_decimal": 59138
+    },
+    {
+      "icon_id": "33292120",
+      "name": "icon_n",
+      "font_class": "icon_n",
+      "unicode": "e703",
+      "unicode_decimal": 59139
+    },
+    {
+      "icon_id": "33292121",
+      "name": "form",
+      "font_class": "form",
+      "unicode": "e704",
+      "unicode_decimal": 59140
+    },
+    {
+      "icon_id": "33292122",
+      "name": "footprint_l",
+      "font_class": "footprint_l",
+      "unicode": "e705",
+      "unicode_decimal": 59141
+    },
+    {
+      "icon_id": "33292123",
+      "name": "footprint_r",
+      "font_class": "footprint_r",
+      "unicode": "e706",
+      "unicode_decimal": 59142
+    },
+    {
+      "icon_id": "33292124",
+      "name": "fire_p",
+      "font_class": "fire_p",
+      "unicode": "e707",
+      "unicode_decimal": 59143
+    },
+    {
+      "icon_id": "33292125",
+      "name": "rectangle",
+      "font_class": "rectangle",
+      "unicode": "e708",
+      "unicode_decimal": 59144
+    },
+    {
+      "icon_id": "33292126",
+      "name": "shoeprints_l",
+      "font_class": "shoeprints_l",
+      "unicode": "e709",
+      "unicode_decimal": 59145
+    },
+    {
+      "icon_id": "33292127",
+      "name": "text",
+      "font_class": "text",
+      "unicode": "e70a",
+      "unicode_decimal": 59146
+    },
+    {
+      "icon_id": "33292128",
+      "name": "fingerprint",
+      "font_class": "fingerprint",
+      "unicode": "e70b",
+      "unicode_decimal": 59147
+    },
+    {
+      "icon_id": "33292129",
+      "name": "shoeprints_r",
+      "font_class": "shoeprints_r",
+      "unicode": "e70c",
+      "unicode_decimal": 59148
+    },
+    {
+      "icon_id": "25631133",
+      "name": "nav-setup",
+      "font_class": "nav-setup",
+      "unicode": "e64b",
+      "unicode_decimal": 58955
+    },
+    {
+      "icon_id": "31503933",
+      "name": "a-film",
+      "font_class": "a-film",
+      "unicode": "e6e8",
+      "unicode_decimal": 59112
+    },
+    {
+      "icon_id": "25631122",
+      "name": "nav-edit",
+      "font_class": "nav-edit",
+      "unicode": "e642",
+      "unicode_decimal": 58946
+    },
+    {
+      "icon_id": "23786363",
+      "name": "pic",
+      "font_class": "pic",
+      "unicode": "e648",
+      "unicode_decimal": 58952
+    },
+    {
+      "icon_id": "31485449",
+      "name": "list-scene",
+      "font_class": "list-scene",
+      "unicode": "e6e4",
+      "unicode_decimal": 59108
+    },
+    {
+      "icon_id": "31485450",
+      "name": "list-file",
+      "font_class": "list-file",
+      "unicode": "e6e5",
+      "unicode_decimal": 59109
+    },
+    {
+      "icon_id": "31485451",
+      "name": "list-record",
+      "font_class": "list-record",
+      "unicode": "e6e6",
+      "unicode_decimal": 59110
+    },
+    {
+      "icon_id": "31485452",
+      "name": "list-view",
+      "font_class": "list-view",
+      "unicode": "e6e7",
+      "unicode_decimal": 59111
+    },
+    {
+      "icon_id": "23781429",
+      "name": "video",
+      "font_class": "video1",
+      "unicode": "e63b",
+      "unicode_decimal": 58939
+    },
+    {
+      "icon_id": "30948192",
+      "name": "order",
+      "font_class": "order",
+      "unicode": "e6dd",
+      "unicode_decimal": 59101
+    },
+    {
+      "icon_id": "31365069",
+      "name": "pin",
+      "font_class": "pin1",
+      "unicode": "e6e3",
+      "unicode_decimal": 59107
+    },
+    {
+      "icon_id": "25631400",
+      "name": "nav-measure",
+      "font_class": "nav-measure",
+      "unicode": "e64a",
+      "unicode_decimal": 58954
+    },
+    {
+      "icon_id": "26077352",
+      "name": "v-l",
+      "font_class": "v-l",
+      "unicode": "e66f",
+      "unicode_decimal": 58991
+    },
+    {
+      "icon_id": "26077353",
+      "name": "h-r",
+      "font_class": "h-r",
+      "unicode": "e670",
+      "unicode_decimal": 58992
+    },
+    {
+      "icon_id": "26077356",
+      "name": "f-l",
+      "font_class": "f-l",
+      "unicode": "e673",
+      "unicode_decimal": 58995
+    },
+    {
+      "icon_id": "25631464",
+      "name": "search",
+      "font_class": "search",
+      "unicode": "e64c",
+      "unicode_decimal": 58956
+    },
+    {
+      "icon_id": "27765016",
+      "name": "left",
+      "font_class": "left1",
+      "unicode": "e6ae",
+      "unicode_decimal": 59054
+    },
+    {
+      "icon_id": "27765017",
+      "name": "right",
+      "font_class": "right",
+      "unicode": "e6af",
+      "unicode_decimal": 59055
+    },
+    {
+      "icon_id": "22132762",
+      "name": "state_e",
+      "font_class": "state_e",
+      "unicode": "e624",
+      "unicode_decimal": 58916
+    },
+    {
+      "icon_id": "22132763",
+      "name": "state_f",
+      "font_class": "state_f",
+      "unicode": "e625",
+      "unicode_decimal": 58917
+    },
+    {
+      "icon_id": "22132764",
+      "name": "state_s",
+      "font_class": "state_s",
+      "unicode": "e626",
+      "unicode_decimal": 58918
+    },
+    {
+      "icon_id": "22099675",
+      "name": "eye-n",
+      "font_class": "eye-n",
+      "unicode": "e621",
+      "unicode_decimal": 58913
+    },
+    {
+      "icon_id": "22099676",
+      "name": "eye-s",
+      "font_class": "eye-s",
+      "unicode": "e622",
+      "unicode_decimal": 58914
+    },
+    {
+      "icon_id": "11304931",
+      "name": "more read",
+      "font_class": "more",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "25764812",
+      "name": "element",
+      "font_class": "element",
+      "unicode": "e666",
+      "unicode_decimal": 58982
+    },
+    {
+      "icon_id": "27032625",
+      "name": "extend",
+      "font_class": "extend",
+      "unicode": "e690",
+      "unicode_decimal": 59024
+    },
+    {
+      "icon_id": "27032630",
+      "name": "shrink",
+      "font_class": "shrink",
+      "unicode": "e691",
+      "unicode_decimal": 59025
+    },
+    {
+      "icon_id": "23773343",
+      "name": "pause",
+      "font_class": "pause",
+      "unicode": "e636",
+      "unicode_decimal": 58934
+    },
+    {
+      "icon_id": "23773344",
+      "name": "preview",
+      "font_class": "preview",
+      "unicode": "e63a",
+      "unicode_decimal": 58938
+    },
+    {
+      "icon_id": "23781433",
+      "name": "clear",
+      "font_class": "clear",
+      "unicode": "e63f",
+      "unicode_decimal": 58943
+    },
+    {
+      "icon_id": "29255507",
+      "name": "play_stop",
+      "font_class": "play_stop",
+      "unicode": "e6b4",
+      "unicode_decimal": 59060
+    },
+    {
+      "icon_id": "30499411",
+      "name": "transparency",
+      "font_class": "transparency",
+      "unicode": "e6d7",
+      "unicode_decimal": 59095
+    },
+    {
+      "icon_id": "22099518",
+      "name": "pull-down",
+      "font_class": "pull-down",
+      "unicode": "e61d",
+      "unicode_decimal": 58909
+    },
+    {
+      "icon_id": "22099519",
+      "name": "pull-up",
+      "font_class": "pull-up",
+      "unicode": "e61e",
+      "unicode_decimal": 58910
+    },
+    {
+      "icon_id": "23773068",
+      "name": "add",
+      "font_class": "add",
+      "unicode": "e631",
+      "unicode_decimal": 58929
+    },
+    {
+      "icon_id": "23773070",
+      "name": "close",
+      "font_class": "close",
+      "unicode": "e633",
+      "unicode_decimal": 58931
+    },
+    {
+      "icon_id": "26621105",
+      "name": "pin",
+      "font_class": "pin",
+      "unicode": "e67c",
+      "unicode_decimal": 59004
+    },
+    {
+      "icon_id": "26625827",
+      "name": "flip",
+      "font_class": "flip",
+      "unicode": "e67e",
+      "unicode_decimal": 59006
+    },
+    {
+      "icon_id": "26625859",
+      "name": "move",
+      "font_class": "move",
+      "unicode": "e680",
+      "unicode_decimal": 59008
+    },
+    {
+      "icon_id": "23773069",
+      "name": "del",
+      "font_class": "del",
+      "unicode": "e632",
+      "unicode_decimal": 58930
+    },
+    {
+      "icon_id": "23842269",
+      "name": "sel",
+      "font_class": "checkbox",
+      "unicode": "e649",
+      "unicode_decimal": 58953
+    },
+    {
+      "icon_id": "27200779",
+      "name": "nor",
+      "font_class": "nor",
+      "unicode": "e696",
+      "unicode_decimal": 59030
+    },
+    {
+      "icon_id": "31132312",
+      "name": "joint",
+      "font_class": "joint",
+      "unicode": "e6e0",
+      "unicode_decimal": 59104
+    },
+    {
+      "icon_id": "31132318",
+      "name": "path",
+      "font_class": "path",
+      "unicode": "e6e1",
+      "unicode_decimal": 59105
+    },
+    {
+      "icon_id": "31132319",
+      "name": "label",
+      "font_class": "label",
+      "unicode": "e6e2",
+      "unicode_decimal": 59106
+    },
+    {
+      "icon_id": "30885164",
+      "name": "case",
+      "font_class": "case",
+      "unicode": "e6da",
+      "unicode_decimal": 59098
+    },
+    {
+      "icon_id": "30885165",
+      "name": "scene",
+      "font_class": "scene",
+      "unicode": "e6db",
+      "unicode_decimal": 59099
+    }
+  ]
+}

BIN
public/css/iconfont/iconfont.ttf


BIN
public/css/iconfont/iconfont.woff


BIN
public/css/iconfont/iconfont.woff2


+ 146 - 0
src/api/board.ts

@@ -0,0 +1,146 @@
+import axios from './instance'
+import { DELETE_DRAW_FILE, GET_DRAW_FILE, INSERT_DRAW_FILE, UPDATE_DRAW_FILE } from 'constant'
+
+import type { 
+  brokenLine,
+  text,
+  table,
+  rect,
+  circular,
+  arrow,
+  icon,
+  cigarette,
+  fireoint,
+  footPrint,
+  shoePrint,
+  fingerPrint,
+  corpse,
+  theBlood,
+} from 'views/draw-file/board'
+
+export interface Pos { 
+  x: number,
+  y: number
+}
+
+interface Shape {
+  color: string
+}
+
+interface CurrencyShape extends Shape {
+  pos: Pos,
+  width: number,
+  height: number
+}
+
+export interface BrokenLineShape extends Shape {
+  type: typeof brokenLine
+  points: Pos[]
+}
+
+export interface TextShape extends Shape {
+  type: typeof text
+  fontSize: number
+}
+
+export interface TableShape extends CurrencyShape {
+  type: typeof table
+  content: string[][]
+}
+
+export interface RectShape extends CurrencyShape {
+  type: typeof rect
+}
+
+export interface CircularShape extends CurrencyShape {
+  type: typeof circular
+}
+
+export interface ArrowShape extends CurrencyShape {
+  type: typeof arrow
+  direction: number
+}
+
+export interface IconShape extends CurrencyShape {
+  type: typeof icon
+  index: number
+}
+
+export interface CigaretteShape extends CurrencyShape {
+  type: typeof cigarette
+}
+
+export interface FireointShape extends CurrencyShape {
+  type: typeof fireoint
+}
+
+export interface FootPrintShape extends CurrencyShape {
+  type: typeof footPrint
+}
+
+export interface ShoePrintShape extends CurrencyShape {
+  type: typeof shoePrint
+}
+
+export interface FingerPrintShape extends CurrencyShape {
+  type: typeof fingerPrint
+}
+
+export interface CorpseShape extends CurrencyShape {
+  type: typeof corpse
+}
+
+export interface TheBloodShape extends CurrencyShape {
+  type: typeof theBlood
+}
+
+export interface FootPrintShape extends CurrencyShape {
+  type: typeof footPrint
+}
+
+export type BoardShape = BrokenLineShape | TextShape | TableShape | RectShape | CircularShape 
+  | ArrowShape | IconShape | CigaretteShape | FireointShape | FootPrintShape | ShoePrintShape 
+  | FingerPrintShape | CorpseShape | TheBloodShape
+
+
+export interface BoardData {
+  id: number
+  bgImage?: string
+  shapes: BoardShape[]
+}
+
+export enum BoardType {
+  map = '0',
+  scene = '1'
+}
+
+export const BoardTypeDesc = {
+  [BoardType.map]: '方位图',
+  [BoardType.scene]: '现场图',
+}
+
+export const getBoardById = (params: { id: number, type: BoardType }) => {
+  axios.get<BoardData>(GET_DRAW_FILE, { params: params })
+  return {
+    id: params.id,
+    shapes: []
+  }
+}
+
+export const delBoard = (params: { id: number }) => {
+  axios.post<undefined>(DELETE_DRAW_FILE, params)
+}
+  
+
+export const addBoard = (params: { type: BoardType, data: BoardData }) => {
+  axios.post<BoardData>(INSERT_DRAW_FILE, params)
+  return {
+    ...params.data,
+    id: 1,
+  }
+}
+
+export const setBoard = (params: {id: number, data: BoardData}) => {
+  axios.post<undefined>(UPDATE_DRAW_FILE, params)
+  return params.data
+}

+ 2 - 1
src/api/index.ts

@@ -24,4 +24,5 @@ export * from './instance'
 export * from './user'
 export * from './example'
 export * from './files'
-export * from './sys'
+export * from './sys'
+export * from './board'

+ 4 - 0
src/assets/svg/arrow.svg

@@ -0,0 +1,4 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M45 24H6" stroke="black" stroke-width="2"/>
+<path d="M16.3137 13L5.00001 24.3137L16.3137 35.6274" stroke="black" stroke-width="2"/>
+</svg>

+ 3 - 0
src/assets/svg/brokenLine.svg

@@ -0,0 +1,3 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 43H25V5H44" stroke="black" stroke-width="2"/>
+</svg>

+ 7 - 0
src/assets/svg/cigarette.svg

@@ -0,0 +1,7 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M38.6342 4C38.6342 4 41.1545 9.5 39.1546 12.5C37.1547 15.5 39.1546 19 39.1546 19C36.0776 15.923 34.9395 14 37.1547 10.5C39.3699 7 38.6342 4 38.6342 4Z" fill="black"/>
+<path d="M4 29C2.89543 29 2 29.8954 2 31V39C2 40.1046 2.89543 41 4 41H34V29H4Z" fill="black"/>
+<path d="M40.7714 20.5C38.7716 17.5 41.2918 12 41.2918 12C41.2918 12 40.5561 15 42.7713 18.5C44.9866 22 43.8485 23.923 40.7714 27C40.7714 27 42.7713 23.5 40.7714 20.5Z" fill="black"/>
+<path d="M37.5 29C36.6716 29 36 29.6716 36 30.5V39.5C36 40.3284 36.6716 41 37.5 41C38.3284 41 39 40.3284 39 39.5V30.5C39 29.6716 38.3284 29 37.5 29Z" fill="black"/>
+<path d="M42 29C41.1716 29 40.5 29.6716 40.5 30.5V39.5C40.5 40.3284 41.1716 41 42 41C42.8284 41 43.5 40.3284 43.5 39.5V30.5C43.5 29.6716 42.8284 29 42 29Z" fill="black"/>
+</svg>

+ 3 - 0
src/assets/svg/circular.svg

@@ -0,0 +1,3 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="4" y="4" width="40" height="40" rx="20" stroke="black" stroke-width="2"/>
+</svg>

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


File diff suppressed because it is too large
+ 6 - 0
src/assets/svg/fingerPrint.svg


File diff suppressed because it is too large
+ 3 - 0
src/assets/svg/fireoint.svg


+ 8 - 0
src/assets/svg/footPrint.svg

@@ -0,0 +1,8 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M26.5 7C25.1193 7 24 5.88071 24 4.5C24 3.11929 25.1193 2 26.5 2C27.8807 2 29 3.11929 29 4.5C29 5.88071 27.8807 7 26.5 7Z" fill="black"/>
+<path d="M32 12C30.3431 12 29 10.6569 29 9C29 7.34315 30.3431 6 32 6C33.6569 6 35 7.34315 35 9C35 10.6569 33.6569 12 32 12Z" fill="black"/>
+<path d="M18 5C18 6.10457 18.8954 7 20 7C21.1046 7 22 6.10457 22 5C22 3.89543 21.1046 3 20 3C18.8954 3 18 3.89543 18 5Z" fill="black"/>
+<path d="M15 10C13.8954 10 13 9.10457 13 8C13 6.89543 13.8954 6 15 6C16.1046 6 17 6.89543 17 8C17 9.10457 16.1046 10 15 10Z" fill="black"/>
+<path d="M10 12.5C10 13.3284 10.6716 14 11.5 14C12.3284 14 13 13.3284 13 12.5C13 11.6716 12.3284 11 11.5 11C10.6716 11 10 11.6716 10 12.5Z" fill="black"/>
+<path d="M29.0721 31.5068C29.0721 31.5068 28.037 29.0085 29.0721 27.2243C30.1072 25.4402 30.9125 23.8936 31.6392 20.6224C32.3658 17.3511 30.3143 9.73826 22.1998 10.0059C14.0852 10.2735 13.2572 17.4108 13.0088 21.4254C12.7604 25.44 17.8113 38.0193 21.4544 42.1233C25.0975 46.2273 27.6647 46.5838 29.9831 45.424C32.3016 44.2642 33.3778 42.5692 32.881 40.4282C32.3842 38.2873 29.0721 31.5068 29.0721 31.5068Z" fill="black"/>
+</svg>

+ 8 - 0
src/assets/svg/footPrintRever.svg

@@ -0,0 +1,8 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.5 7C19.8807 7 21 5.88071 21 4.5C21 3.11929 19.8807 2 18.5 2C17.1193 2 16 3.11929 16 4.5C16 5.88071 17.1193 7 18.5 7Z" fill="black"/>
+<path d="M13 12C14.6569 12 16 10.6569 16 9C16 7.34315 14.6569 6 13 6C11.3431 6 10 7.34315 10 9C10 10.6569 11.3431 12 13 12Z" fill="black"/>
+<path d="M27 5C27 6.10457 26.1046 7 25 7C23.8954 7 23 6.10457 23 5C23 3.89543 23.8954 3 25 3C26.1046 3 27 3.89543 27 5Z" fill="black"/>
+<path d="M30 10C31.1046 10 32 9.10457 32 8C32 6.89543 31.1046 6 30 6C28.8954 6 28 6.89543 28 8C28 9.10457 28.8954 10 30 10Z" fill="black"/>
+<path d="M35 12.5C35 13.3284 34.3284 14 33.5 14C32.6716 14 32 13.3284 32 12.5C32 11.6716 32.6716 11 33.5 11C34.3284 11 35 11.6716 35 12.5Z" fill="black"/>
+<path d="M15.9279 31.5068C15.9279 31.5068 16.963 29.0085 15.9279 27.2243C14.8928 25.4402 14.0875 23.8936 13.3608 20.6224C12.6342 17.3511 14.6857 9.73826 22.8002 10.0059C30.9148 10.2735 31.7428 17.4108 31.9912 21.4254C32.2396 25.44 27.1887 38.0193 23.5456 42.1233C19.9025 46.2273 17.3353 46.5838 15.0169 45.424C12.6984 44.2642 11.6222 42.5692 12.119 40.4282C12.6158 38.2873 15.9279 31.5068 15.9279 31.5068Z" fill="black"/>
+</svg>

+ 4 - 0
src/assets/svg/icon.svg

@@ -0,0 +1,4 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="4" y="4" width="40" height="40" rx="20" stroke="black" stroke-width="2"/>
+<path d="M20.76 32H28.8V30.48H25.86V17.34H24.46C23.66 17.8 22.72 18.14 21.42 18.38V19.54H24.02V30.48H20.76V32Z" fill="black"/>
+</svg>

File diff suppressed because it is too large
+ 11 - 0
src/assets/svg/n-compass.svg


+ 3 - 0
src/assets/svg/rect.svg

@@ -0,0 +1,3 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="4" y="4" width="40" height="40" stroke="black" stroke-width="2"/>
+</svg>

+ 4 - 0
src/assets/svg/shoePrint.svg

@@ -0,0 +1,4 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M29.9064 30.2635L18.3897 31.999C18.3897 31.999 14 24.0766 14 18.5101C14 9.43774 16.8732 1.75238 22.3803 2.00508C28.3968 2.28117 31 9.00467 31 15.3264C31 18.8919 30.7015 20.0653 29.9867 22.8753C29.7961 23.6244 29.5759 24.4898 29.3239 25.5478C28.9957 26.926 29.9064 30.2635 29.9064 30.2635Z" fill="black"/>
+<path d="M30.5 34L19 35.4383C19 35.4383 19.316 39.7531 19.8057 42.3198C20.2953 44.8865 22.9828 46.465 26.2418 45.8773C29.5007 45.2895 32.4426 43.1091 31.9447 40.287C31.4469 37.4649 30.5 34 30.5 34Z" fill="black"/>
+</svg>

+ 4 - 0
src/assets/svg/shoePrintRever.svg

@@ -0,0 +1,4 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.0936 30.2635L27.6103 31.999C27.6103 31.999 32 24.0766 32 18.5101C32 9.43774 29.1268 1.75238 23.6197 2.00508C17.6032 2.28117 15 9.00467 15 15.3264C15 18.8919 15.2985 20.0653 16.0133 22.8753C16.2039 23.6244 16.4241 24.4898 16.6761 25.5478C17.0043 26.926 16.0936 30.2635 16.0936 30.2635Z" fill="black"/>
+<path d="M15.5 34L27 35.4383C27 35.4383 26.684 39.7531 26.1943 42.3198C25.7047 44.8865 23.0172 46.465 19.7582 45.8773C16.4993 45.2895 13.5574 43.1091 14.0553 40.287C14.5531 37.4649 15.5 34 15.5 34Z" fill="black"/>
+</svg>

+ 7 - 0
src/assets/svg/table.svg

@@ -0,0 +1,7 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="4" y="4" width="40" height="40" rx="4" stroke="black" stroke-width="2"/>
+<path d="M4 14H44" stroke="black" stroke-width="2"/>
+<path d="M4 24H44" stroke="black" stroke-width="2"/>
+<path d="M4 34H44" stroke="black" stroke-width="2"/>
+<path d="M17 4V44" stroke="black" stroke-width="2"/>
+</svg>

+ 4 - 0
src/assets/svg/text.svg

@@ -0,0 +1,4 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M44 4H4" stroke="black" stroke-width="2"/>
+<path d="M24 5V44" stroke="black" stroke-width="2"/>
+</svg>

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


+ 5 - 0
src/constant/api.ts

@@ -41,5 +41,10 @@ export const EXAMPLE_FILE_LIST = `/fusion/caseFiles/allList`
 export const INSERT_EXAMPLE_FILE = `/fusion/caseFiles/add`
 export const DELETE_EXAMPLE_FILE = `/fusion/caseFiles/delete`
 
+export const GET_DRAW_FILE = '/fusion/caseFiles/draw'
+export const INSERT_DRAW_FILE = '/fusion/caseFiles/draw'
+export const DELETE_DRAW_FILE = '/fusion/caseFiles/draw'
+export const UPDATE_DRAW_FILE = '/fusion/caseFiles/draw'
+
 // 上传文件
 export const UPLOAD_FILE = `/fusion/upload/file`

+ 3 - 1
src/constant/index.ts

@@ -2,4 +2,6 @@ export * from './route'
 export * from './scene'
 export * from './thunk'
 export * from './api'
-export * from './sys'
+export * from './sys'
+
+export const DrawType = 1

+ 2 - 1
src/constant/route.ts

@@ -3,7 +3,8 @@ export enum RoutePath {
   home = '/',
   scene = '/scene',
   example = '/example',
-  test = '/test'
+  files = '/files/:id',
+  drawFile = '/draw/:caseId/:type/:id'
 }
 
 export const ViewHome = RoutePath.scene

+ 2 - 2
src/constant/scene.ts

@@ -23,7 +23,7 @@ export const SceneTypeDomain: { [key in SceneType]: string } = {
   [SceneType.SWKK]: window.location.href,
   [SceneType.SWKJ]: window.location.href,
   [SceneType.SWSS]: window.location.href,
-  [SceneType.SWMX]: process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : window.location.href,
+  [SceneType.SWMX]: window.location.href,
   [SceneType.SWSSMX]: window.location.href
 }
 
@@ -31,7 +31,7 @@ export const SceneTypePaths: { [key in SceneType]: string[] } = {
   [SceneType.SWKK]: ['/swkk/spg.html', '/swkk/epg.html'],
   [SceneType.SWKJ]: ['/swkk/spg.html', '/swkk/epg.html'],
   [SceneType.SWSS]: ['/swss/index.html', '/swss/index.html'],
-  [SceneType.SWMX]: process.env.NODE_ENV === 'development' ? ['index.html', 'index.html'] : ['/code/index.html', '/code/index.html'],
+  [SceneType.SWMX]: process.env.NODE_ENV === 'development' ? ['/dev-code/index.html', '/dev-code/index.html'] : ['/code/index.html', '/code/index.html'],
   [SceneType.SWSSMX]: ['/swkk/spg.html', '/swkk/epg.html'],
 }
 

+ 5 - 0
src/public.scss

@@ -37,4 +37,9 @@ html, body, #root {
 .ant-table-tbody > tr > td,
 .ant-table-thead > tr > th {
   text-align: center;
+}
+
+.disabled {
+  opacity: .3;
+  pointer-events: none;
 }

+ 8 - 4
src/router/config.ts

@@ -9,7 +9,7 @@ export type RouteConfig = {
   path:  RoutePath,
   element: () => Promise<{ default: ComponentType<any> }>,
   children?: RoutesConfig,
-  index?: boolean,
+  // index?: boolean,
   meta?: {
     icon: FunctionComponent,
     label: string
@@ -42,12 +42,16 @@ export const routesConfig: RoutesConfig = [
           label: '案件管理',
           icon: ContainerOutlined
         }
+      },
+      {
+        path: RoutePath.files,
+        element: () => import('views/files') as any
       }
     ]
   },
   {
-    path: RoutePath.test,
-    element: () => import('views/test')
+    path: RoutePath.drawFile,
+    element: () => import('views/draw-file')
   }
 ]
 
@@ -78,4 +82,4 @@ export const useRoute = () => {
 
 export { RoutePath }
 export { ViewHome } from 'constant/route'
-export { fillRoutePath } from 'utils'
+export { fillRoutePath, verifiRoutePath } from 'utils'

+ 22 - 2
src/router/index.tsx

@@ -1,7 +1,8 @@
-import { HashRouter as Router, useNavigate, useRoutes } from 'react-router-dom'
+import { HashRouter as Router, useLocation, useNavigate, useRoutes } from 'react-router-dom'
 import { lazy, useEffect } from 'react'
 import { AsyncComponent } from 'components'
 import { routesConfig, useRoute, RoutePath, ViewHome } from './config'
+import { verifiRoutePath } from 'utils'
 import { 
   addReqErrorHandler, 
   addResErrorHandler, 
@@ -64,10 +65,29 @@ export const AppRouter = () => (
   </Router>
 )
 
+export const usePathData = () => {
+  const location = useLocation()
+  const route = useRoute()
+
+  if (route?.path) {
+    let values = verifiRoutePath(location.pathname, route.path) as string[]
+    let names = verifiRoutePath(route.path, route.path) as string[]
+    if (values && names) {
+      names = names.slice(1)
+      values = values.slice(1)
+      return names.reduce((t, name, index) => {
+        t[name.substring(1)] = values[index]
+        return t
+      }, {} as { [key in string]: string })
+    }
+  }
+}
+
 export * from './config'
 export {
   useNavigate,
   useHref,
-  Outlet
+  useLocation,
+  Outlet,
 } from 'react-router-dom'
 export { Router }

+ 10 - 0
src/setupProxy.js

@@ -10,6 +10,16 @@ const proxys = {
     changeOrigin: true,
     pathRewrite: { '^/api': '' }
   },
+  '/fusion': {
+    target: 'https://test-mix3d.4dkankan.com',
+    changeOrigin: true,
+    pathRewrite: { '^/api': '/fusion' }
+  },
+  '/dev-code': {
+    target: 'https://test-mix3d.4dkankan.com/code',
+    changeOrigin: true,
+    pathRewrite: { '^/dev-code': '' }
+  },
   '/swkk': {
     target: config.env === 'dev' ? 'https://test.4dkankan.com' : 'https://www.4dkankan.com',
     changeOrigin: true,

+ 91 - 0
src/store/board.ts

@@ -0,0 +1,91 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+import { getBoardById, setBoard, addBoard } from 'api'
+
+import type { BoardData } from 'api'
+import type { StoreState } from 'store'
+
+export type { BoardData, BoardShape } from 'api'
+export { BoardType, BoardTypeDesc } from 'api'
+
+export interface BoardState {
+  list: BoardData[]
+  currentIndex: number
+}
+
+const initialState: BoardState = { 
+  list: [{ shapes: [], id: -1 }], 
+  currentIndex: 0
+}
+
+const replaceState = (state: BoardState, { payload }: { payload: BoardData }) => {
+  state.list = [payload]
+  state.currentIndex = 0
+}
+const pushState = (state: BoardState, { payload }: { payload: BoardData }) => {
+  const current = state.list[state.currentIndex]
+  const newList = state.list.slice(0, state.currentIndex + 1)
+  newList.push({
+    ...payload,
+    id: current.id,
+  })
+  state.list = newList
+  state.currentIndex += 1
+}
+
+const boardSlice = createSlice({
+  name: 'board',
+  initialState,
+  reducers: {
+    forward(state, { payload = 1 }: { payload: number }) {
+      let move = state.currentIndex + payload
+      state.currentIndex = move > state.list.length - 1 ? state.list.length - 1 : move
+      console.log('forward')
+    },
+    backoff(state, { payload = 1 }: { payload: number }) {
+      let move = state.currentIndex - payload
+      state.currentIndex = move < 0 ? 0 : move
+      console.log('backoff')
+      return state
+    },
+    push: pushState,
+    replace: replaceState,
+    destory(state) {
+      replaceState(state, { payload: initialState.list[0] })
+    }
+  },
+  extraReducers(builder) {
+    builder
+      .addCase(insertBoard.fulfilled, (state, action) => {
+        const current = state.list[state.currentIndex]
+        state.list = state.list.map(data => ({
+          ...data,
+          bgImage: data.bgImage === current.bgImage ? action.payload.bgImage : data.bgImage,
+          id: action.payload.id
+        }))
+      })
+      .addCase(fetchBoard.fulfilled, replaceState)
+  }
+})
+
+export const boardName = boardSlice.name
+export const boardReducer = boardSlice.reducer
+
+export const currentBoard = (state: StoreState) => state.board.list[state.board.currentIndex]
+export const boardStatus = (state: StoreState) => {
+  const len = state.board.list.length
+  const index = state.board.currentIndex
+
+  return {
+    canBack: index !== 0,
+    canForward: index < len - 1,
+    top: index === len - 1
+  }
+}
+
+export const copyBoard = (boardData: BoardData): BoardData => {
+  return JSON.parse(JSON.stringify(boardData))
+}
+
+export const fetchBoard = createAsyncThunk('fetch/board', getBoardById)
+export const insertBoard = createAsyncThunk('insert/board', addBoard)
+export const updateBoard = createAsyncThunk('update/board', setBoard)

+ 2 - 0
src/store/files.ts

@@ -8,6 +8,7 @@ import {
 import type { ExampleFiles, ExampleFileTypes } from 'api'
 import type { StoreState } from './'
 
+export type { ExampleFile, ExampleFiles, ExampleFileType, ExampleFileTypes } from 'api'
 
 export type ExampleFileState = {
   value: ExampleFiles, 
@@ -54,5 +55,6 @@ export const exampleTypeFiles = (state: StoreState) => {
   }))
 }
 
+export { DrawType } from 'constant'
 export const fetchExampleFiles = createAsyncThunk('fetch/example/files', getExampleFiles)
 export const fetchExampleFileTypes = createAsyncThunk('fetch/example/filesTypes', getExampleFileTypes)

+ 5 - 2
src/store/index.tsx

@@ -5,6 +5,7 @@ import { sceneReducer, sceneName } from './scene'
 import { userReducers, userName } from './user'
 import { exampleReducer, exampleName } from './example'
 import { exampleFileName, exampleFileReducer } from './files'
+import { boardName, boardReducer } from './board'
 
 import type { TypedUseSelectorHook } from 'react-redux'
 
@@ -13,7 +14,8 @@ export const store = configureStore({
     [sceneName]: sceneReducer,
     [userName]: userReducers,
     [exampleName]: exampleReducer,
-    [exampleFileName]: exampleFileReducer
+    [exampleFileName]: exampleFileReducer,
+    [boardName]: boardReducer
   }
 })
 
@@ -34,4 +36,5 @@ export default store
 export * from './scene'
 export * from './user'
 export * from './example'
-export * from './files'
+export * from './files'
+export * from './board'

+ 60 - 0
src/utils/base.ts

@@ -1,3 +1,63 @@
 
 export const rawType = (data: any) => Object.prototype.toString.call(data).slice(8, -1)
 export const isObject = (data: any) => rawType(data) === 'Object'
+
+
+// 是否修改
+const _inRevise = (raw1: any, raw2: any, readly: Set<[any, any]>): boolean => {
+  if (raw1 === raw2) return false
+
+  const rawType1 = rawType(raw1)
+  const rawType2 = rawType(raw2)
+
+  if (rawType1 !== rawType2) {
+    return true
+  } else if (
+    rawType1 === 'String' ||
+    rawType1 === 'Number' ||
+    rawType1 === 'Boolean'
+  ) {
+    if (rawType1 === 'Number' && isNaN(raw1) && isNaN(raw2)) {
+      return false
+    } else {
+      return raw1 !== raw2
+    }
+  }
+
+  const rawsArray = Array.from(readly.values())
+  for (const raws of rawsArray) {
+    if (raws.includes(raw1) && raws.includes(raw2)) {
+      return false
+    }
+  }
+  readly.add([raw1, raw2])
+
+  if (rawType1 === 'Array') {
+    return (
+      raw1.length !== raw2.length ||
+      raw1.some((item1: any, i: number) => _inRevise(item1, raw2[i], readly))
+    )
+  } else if (rawType1 === 'Object') {
+    const rawKeys1 = Object.keys(raw1).sort()
+    const rawKeys2 = Object.keys(raw2).sort()
+
+    return (
+      _inRevise(rawKeys1, rawKeys2, readly) ||
+      rawKeys1.some(key => _inRevise(raw1[key], raw2[key], readly))
+    )
+  } else if (rawType1 === 'Map') {
+    const rawKeys1 = Array.from(raw1.keys()).sort()
+    const rawKeys2 = Array.from(raw2.keys()).sort()
+
+    return (
+      _inRevise(rawKeys1, rawKeys2, readly) ||
+      rawKeys1.some(key => _inRevise(raw1.get(key), raw2.get(key), readly))
+    )
+  } else if (rawType1 === 'Set') {
+    return inRevise(Array.from(raw1.values()), Array.from(raw2.values()))
+  } else {
+    return raw1 !== raw2
+  }
+}
+
+export const inRevise = (raw1: any, raw2: any) => _inRevise(raw1, raw2, new Set())

+ 170 - 0
src/utils/file-serve.ts

@@ -0,0 +1,170 @@
+const location = window.location
+
+function bom(blob: Blob, opts: any) {
+  if (typeof opts === 'undefined') opts = { autoBom: false }
+  else if (typeof opts !== 'object') {
+    console.warn('Deprecated: Expected third argument to be a object')
+    opts = { autoBom: !opts }
+  }
+
+  if (
+    opts.autoBom &&
+    /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(
+      blob.type
+    )
+  ) {
+    return new Blob([String.fromCharCode(0xfeff), blob], { type: blob.type })
+  }
+  return blob
+}
+
+function download(url: string, name: string, opts: any): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest()
+    xhr.open('GET', url)
+    xhr.responseType = 'blob'
+    xhr.onload = function () {
+      saveAs(xhr.response, name, opts).then(resolve)
+    }
+    xhr.onerror = function () {
+      reject('could not download file')
+    }
+    xhr.send()
+  })
+}
+
+function corsEnabled(url: string) {
+  const xhr = new XMLHttpRequest()
+  // use sync to avoid popup blocker
+  xhr.open('HEAD', url, false)
+  try {
+    xhr.send()
+  } catch (e) {}
+  return xhr.status >= 200 && xhr.status <= 299
+}
+
+function click(node: any) {
+  return new Promise<void>(resolve => {
+    setTimeout(() => {
+      try {
+        node.dispatchEvent(new MouseEvent('click'))
+      } catch (e) {
+        const evt = document.createEvent('MouseEvents')
+        evt.initMouseEvent(
+          'click',
+          true,
+          true,
+          window,
+          0,
+          0,
+          0,
+          80,
+          20,
+          false,
+          false,
+          false,
+          false,
+          0,
+          null
+        )
+        node.dispatchEvent(evt)
+      }
+      resolve()
+    }, 0)
+  })
+}
+
+const isMacOSWebView =
+  navigator &&
+  /Macintosh/.test(navigator.userAgent) &&
+  /AppleWebKit/.test(navigator.userAgent) &&
+  !/Safari/.test(navigator.userAgent)
+
+type SaveAs = (
+  blob: Blob | string,
+  name?: string,
+  opts?: { autoBom: boolean }
+) => Promise<void>
+
+export const saveAs: SaveAs =
+  'download' in HTMLAnchorElement.prototype && !isMacOSWebView
+    ? (blob, name = 'download', opts) => {
+        const URL = global.URL || global.webkitURL
+        const a = document.createElement('a')
+
+        a.download = name
+        a.rel = 'noopener'
+
+        if (typeof blob === 'string') {
+          a.href = blob
+          if (a.origin !== window.location.origin) {
+            if (corsEnabled(a.href)) {
+              return download(blob, name, opts)
+            }
+            a.target = '_blank'
+          }
+          return click(a)
+        } else {
+          a.href = URL.createObjectURL(blob)
+          setTimeout(function () {
+            URL.revokeObjectURL(a.href)
+          }, 4e4) // 40s
+          return click(a)
+        }
+      }
+    : 'msSaveOrOpenBlob' in navigator
+    ? (blob, name = 'download', opts) => {
+        if (typeof blob === 'string') {
+          if (corsEnabled(blob)) {
+            return download(blob, name, opts)
+          } else {
+            const a = document.createElement('a')
+            a.href = blob
+            a.target = '_blank'
+            return click(a)
+          }
+        } else {
+          return (navigator as any).msSaveOrOpenBlob(bom(blob, opts), name)
+            ? Promise.resolve()
+            : Promise.reject('unknown')
+        }
+      }
+    : (blob, name, opts) => {
+        if (typeof blob === 'string') return download(blob, name as any, opts)
+
+        const force = blob.type === 'application/octet-stream'
+        const isSafari =
+          /constructor/i.test(HTMLElement.toString()) || (global as any).safari
+        const isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
+
+        if (
+          (isChromeIOS || (force && isSafari) || isMacOSWebView) &&
+          typeof FileReader !== 'undefined'
+        ) {
+          return new Promise<void>((resolve, reject) => {
+            const reader = new FileReader()
+            reader.onloadend = function () {
+              let url = reader.result as string
+              url = isChromeIOS
+                ? url
+                : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
+              location.href = url
+              resolve()
+            }
+            reader.onerror = function () {
+              reject()
+            }
+            reader.readAsDataURL(blob)
+          })
+        } else {
+          const URL = global.URL || global.webkitURL
+          const url = URL.createObjectURL(blob)
+          location.href = url
+          setTimeout(function () {
+            URL.revokeObjectURL(url)
+          }, 4e4) // 40s
+          return Promise.resolve()
+        }
+      }
+
+export default saveAs

+ 3 - 1
src/utils/index.ts

@@ -6,6 +6,7 @@ export * from './setState'
 export * from './serve'
 export * from './sys'
 export * from './url'
+export * from './file-serve'
 export * from './only-open'
 
 // 字符串转params对象
@@ -24,4 +25,5 @@ export const strToParams = (str: string) => {
   }
 
   return result
-}
+}
+

+ 3 - 3
src/utils/route.ts

@@ -3,7 +3,7 @@ export const fillRoutePath = <T extends string>(path: T, params: ExtractRoutePar
   let processPath: string = path
 
   for (const [key, value] of Object.entries(params)) {
-    const rg = new RegExp(`:${key}/?`)
+    const rg = new RegExp(`:${key}`)
     processPath = processPath.replace(rg, value as string)
   }
 
@@ -14,7 +14,7 @@ const place = /(?:\/:([^/]*))/g
 export const verifiRoutePath = (pathname: string, path: string) => {
   const rg = path
     .replace(/\/([^:])/g, (_, d) => `\\/${d}`)
-    .replace(place, () => `(?:/[^/]*)`)
-  return new RegExp(`^${rg}$`).test(pathname)
+    .replace(place, () => `(?:/([^/]*))`)
+  return new RegExp(`^${rg}$`).exec(pathname)
 }
 

+ 48 - 0
src/views/draw-file/board/index.d.ts

@@ -0,0 +1,48 @@
+import { metas as fMetas } from './shape'
+import type { BoardData } from 'store'
+import type {  } from 'mime'
+
+type Metas = typeof fMetas
+export type ShapeType = keyof Metas
+export interface Pos {
+  x: number
+  y: number
+}
+
+export type Board = {
+  el: HTMLCanvasElement
+  bus: Emitter<{
+    storeChange: undefined
+  }>
+  setImage(url: string): void
+  addShape(type: ShapeType, pos: Pos): void
+  getPosByScreen(screen: Pos): Pos
+  getCurrentStore(): BoardData
+  drawStore(store: BoardData): void
+  export(): Promise<Blob>
+  destroy(): void
+}
+
+function create (store: BoardData, canvas: HTMLCanvasElement): Board
+
+export const metas: Metas
+export const images: ShapeType[]
+export const labels: ShapeType[]
+export {
+  brokenLine,
+  text,
+  table,
+  rect,
+  circular,
+  arrow,
+  icon,
+  cigarette,
+  fireoint,
+  footPrint,
+  shoePrint,
+  fingerPrint,
+  corpse,
+  theBlood,
+} from './shape'
+
+export default create

+ 43 - 0
src/views/draw-file/board/index.js

@@ -0,0 +1,43 @@
+import mitt from 'mitt'
+
+export const create = (store, canvas) => {
+  const bus = mitt()
+  const board = {
+    el: canvas,
+    bus,
+    getCurrentStore() {
+      return store
+    },
+    drawStore(newStore) {
+      store = newStore
+    },
+    getPosByScreen(screen) {
+      return screen
+    },
+    addShape(shapeType, pos) {
+      store.shapes.push({
+        type: shapeType,
+        pos
+      })
+      bus.emit('storeChange')
+      console.log('添加', shapeType, pos)
+    },
+    setImage(url) {
+      console.log('设置底图', url)
+      store.bgImage = url
+      bus.emit('storeChange')
+    },
+    export() {
+      return new Promise(resolve => canvas.toBlob(resolve))
+    },
+    destroy() {
+      
+    }
+  }
+
+  return board
+}
+
+export * from './shape'
+
+export default create

+ 74 - 0
src/views/draw-file/board/shape.js

@@ -0,0 +1,74 @@
+import brokenLineSVG from 'assets/svg/brokenLine.svg'
+import textSVG from 'assets/svg/text.svg'
+import tableSVG from 'assets/svg/table.svg'
+import rectSVG from 'assets/svg/rect.svg'
+import circularSVG from 'assets/svg/circular.svg'
+import arrowSVG from 'assets/svg/arrow.svg'
+import iconSVG from 'assets/svg/icon.svg'
+import cigaretteSVG from 'assets/svg/cigarette.svg'
+import fireointSVG from 'assets/svg/fireoint.svg'
+import footPrintSVG from 'assets/svg/footPrint.svg'
+import footPrintReverSVG from 'assets/svg/footPrintRever.svg'
+import shoePrintSVG from 'assets/svg/shoePrint.svg'
+import shoePrintReverSVG from 'assets/svg/shoePrintRever.svg'
+import fingerPrintSVG from 'assets/svg/fingerPrint.svg'
+import corpseSVG from 'assets/svg/corpse.svg'
+import theBloodSVG from 'assets/svg/theBlood.svg'
+
+export const brokenLine = 'broken'
+export const text = 'text'
+export const table = 'table'
+export const rect = 'rect'
+export const circular = 'circular'
+export const arrow = 'arrow'
+export const icon = 'icon'
+export const cigarette = 'cigarette'
+export const fireoint = 'fireoint'
+export const footPrint = 'footPrint'
+export const footPrintRever = 'footPrintRever'
+export const shoePrint = 'shoePrint'
+export const shoePrintRever = 'shoePrintRever'
+export const fingerPrint = 'fingerPrint'
+export const corpse = 'corpse'
+export const theBlood = 'theBlood'
+
+export const labels = [
+  brokenLine,
+  text,
+  table,
+  rect,
+  circular,
+  arrow,
+  icon
+]
+
+export const images = [
+  cigarette,
+  fireoint,
+  footPrint,
+  shoePrint,
+  fingerPrint,
+  shoePrintRever,
+  footPrintRever,
+  corpse,
+  theBlood
+]
+
+export const metas = {
+  [brokenLine]: { desc: '折现', icon: brokenLineSVG },
+  [text]: { desc: '文本', icon: textSVG },
+  [table]: { desc: '表格', icon: tableSVG },
+  [rect]: { desc: '矩形', icon: rectSVG },
+  [circular]: { desc: '圆形', icon: circularSVG },
+  [arrow]: { desc: '箭头', icon: arrowSVG },
+  [icon]: { desc: '图标', icon: iconSVG },
+  [cigarette]: { desc: '烟头', icon: cigaretteSVG },
+  [fireoint]: { desc: '起火点', icon: fireointSVG },
+  [footPrint]: { desc: '脚印', icon: footPrintSVG },
+  [footPrintRever]: { desc: '脚印', icon: footPrintReverSVG },
+  [shoePrint]: { desc: '鞋印', icon: shoePrintSVG },
+  [shoePrintRever]: { desc: '鞋印', icon: shoePrintReverSVG },
+  [fingerPrint]: { desc: '指纹', icon: fingerPrintSVG },
+  [corpse]: { desc: '尸体', icon: corpseSVG },
+  [theBlood]: { desc: '血迹', icon: theBloodSVG },
+}

+ 77 - 0
src/views/draw-file/header.tsx

@@ -0,0 +1,77 @@
+import { ArrowLeftOutlined, ArrowRightOutlined, DoubleLeftOutlined } from "@ant-design/icons"
+import { Button } from "antd"
+import { useNavigate, fillRoutePath, RoutePath, usePathData } from "router"
+import { saveAs } from 'utils'
+import style from './style.module.scss'
+import { 
+  useSelector, 
+  boardStatus, 
+  useDispatch, 
+  insertBoard, 
+  updateBoard, 
+  currentBoard, 
+  BoardTypeDesc, 
+  BoardType 
+} from "store"
+
+import type { RefObject } from "react"
+import type { Board } from "./board"
+
+type HeaderProps = {
+  board: RefObject<Board>
+  type: BoardType
+}
+const Header = ({ board, type }: HeaderProps) => {
+  const path = usePathData()
+  const status = useSelector(boardStatus)
+  const current = useSelector(currentBoard)
+  const dispatch = useDispatch()
+  const navigate = useNavigate()
+  const exportPng = async () => {
+    if (board.current) {
+      const blob = await board.current.export()
+      saveAs(blob, '现场图.png')
+    }
+  }
+  const save = async () => {
+    if (current.id === -1) {
+      const data = await dispatch(insertBoard({ type, data: current })).unwrap()
+      navigate(
+        fillRoutePath(RoutePath.drawFile, { ...path as any, id: data.id.toString() }),
+        { replace: true }
+      )
+    } else {
+      dispatch(updateBoard({ id: current.id, data: current }))
+    }
+  }
+
+  return (
+    <>
+      <div className={style['df-header-left']}>
+        <span className={style['df-header-back']} onClick={() => navigate(-1)}>
+          <DoubleLeftOutlined />
+          返回
+        </span>
+        <div className={style['df-header-action']}>
+          <ArrowLeftOutlined 
+            className={!status.canBack ? 'disabled': ''} 
+            onClick={() => dispatch({ type: 'board/backoff' })} 
+          />
+          <ArrowRightOutlined 
+            className={!status.canForward ? 'disabled': ''} 
+            onClick={() => dispatch({ type: 'board/forward' })} 
+          />
+        </div>
+      </div>
+      <h1 className={style['df-header-center']}>
+        创建{ BoardTypeDesc[type] }
+      </h1>
+      <div className={style['df-header-right']}>
+        <Button type="primary" size="large" onClick={exportPng}>导出</Button>
+        <Button type="primary" size="large" onClick={save}>保存</Button>
+      </div>
+    </>
+  )
+}
+
+export default Header

+ 95 - 0
src/views/draw-file/index.tsx

@@ -0,0 +1,95 @@
+import { Layout } from 'antd'
+import { useRef, forwardRef, useImperativeHandle, memo, useCallback, useEffect } from 'react'
+import { usePathData } from 'router'
+import { inRevise } from 'utils'
+import style from './style.module.scss'
+import boardFactory from './board'
+import DfSlider from './slider'
+import DfHeader from './header'
+import { 
+  currentBoard, 
+  useDispatch, 
+  useSelector, 
+  fetchBoard, 
+  copyBoard, 
+  BoardType 
+} from 'store'
+
+import type { Board } from './board'
+
+const { Header, Sider, Content  } = Layout
+
+const DfBoard = memo(forwardRef((_: {}, ref) => {
+  const boardData = useSelector(currentBoard)
+  const board = useRef<Board>()
+  const dispatch = useDispatch()
+
+  const createBoard = useCallback((dom: HTMLCanvasElement | null) => {
+    if (dom) {
+      const boardRef = boardFactory(copyBoard(boardData), dom)
+      boardRef.bus.on('storeChange', () => {
+        dispatch({ 
+          type: 'board/push', 
+          payload: copyBoard(boardRef.getCurrentStore())
+        })
+      })
+      board.current = boardRef
+    } else if (board.current) {
+      board.current.destroy()
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+  useImperativeHandle(ref, () => board.current)
+
+  useEffect(() => {
+    if (board.current && inRevise(board.current.getCurrentStore(), boardData)) {
+      board.current.drawStore(copyBoard(boardData))
+    }
+  }, [boardData, board])
+
+  return (
+    <div className={style['df-board']}>
+      <canvas ref={createBoard}></canvas>
+    </div>
+  )
+}))
+
+export const DrawFile = () => {
+  const path = usePathData()
+  const dispatch = useDispatch()
+  const pathId = Number(path!.id)
+  const caseId = Number(path!.caseId)
+  const pathType = path!.type as BoardType
+  const boardData = useSelector(currentBoard)
+  const board = useRef<Board>(null)
+
+  useEffect(() => {
+    if (pathId !== boardData.id) {
+      if (pathId !== -1) {
+        dispatch(fetchBoard({ id: pathId, type: pathType }))
+      }
+    }
+  }, [pathId, pathType, boardData.id, dispatch])
+
+  useEffect(() => () => {
+    dispatch({ type: 'board/destory' })
+  }, [dispatch])
+
+  return (
+    <Layout className={style['df-layout']}>
+      <Header className={style['df-header']}>
+        <DfHeader board={ board } type={ pathType } />
+      </Header>
+      <Layout>
+        <Sider className={style['df-sider']}>
+          <DfSlider board={ board } type={ pathType } caseId={ caseId } />
+        </Sider>
+        <Content>
+          { pathId === boardData.id && <DfBoard ref={board} /> }
+        </Content>
+      </Layout>
+    </Layout>
+  )
+}
+
+export default DrawFile

+ 199 - 0
src/views/draw-file/modal.tsx

@@ -0,0 +1,199 @@
+import { Input, Modal } from 'antd'
+import { useEffect, useRef, useState } from 'react'
+import AMapLoader from '@amap/amap-jsapi-loader';
+import style from './style.module.scss'
+import html2canvas from 'html2canvas'
+import { SceneType, SceneTypeDomain, SceneTypePaths } from 'constant';
+import { getHref } from 'utils';
+import { asyncLoading } from 'components/loading';
+
+
+const domScreenshot = async (dom: HTMLElement, foreignObjectRendering: boolean) => {
+  const imgs = Array.from(dom.querySelectorAll('img'))
+  try {
+    await Promise.all(
+      imgs.map(img => {
+        if (!img.src) {
+          return null;
+        }
+        const req = fetch(img.src, {
+          method: 'get'
+        })
+        return req
+          .then(res => res.blob())
+          .then(blob => {
+            const render = new FileReader()
+            return new Promise<void>(resolve => {
+              render.onload = e => {
+                if (e.target?.result) {
+                  img.src = e.target?.result as string
+                }
+                resolve()
+              }
+              render.readAsDataURL(blob)
+            })
+          })
+      }) 
+    )
+  } catch {
+  }
+
+  const canvas = await html2canvas(dom, {
+    allowTaint: true,
+    useCORS: true,
+    imageTimeout: 0,
+    removeContainer: false,
+    foreignObjectRendering,
+    width: dom.offsetWidth,
+    height: dom.offsetHeight
+  })
+  return new Promise<Blob | null>(resolve => canvas.toBlob(resolve))
+}
+
+
+type SelectImageProps = {
+  onClose: () => void
+  onSave: (url: Blob) => void
+}
+
+
+let AMap: any
+AMapLoader.load({ 
+  plugins: ['AMap.PlaceSearch'],
+  key: 'e661b00bdf2c44cccf71ef6070ef41b8', 
+  version: '2.0',
+}).then(result => AMap = result)
+
+type MapInfo = { lat: number, lng: number, zoom: number }
+export const SelectMap = (props: SelectImageProps) => {
+  const [open, setOpen] = useState(true)
+  const [info, setInfo] = useState<MapInfo>()
+  const [keyword, setKeyword] = useState('')
+  const mapEle = useRef<HTMLDivElement>(null)
+  const searchResultEle = useRef<HTMLDivElement>(null)
+  const searchAMap = useRef<any>()
+
+  const onSubmit = async () => {
+    if (mapEle.current) {
+      const blob = await domScreenshot(mapEle.current, false)
+      if (blob) {
+        await props.onSave(blob)
+        setOpen(false)
+      }
+    }
+  }
+
+  const renderInfo = info && <div className={style['def-map-info']}>
+    <p><span>经度</span>{ info.lat }</p>
+    <p><span>维度</span>{ info.lng }</p>
+    <p><span>缩放级别</span>{ info.zoom }</p>
+  </div>
+
+  useEffect(() => {
+    if (!mapEle.current) {
+      return;
+    }
+    const map = new AMap.Map(mapEle.current, {
+      WebGLParams: {
+        preserveDrawingBuffer: true
+      },
+      resizeEnable: true
+    })
+    const placeSearch = new AMap.PlaceSearch({
+      pageSize: 5, 
+      pageIndex: 1, 
+      map: map, 
+      panel: searchResultEle.current, 
+      autoFitView: true 
+    });
+    const getMapInfo = (): MapInfo => {
+      var zoom = map.getZoom(); //获取当前地图级别
+      var center = map.getCenter();
+      return {
+        zoom,
+        lat: center.lat,
+        lng: center.lng
+      }
+    }
+    //绑定地图移动与缩放事件
+    map.on('moveend', () => setInfo(getMapInfo()));
+    map.on('zoomend', () => setInfo(getMapInfo()));
+    searchAMap.current = placeSearch
+
+    return () => {
+      searchAMap.current = null
+      map.destroy()
+    }
+  }, [mapEle])
+
+  useEffect(() => {
+    keyword && searchAMap.current?.search(keyword)
+  }, [keyword, searchAMap])
+
+  return (
+    <Modal 
+      width="700px"
+      title="选择地址" 
+      open={open} 
+      onCancel={() => setOpen(false)}
+      onOk={() => asyncLoading(onSubmit())} 
+      afterClose={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <div className={style['search-layout']}>
+        <Input.Search
+          allowClear
+          placeholder="输入名称搜索" 
+          onSearch={setKeyword}
+          style={{ width: 300 }} 
+        />
+        <div 
+          className={`${style['search-result']} ${keyword ? style['show']: ''}`} 
+          ref={searchResultEle} 
+        />
+      </div>
+      <div ref={mapEle} className={style['def-select-map']}></div>
+      { renderInfo }
+    </Modal>
+  )
+}
+
+const getFuseUrl = (caseId: number) =>
+  `${getHref(SceneTypeDomain[SceneType.SWMX]!, SceneTypePaths[SceneType.SWMX][0], { caseId: caseId.toString() })}&share=1#show/summary`
+
+export const SelectFuse = (props: SelectImageProps & {caseId: number}) => {
+  const url = getFuseUrl(props.caseId)
+  const [open, setOpen] = useState(true)
+  const iframeRef = useRef<HTMLIFrameElement>(null)
+
+  const onSubmit = async () => {
+    if (iframeRef.current?.contentWindow) {
+      const fuseBody = iframeRef.current.contentWindow.document.documentElement.querySelector('.scene-canvas')
+      if (fuseBody) {
+        const blob = await domScreenshot(fuseBody as HTMLElement, true)
+        if (blob) {
+          console.log(blob)
+          setOpen(false)
+        }
+      }
+    }
+  }
+
+  return (
+    <Modal 
+      width="700px"
+      title="选择户型图" 
+      open={open} 
+      onCancel={() => setOpen(false)}
+      onOk={() => asyncLoading(onSubmit())} 
+      afterClose={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <div className={style['iframe-layout']}>
+        <iframe src={url} ref={iframeRef} title="fuce-code" />
+      </div>
+    </Modal>
+  )
+}

+ 76 - 0
src/views/draw-file/slider.tsx

@@ -0,0 +1,76 @@
+import { Button } from 'antd'
+import { useEffect, useState } from 'react'
+import { metas, labels, images } from './board'
+import { BoardType, BoardTypeDesc } from 'store'
+import { SelectMap, SelectFuse } from './modal'
+import style from './style.module.scss'
+
+import type { ShapeType, Board } from './board'
+import type { RefObject } from 'react'
+
+type SliderProps = {
+  board: RefObject<Board>
+  type: BoardType
+  caseId: number
+}
+export const DfSlider = ({ board, type, caseId }: SliderProps) => {
+  const [selectMode, setSelectMode] = useState(false)
+  const [currentShape, setCurrentShape] = useState<ShapeType>()
+  const getEle = (shapeType: ShapeType) => (
+    <div 
+      key={shapeType}
+      className={style['df-slide-shape'] + (currentShape === shapeType ? ` ${style['active']}` : '')} 
+      onClick={() => setCurrentShape(shapeType)}
+    >
+      <img src={metas[shapeType].icon} />
+      <p>{metas[shapeType].desc}</p>
+    </div>
+  )
+  const setBoardImage = async (blob: Blob) => {
+    const url = URL.createObjectURL(blob)
+    board.current?.setImage(url)
+  }
+  const SelectImage = type  === BoardType.map ? SelectMap : SelectFuse
+  const renderSelect = selectMode && <SelectImage
+    caseId={caseId}
+    onClose={() => setSelectMode(false)} 
+    onSave={setBoardImage}
+  />
+
+  useEffect(() => {
+    const boardRef = board.current
+    if (currentShape && boardRef) {
+      const clickHandler = (ev: MouseEvent) => {
+        const pos = boardRef.getPosByScreen({ x: ev.offsetX, y: ev.offsetY })
+        boardRef.addShape(currentShape, pos)
+        setCurrentShape(void 0)
+      }
+      const keyupHandler = (ev: KeyboardEvent) => {
+        if (ev.key === 'Escape') {
+          setCurrentShape(void 0)
+        }
+      }
+      boardRef.el.addEventListener('click', clickHandler)
+      document.documentElement.addEventListener('keyup', keyupHandler)
+
+      return () => {
+        boardRef.el.removeEventListener('click', clickHandler)
+        document.documentElement.removeEventListener('keyup', keyupHandler)
+      }
+    }
+  }, [currentShape, board])
+  
+  return (
+    <div className={style['df-slide-content']}>
+      { renderSelect }
+      <h3>户型图</h3>
+      <Button type="primary" block ghost  onClick={() => setSelectMode(true)}>设置{ BoardTypeDesc[type] }</Button>
+      <h3>标注</h3>
+      <div className={style['df-shape-layout']}>{ labels.map(getEle) }</div>
+      <h3>图例</h3>
+      <div className={style['df-shape-layout']}>{ images.map(getEle) }</div>
+    </div>
+  )
+}
+
+export default DfSlider

File diff suppressed because it is too large
+ 175 - 0
src/views/draw-file/style.module.scss


+ 4 - 9
src/views/example/index.tsx

@@ -10,8 +10,8 @@ import { setExample, repExampleScenes, deleteExample, getToken, getExampleScenes
 import { ExampleScenes } from './scene/list'
 import { useState } from 'react'
 import { alert, confirm, getHref, onlyOpenWindow } from 'utils'
-import { ExampleFiles } from './files/list'
 import { SceneType, SceneTypeDomain, SceneTypePaths } from 'constant'
+import { useNavigate, RoutePath, fillRoutePath } from 'router'
 
 import type { ColumnAction } from 'components'
 import type { MenuProps } from 'antd'
@@ -70,9 +70,9 @@ export const ExampleAction = ({ example, query, deleteExample, ...actionCallback
 export const ExamplePage = () => {
   const examples = useSelector(examplesSelector)
   const states = useThunkPaging({ caseTitle: '' }, fetchExamples)
+  const navigate = useNavigate()
   const [[paging, setPaging], [params, setParams], refresh] = states
   const [scenesCaseId, setScenesCaseId] = useState<Example['caseId'] | null>(null)
-  const [fileCaseId, setFileCaseId] = useState<Example['caseId'] | null>(null)
   const [inInsert, setInInsert] = useState(false)
   const getFuseCodeLink = (caseId: Example['caseId'], query?: boolean) => {
     const params: { token?: string, caseId: string } = { 
@@ -86,6 +86,7 @@ export const ExamplePage = () => {
   }
   const checkScenesOpen = async (caseId: Example['caseId'], url: URL | string) => {
     const scenes = await getExampleScenes({ caseId })
+    console.log(url)
     if (!scenes.length) {
       alert('当前案件下无场景,请先添加场景。')
     } else {
@@ -121,7 +122,7 @@ export const ExamplePage = () => {
           }}
           example={record}
           sceneManage={() => setScenesCaseId(record.caseId)}
-          file={() => setFileCaseId(record.caseId)}
+          file={() => navigate(fillRoutePath(RoutePath.files, { id: record.caseId.toString() }))}
           query={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId, true)}&share=1#show/summary`)}
           fuse={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId)}#fuseEdit/merge`)}
           getView={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId)}#sceneEdit/view`)}
@@ -145,17 +146,11 @@ export const ExamplePage = () => {
         onClose={() => setScenesCaseId(null)} 
         onChangeScenes={(idents) => repExampleScenes({ sceneNumParam: idents, caseId: scenesCaseId })}
       />
-  const renderEditFile = fileCaseId
-    && <ExampleFiles
-        caseId={fileCaseId} 
-        onClose={() => setFileCaseId(null)} 
-      />
 
   return (
     <div className='content-layout'>
       { renderAddExample }
       { renderEditScene }
-      { renderEditFile }
       <ExampleHeader 
         onBeforeCreate={() => setInInsert(true)}
         onSearch={setParams}

+ 47 - 0
src/views/files/columns.tsx

@@ -0,0 +1,47 @@
+import { confirm, onlyOpenWindow } from "utils"
+import { ActionsButton } from 'components'
+import { deleteExampleFile } from 'api'
+import { fetchExampleFiles, useDispatch } from "store"
+
+import type { ExampleFile } from "api"
+import type { ColumnsType } from 'antd/es/table'
+
+const OperActions = (data: ExampleFile) => {
+  const dispatch = useDispatch()
+  const actions = [
+    {
+      text: '查看', 
+      action: () => onlyOpenWindow(data.filesUrl), 
+    },
+    {
+      text: '删除', 
+      action: async () => {
+        if (await confirm('确定要删除此数据?')) {
+          await deleteExampleFile(data)
+          dispatch(fetchExampleFiles({ caseId: Number(data.caseId) }))
+        }
+      },
+    },
+  ]
+  return <ActionsButton actions={actions} />
+}
+
+export const fileColumns: ColumnsType<ExampleFile> = [
+  {
+    width: '300px',
+    title: '名称',
+    dataIndex: 'filesTitle',
+    key: 'filesTitle',
+  },
+  {
+    width: '200px',
+    title: '创建时间',
+    dataIndex: 'createTime',
+    key: 'createTime',
+  },
+  {
+    title: '操作',
+    key: 'action',
+    render: (data) => <OperActions {...data} />
+  },
+]

+ 100 - 0
src/views/files/index.tsx

@@ -0,0 +1,100 @@
+import style from './style.module.scss'
+import { useEffect, useState, useMemo } from 'react'
+import { usePathData, useNavigate, RoutePath, fillRoutePath } from 'router'
+import { fileColumns } from './columns'
+import { Tabs, Table } from 'components'
+import { AddExampleFile } from './upload'
+import { Button } from 'antd'
+import { 
+  useDispatch, 
+  useSelector, 
+  fetchExampleFileTypes, 
+  fetchExampleFiles,
+  exampleTypeFiles,
+  DrawType,
+  BoardTypeDesc,
+  BoardType
+} from 'store'
+
+import type { ExampleFileType } from 'store'
+import type { Example } from 'api'
+
+type FileTableProps = {
+  type: ExampleFileType['filesTypeId']
+  caseId: Example['caseId']
+
+}
+export const FileTable = (props: FileTableProps) => {
+  const [insert, setInser] = useState(false)
+  const navigate = useNavigate()
+  const dispatch = useDispatch()
+  const files = useSelector(store => {
+    return exampleTypeFiles(store).find(item => item.filesTypeId === props.type)?.children || []
+  })
+  const renderUpload = insert && <AddExampleFile {...props} onClose={() => setInser(false)} />
+  const renderRraws = props.type === DrawType && Object.keys(BoardTypeDesc).map((type) => (
+    <Button 
+      type="primary" 
+      children={`创建${BoardTypeDesc[type as BoardType]}`}
+      onClick={() => navigate(fillRoutePath(RoutePath.drawFile, { type, caseId: props.caseId.toString(), id: '-1' }))} 
+    />
+  ))
+
+  useEffect(() => {
+    dispatch(fetchExampleFiles({ caseId: props.caseId }))
+  }, [dispatch, props.caseId])
+
+  return (
+    <div className='content-layout'>
+      <div className={`content-header ${style['header']}`}>
+        { renderUpload }
+        { renderRraws }
+        <Button type="primary" children="上传" onClick={() => setInser(true)} />
+      </div>
+      <Table 
+        rowKey='filesId'
+        columns={fileColumns}
+        data={files}
+      />
+    </div>
+  )
+}
+
+export const Files = () => {
+  const data = usePathData()
+  const caseId = data?.id && Number(data.id)
+  const navigate = useNavigate()
+  const dispatch = useDispatch()
+  const typeFiles = useSelector(state => state['example/file'].types)
+  const tabItems = useMemo(
+    () => typeFiles.reduce((t, c) => {
+      t.push([c.filesTypeId, c.filesTypeName])
+      return t
+    }, [] as [number, string][]),
+    [typeFiles]
+  )
+  const [type, setType] = useState<ExampleFileType['filesTypeId']>()
+
+  useEffect(() => {
+    if (caseId) {
+      dispatch(fetchExampleFileTypes())
+    } else {
+      navigate(RoutePath.home)
+    }
+  }, [dispatch, navigate, caseId])
+
+  useEffect(() => {
+    tabItems.length && setType(tabItems[0][0])
+  }, [tabItems])
+
+  return (
+    type && caseId && <Tabs
+      items={tabItems} 
+      active={type} 
+      onChange={type => setType(Number(type))} 
+      renderContent={type => <FileTable type={type} caseId={caseId} />}
+    />
+  )
+}
+
+export default Files

+ 3 - 0
src/views/files/style.module.scss

@@ -0,0 +1,3 @@
+.header >:global(.ant-btn) {
+  margin-left: 10px;
+}

+ 116 - 0
src/views/files/upload.tsx

@@ -0,0 +1,116 @@
+import { useRef } from 'react'
+import { addExampleFile, ExampleFile } from 'api'
+import { UploadOutlined } from '@ant-design/icons'
+import { onlyOpenWindow } from 'utils'
+import { 
+  Modal, 
+  Input, 
+  Button, 
+  Form, 
+  Upload,
+  message,
+} from 'antd'
+import { 
+  useDispatch, 
+  fetchExampleFiles,
+} from 'store'
+
+import type { Example } from "api";
+import type { FormInstance } from 'antd/es/form'
+import type { UploadProps } from 'antd'
+
+const FileTitleInput = (props: { value?: string, onChange?: (value: string) => void;}) => 
+  <Input 
+    onChange={ev => {
+      const value = ev.target.value.trim()
+      if (value.length <= 50) {
+        props.onChange && props.onChange(value)
+      }
+    }} 
+    value={props.value} 
+  />
+
+
+export type AddExampleFileProps = Pick<Example, 'caseId'> & { onClose: () => void, type: ExampleFile['filesTypeId']}
+export const AddExampleFile = (props: AddExampleFileProps) => {
+  const dispatch = useDispatch()
+  const from = useRef<FormInstance | null>(null)
+  const onFinish = async (values: any) => {
+    if (!values.filesUrl.fileList.length) {
+      message.error('附件文件不能为空!')
+      return;
+    }
+    if (values.filesUrl.file?.error?.message) {
+      message.error(values.filesUrl.file?.error?.message)
+      return;
+    }
+    await addExampleFile({
+      ...values,
+      filesUrl: void 0,
+      filesTypeId: props.type,
+      caseId: props.caseId,
+      file: values.filesUrl.file.originFileObj
+    })
+    props.onClose()
+    dispatch(fetchExampleFiles({ caseId: props.caseId }))
+  };
+  const onSubmit = () => {
+    from.current?.submit()
+  }
+  const accpets = [".pdf", ".doc", ".docx", ".jpg"]
+
+  const uploadProps: UploadProps = {
+    async customRequest(option) {
+      const file = option.file as File
+      const filename = file.name
+      const ext = filename.substring(filename.lastIndexOf('.'))
+      const isSuper = accpets.includes(ext)
+      if (!isSuper) {
+        message.error(`只能${accpets.join(',')}上传文件`)
+        option.onError &&  option.onError(new Error(`只能${accpets.join(',')}上传文件`))
+        return 
+      } else if (file.size > 100 * 1024 * 1024) {
+        message.error(`上传文件不能超过100M`)
+        option.onError &&  option.onError(new Error(`上传文件不能超过100M`))
+        return
+      }
+      option.onSuccess &&  option.onSuccess(file.name)
+    },
+    listType: 'picture',
+    multiple: false,
+    accept: accpets.join(","),
+    maxCount: 1,
+    onPreview(file) {
+      onlyOpenWindow(URL.createObjectURL(file.originFileObj!))
+    }
+  };
+
+  return (
+    <Modal 
+      width="500px"
+      title="上传附件" 
+      visible={true} 
+      onOk={onSubmit} 
+      onCancel={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <Form 
+        labelCol={{span: 8}} 
+        wrapperCol={{span: 16}}  
+        name="control-ref" 
+        ref={from} 
+        onFinish={onFinish}
+      >
+        <Form.Item name="filesUrl" label="上传附件" rules={[{ required: true, message: '附件文件不能为空' }]}>
+          <Upload {...uploadProps}>
+            <Button icon={<UploadOutlined />}>请上传{accpets.join('/')}格式文件</Button>
+          </Upload>
+        </Form.Item>
+        <Form.Item name="filesTitle" label="附件标题" rules={[{ required: true, message: '附件标题不能为空' }]} >
+          <FileTitleInput />
+        </Form.Item>
+      </Form>
+    </Modal>
+  )
+}

+ 0 - 19
src/views/test/index.tsx

@@ -1,19 +0,0 @@
-import { useEffect, useState } from "react"
-
-const Test = () => {
-  const [show, setShow] = useState({ current: 1 })
-
-  const aaa = () => {
-    show.current = 2
-    setShow(show)
-  }
-
-  return (
-    <>
-      <button onClick={aaa}>切换</button>
-      { show.current }
-    </>
-  )
-}
-
-export default Test