Browse Source

火调初始化

bill-lai 3 years ago
parent
commit
9624bc02d9
94 changed files with 21408 additions and 0 deletions
  1. 2 0
      .env
  2. 2 0
      .env.fire-test
  3. 23 0
      .gitignore
  4. 5 0
      babel.config.js
  5. 12516 0
      package-lock.json
  6. 56 0
      package.json
  7. BIN
      public/favicon.ico
  8. 18 0
      public/index.html
  9. 13 0
      src/App.vue
  10. 6 0
      src/app.js
  11. 539 0
      src/assets/icon/demo.css
  12. 746 0
      src/assets/icon/demo_index.html
  13. 117 0
      src/assets/icon/iconfont.css
  14. BIN
      src/assets/icon/iconfont.eot
  15. 1 0
      src/assets/icon/iconfont.js
  16. 184 0
      src/assets/icon/iconfont.json
  17. 101 0
      src/assets/icon/iconfont.svg
  18. BIN
      src/assets/icon/iconfont.ttf
  19. BIN
      src/assets/icon/iconfont.woff
  20. BIN
      src/assets/icon/iconfont.woff2
  21. BIN
      src/assets/image/close.png
  22. BIN
      src/assets/image/code.png
  23. BIN
      src/assets/image/decoration_collect@2x.png
  24. BIN
      src/assets/image/edit_type_panorama.png
  25. BIN
      src/assets/image/goto.png
  26. BIN
      src/assets/image/home_bg.png
  27. BIN
      src/assets/image/icon_add.png
  28. BIN
      src/assets/image/img_login_logo.png
  29. BIN
      src/assets/image/img_loginbg.png
  30. BIN
      src/assets/image/list_arrow.png
  31. BIN
      src/assets/image/logo_big.png
  32. BIN
      src/assets/image/menu_vrhouse.png
  33. BIN
      src/assets/image/page_arrow.png
  34. BIN
      src/assets/image/photoview.png
  35. BIN
      src/assets/image/scene.png
  36. BIN
      src/assets/image/scene_error.png
  37. BIN
      src/assets/image/scene_map_3d.png
  38. BIN
      src/assets/image/scene_share.png
  39. BIN
      src/assets/image/top-text.png
  40. BIN
      src/assets/image/top_exit.png
  41. BIN
      src/assets/image/top_my.png
  42. BIN
      src/assets/image/top_set.png
  43. BIN
      src/assets/image/vrmodel-calc.png
  44. BIN
      src/assets/image/vrmodel-err.png
  45. 601 0
      src/assets/style/public.scss
  46. 146 0
      src/components/company-select/index.vue
  47. 59 0
      src/components/dialog/index.vue
  48. 51 0
      src/components/dialog/style.scss
  49. 125 0
      src/components/head/index.vue
  50. 136 0
      src/components/pagination/index.vue
  51. 77 0
      src/components/record/index.vue
  52. 37 0
      src/components/role-select/index.vue
  53. 52 0
      src/components/select.js
  54. 161 0
      src/components/share/index.vue
  55. 16 0
      src/constant/REG.js
  56. 526 0
      src/constant/index.js
  57. 7 0
      src/constant/view.js
  58. 13 0
      src/main.js
  59. 25 0
      src/request/authentication.js
  60. 187 0
      src/request/config.js
  61. 18 0
      src/request/errorMsg.js
  62. 27 0
      src/request/loading.js
  63. 83 0
      src/request/setupAxios.js
  64. 133 0
      src/router/index.js
  65. 69 0
      src/state/navs.js
  66. 246 0
      src/state/tableRef.js
  67. 84 0
      src/state/user.js
  68. 42 0
      src/state/viewAuth.js
  69. 104 0
      src/util/index.js
  70. 185 0
      src/view/camera/index.vue
  71. 254 0
      src/view/dispatch/archives.vue
  72. 789 0
      src/view/dispatch/index.vue
  73. 171 0
      src/view/dispatch/leaveMsg.vue
  74. 38 0
      src/view/home/index.vue
  75. 54 0
      src/view/home/style.scss
  76. 91 0
      src/view/layout/index.vue
  77. 64 0
      src/view/layout/player/index.vue
  78. 27 0
      src/view/layout/player/style.scss
  79. 39 0
      src/view/layout/slide/index.vue
  80. 13 0
      src/view/layout/slide/submenu.vue
  81. 172 0
      src/view/layout/top/index.vue
  82. 91 0
      src/view/layout/top/style.scss
  83. 180 0
      src/view/scene/index.vue
  84. 211 0
      src/view/scene/style.scss
  85. 177 0
      src/view/system/forget.vue
  86. 55 0
      src/view/system/index.vue
  87. 264 0
      src/view/system/login.vue
  88. 201 0
      src/view/system/register.vue
  89. 81 0
      src/view/system/style.scss
  90. 218 0
      src/view/teaching/index.vue
  91. 289 0
      src/view/user/index.vue
  92. 82 0
      src/view/vrmodel/async.vue
  93. 279 0
      src/view/vrmodel/index.vue
  94. 29 0
      vue.config.js

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VUE_APP_DOMAIN=https://www.4dkankan.com
+VUE_APP_PREFIX=

+ 2 - 0
.env.fire-test

@@ -0,0 +1,2 @@
+VUE_APP_PREFIX=
+VUE_APP_DOMAIN=https://test.4dkankan.com

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

File diff suppressed because it is too large
+ 12516 - 0
package-lock.json


+ 56 - 0
package.json

@@ -0,0 +1,56 @@
+{
+  "name": "hd_vr_house_back",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "build-test": "vue-cli-service build --mode fire-test",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "Base64": "^1.1.0",
+    "axios": "^0.21.1",
+    "core-js": "^3.6.5",
+    "element-plus": "^1.0.1-beta.20",
+    "element-ui": "^2.14.1",
+    "qs": "^6.9.4",
+    "vue": "^3.0.0-beta.1",
+    "vue-router": "^4.0.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "^3.0.0-beta.1",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^7.0.0-alpha.0",
+    "less": "^4.0.0",
+    "less-loader": "^7.2.1",
+    "sass": "^1.32.0",
+    "sass-loader": "^10.1.0",
+    "style-resources-loader": "^1.3.2",
+    "vue-cli-plugin-style-resources-loader": "~0.1.4",
+    "vue-cli-plugin-vue-next": "~0.1.4"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/vue3-essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 18 - 0
public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="zh">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>广东省消防救援总队火场三维数据平台 </title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but 房车宝VR看房平台 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <div id="dialog"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 13 - 0
src/App.vue

@@ -0,0 +1,13 @@
+<template>
+  <router-view></router-view>
+</template>
+
+<script>
+import { setApp } from './app'
+
+export default {
+  mounted() {
+    setApp(this)
+  }
+}
+</script>

+ 6 - 0
src/app.js

@@ -0,0 +1,6 @@
+let app = null
+
+export const getApp = () => app
+export const setApp = a => {
+  app = a
+}

+ 539 - 0
src/assets/icon/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;
+}

+ 746 - 0
src/assets/icon/demo_index.html

@@ -0,0 +1,746 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title>IconFont Demo</title>
+  <link rel="shortcut icon" href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico" type="image/x-icon"/>
+  <link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg"/>
+  <link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="iconfont.css">
+  <script src="iconfont.js"></script>
+  <!-- jQuery -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
+  <!-- 代码高亮 -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
+</head>
+<body>
+  <div class="main">
+    <h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">&#xe86b;</a></h1>
+    <div class="nav-tabs">
+      <ul id="tabs" class="dib-box">
+        <li class="dib active"><span>Unicode</span></li>
+        <li class="dib"><span>Font class</span></li>
+        <li class="dib"><span>Symbol</span></li>
+      </ul>
+      
+      <a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2466491" target="_blank" class="nav-more">查看项目</a>
+      
+    </div>
+    <div class="tab-container">
+      <div class="content unicode" style="display: block;">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe695;</span>
+                <div class="name">fire_mic_off</div>
+                <div class="code-name">&amp;#xe695;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe696;</span>
+                <div class="name">fire_cancel</div>
+                <div class="code-name">&amp;#xe696;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe697;</span>
+                <div class="name">fire_pen_open</div>
+                <div class="code-name">&amp;#xe697;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe698;</span>
+                <div class="name">fire_pen_off</div>
+                <div class="code-name">&amp;#xe698;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe699;</span>
+                <div class="name">fire_cross</div>
+                <div class="code-name">&amp;#xe699;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe69a;</span>
+                <div class="name">fire_exit</div>
+                <div class="code-name">&amp;#xe69a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe69b;</span>
+                <div class="name">fire_share</div>
+                <div class="code-name">&amp;#xe69b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe69c;</span>
+                <div class="name">fire_vr</div>
+                <div class="code-name">&amp;#xe69c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe69d;</span>
+                <div class="name">fire_mic_on</div>
+                <div class="code-name">&amp;#xe69d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe69e;</span>
+                <div class="name">fire_appendix</div>
+                <div class="code-name">&amp;#xe69e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68f;</span>
+                <div class="name">fire_119</div>
+                <div class="code-name">&amp;#xe68f;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe690;</span>
+                <div class="name">fire_arrow</div>
+                <div class="code-name">&amp;#xe690;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe691;</span>
+                <div class="name">fire_building</div>
+                <div class="code-name">&amp;#xe691;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe692;</span>
+                <div class="name">fire_other</div>
+                <div class="code-name">&amp;#xe692;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe693;</span>
+                <div class="name">fire_bus</div>
+                <div class="code-name">&amp;#xe693;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe694;</span>
+                <div class="name">fire_recycle</div>
+                <div class="code-name">&amp;#xe694;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe686;</span>
+                <div class="name">fire_user</div>
+                <div class="code-name">&amp;#xe686;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe687;</span>
+                <div class="name">fire_camera</div>
+                <div class="code-name">&amp;#xe687;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe688;</span>
+                <div class="name">log_eye_normal</div>
+                <div class="code-name">&amp;#xe688;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe689;</span>
+                <div class="name">log_eye_selected</div>
+                <div class="code-name">&amp;#xe689;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68a;</span>
+                <div class="name">fire_home</div>
+                <div class="code-name">&amp;#xe68a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68b;</span>
+                <div class="name">fire_annex</div>
+                <div class="code-name">&amp;#xe68b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68c;</span>
+                <div class="name">fire_management </div>
+                <div class="code-name">&amp;#xe68c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68d;</span>
+                <div class="name">fire_study</div>
+                <div class="code-name">&amp;#xe68d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68e;</span>
+                <div class="name">fire_scenes</div>
+                <div class="code-name">&amp;#xe68e;</div>
+              </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="unicode-">Unicode 引用</h2>
+          <hr>
+
+          <p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
+          <ul>
+            <li>兼容性最好,支持 IE6+,及所有现代浏览器。</li>
+            <li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
+            <li>但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。</li>
+          </ul>
+          <blockquote>
+            <p>注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式</p>
+          </blockquote>
+          <p>Unicode 使用步骤如下:</p>
+          <h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
+<pre><code class="language-css"
+>@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.eot');
+  src: url('iconfont.eot?#iefix') format('embedded-opentype'),
+      url('iconfont.woff2') format('woff2'),
+      url('iconfont.woff') format('woff'),
+      url('iconfont.ttf') format('truetype'),
+      url('iconfont.svg#iconfont') format('svg');
+}
+</code></pre>
+          <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
+<pre><code class="language-css"
+>.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
+<pre>
+<code class="language-html"
+>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
+</code></pre>
+          <blockquote>
+            <p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+          </blockquote>
+          </div>
+      </div>
+      <div class="content font-class">
+        <ul class="icon_lists dib-box">
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_mic_off"></span>
+            <div class="name">
+              fire_mic_off
+            </div>
+            <div class="code-name">.iconfire_mic_off
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_cancel"></span>
+            <div class="name">
+              fire_cancel
+            </div>
+            <div class="code-name">.iconfire_cancel
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_pen_on"></span>
+            <div class="name">
+              fire_pen_open
+            </div>
+            <div class="code-name">.iconfire_pen_on
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_pen_off"></span>
+            <div class="name">
+              fire_pen_off
+            </div>
+            <div class="code-name">.iconfire_pen_off
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_cross"></span>
+            <div class="name">
+              fire_cross
+            </div>
+            <div class="code-name">.iconfire_cross
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_exit"></span>
+            <div class="name">
+              fire_exit
+            </div>
+            <div class="code-name">.iconfire_exit
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_share"></span>
+            <div class="name">
+              fire_share
+            </div>
+            <div class="code-name">.iconfire_share
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_vr"></span>
+            <div class="name">
+              fire_vr
+            </div>
+            <div class="code-name">.iconfire_vr
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_mic_on"></span>
+            <div class="name">
+              fire_mic_on
+            </div>
+            <div class="code-name">.iconfire_mic_on
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_appendix"></span>
+            <div class="name">
+              fire_appendix
+            </div>
+            <div class="code-name">.iconfire_appendix
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_119"></span>
+            <div class="name">
+              fire_119
+            </div>
+            <div class="code-name">.iconfire_119
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_arrow"></span>
+            <div class="name">
+              fire_arrow
+            </div>
+            <div class="code-name">.iconfire_arrow
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_building"></span>
+            <div class="name">
+              fire_building
+            </div>
+            <div class="code-name">.iconfire_building
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_other"></span>
+            <div class="name">
+              fire_other
+            </div>
+            <div class="code-name">.iconfire_other
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_bus"></span>
+            <div class="name">
+              fire_bus
+            </div>
+            <div class="code-name">.iconfire_bus
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_recycle"></span>
+            <div class="name">
+              fire_recycle
+            </div>
+            <div class="code-name">.iconfire_recycle
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_user"></span>
+            <div class="name">
+              fire_user
+            </div>
+            <div class="code-name">.iconfire_user
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_camera"></span>
+            <div class="name">
+              fire_camera
+            </div>
+            <div class="code-name">.iconfire_camera
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconlog_eye_normal"></span>
+            <div class="name">
+              log_eye_normal
+            </div>
+            <div class="code-name">.iconlog_eye_normal
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconlog_eye_selected"></span>
+            <div class="name">
+              log_eye_selected
+            </div>
+            <div class="code-name">.iconlog_eye_selected
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_home"></span>
+            <div class="name">
+              fire_home
+            </div>
+            <div class="code-name">.iconfire_home
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_annex"></span>
+            <div class="name">
+              fire_annex
+            </div>
+            <div class="code-name">.iconfire_annex
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_management"></span>
+            <div class="name">
+              fire_management 
+            </div>
+            <div class="code-name">.iconfire_management
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_study"></span>
+            <div class="name">
+              fire_study
+            </div>
+            <div class="code-name">.iconfire_study
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconfire_scenes"></span>
+            <div class="name">
+              fire_scenes
+            </div>
+            <div class="code-name">.iconfire_scenes
+            </div>
+          </li>
+          
+        </ul>
+        <div class="article markdown">
+        <h2 id="font-class-">font-class 引用</h2>
+        <hr>
+
+        <p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
+        <p>与 Unicode 使用方式相比,具有如下特点:</p>
+        <ul>
+          <li>兼容性良好,支持 IE8+,及所有现代浏览器。</li>
+          <li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
+          <li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
+          <li>不过因为本质上还是使用的字体,所以多色图标还是不支持的。</li>
+        </ul>
+        <p>使用步骤如下:</p>
+        <h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
+<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
+</code></pre>
+        <h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;span class="iconfont iconxxx"&gt;&lt;/span&gt;
+</code></pre>
+        <blockquote>
+          <p>"
+            iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+        </blockquote>
+      </div>
+      </div>
+      <div class="content symbol">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_mic_off"></use>
+                </svg>
+                <div class="name">fire_mic_off</div>
+                <div class="code-name">#iconfire_mic_off</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_cancel"></use>
+                </svg>
+                <div class="name">fire_cancel</div>
+                <div class="code-name">#iconfire_cancel</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_pen_on"></use>
+                </svg>
+                <div class="name">fire_pen_open</div>
+                <div class="code-name">#iconfire_pen_on</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_pen_off"></use>
+                </svg>
+                <div class="name">fire_pen_off</div>
+                <div class="code-name">#iconfire_pen_off</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_cross"></use>
+                </svg>
+                <div class="name">fire_cross</div>
+                <div class="code-name">#iconfire_cross</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_exit"></use>
+                </svg>
+                <div class="name">fire_exit</div>
+                <div class="code-name">#iconfire_exit</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_share"></use>
+                </svg>
+                <div class="name">fire_share</div>
+                <div class="code-name">#iconfire_share</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_vr"></use>
+                </svg>
+                <div class="name">fire_vr</div>
+                <div class="code-name">#iconfire_vr</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_mic_on"></use>
+                </svg>
+                <div class="name">fire_mic_on</div>
+                <div class="code-name">#iconfire_mic_on</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_appendix"></use>
+                </svg>
+                <div class="name">fire_appendix</div>
+                <div class="code-name">#iconfire_appendix</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_119"></use>
+                </svg>
+                <div class="name">fire_119</div>
+                <div class="code-name">#iconfire_119</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_arrow"></use>
+                </svg>
+                <div class="name">fire_arrow</div>
+                <div class="code-name">#iconfire_arrow</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_building"></use>
+                </svg>
+                <div class="name">fire_building</div>
+                <div class="code-name">#iconfire_building</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_other"></use>
+                </svg>
+                <div class="name">fire_other</div>
+                <div class="code-name">#iconfire_other</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_bus"></use>
+                </svg>
+                <div class="name">fire_bus</div>
+                <div class="code-name">#iconfire_bus</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_recycle"></use>
+                </svg>
+                <div class="name">fire_recycle</div>
+                <div class="code-name">#iconfire_recycle</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_user"></use>
+                </svg>
+                <div class="name">fire_user</div>
+                <div class="code-name">#iconfire_user</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_camera"></use>
+                </svg>
+                <div class="name">fire_camera</div>
+                <div class="code-name">#iconfire_camera</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconlog_eye_normal"></use>
+                </svg>
+                <div class="name">log_eye_normal</div>
+                <div class="code-name">#iconlog_eye_normal</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconlog_eye_selected"></use>
+                </svg>
+                <div class="name">log_eye_selected</div>
+                <div class="code-name">#iconlog_eye_selected</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_home"></use>
+                </svg>
+                <div class="name">fire_home</div>
+                <div class="code-name">#iconfire_home</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_annex"></use>
+                </svg>
+                <div class="name">fire_annex</div>
+                <div class="code-name">#iconfire_annex</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_management"></use>
+                </svg>
+                <div class="name">fire_management </div>
+                <div class="code-name">#iconfire_management</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_study"></use>
+                </svg>
+                <div class="name">fire_study</div>
+                <div class="code-name">#iconfire_study</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconfire_scenes"></use>
+                </svg>
+                <div class="name">fire_scenes</div>
+                <div class="code-name">#iconfire_scenes</div>
+            </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="symbol-">Symbol 引用</h2>
+          <hr>
+
+          <p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
+            这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
+          <ul>
+            <li>支持多色图标了,不再受单色限制。</li>
+            <li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
+            <li>兼容性较差,支持 IE9+,及现代浏览器。</li>
+            <li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
+          </ul>
+          <p>使用步骤如下:</p>
+          <h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
+<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
+</code></pre>
+          <h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
+<pre><code class="language-html">&lt;style&gt;
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+&lt;/style&gt;
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
+  &lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
+&lt;/svg&gt;
+</code></pre>
+          </div>
+      </div>
+
+    </div>
+  </div>
+  <script>
+  $(document).ready(function () {
+      $('.tab-container .content:first').show()
+
+      $('#tabs li').click(function (e) {
+        var tabContent = $('.tab-container .content')
+        var index = $(this).index()
+
+        if ($(this).hasClass('active')) {
+          return
+        } else {
+          $('#tabs li').removeClass('active')
+          $(this).addClass('active')
+
+          tabContent.hide().eq(index).fadeIn()
+        }
+      })
+    })
+  </script>
+</body>
+</html>

File diff suppressed because it is too large
+ 117 - 0
src/assets/icon/iconfont.css


BIN
src/assets/icon/iconfont.eot


File diff suppressed because it is too large
+ 1 - 0
src/assets/icon/iconfont.js


+ 184 - 0
src/assets/icon/iconfont.json

@@ -0,0 +1,184 @@
+{
+  "id": "2466491",
+  "name": "消防火调VR",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "20808329",
+      "name": "fire_mic_off",
+      "font_class": "fire_mic_off",
+      "unicode": "e695",
+      "unicode_decimal": 59029
+    },
+    {
+      "icon_id": "20808330",
+      "name": "fire_cancel",
+      "font_class": "fire_cancel",
+      "unicode": "e696",
+      "unicode_decimal": 59030
+    },
+    {
+      "icon_id": "20808331",
+      "name": "fire_pen_open",
+      "font_class": "fire_pen_on",
+      "unicode": "e697",
+      "unicode_decimal": 59031
+    },
+    {
+      "icon_id": "20808332",
+      "name": "fire_pen_off",
+      "font_class": "fire_pen_off",
+      "unicode": "e698",
+      "unicode_decimal": 59032
+    },
+    {
+      "icon_id": "20808333",
+      "name": "fire_cross",
+      "font_class": "fire_cross",
+      "unicode": "e699",
+      "unicode_decimal": 59033
+    },
+    {
+      "icon_id": "20808334",
+      "name": "fire_exit",
+      "font_class": "fire_exit",
+      "unicode": "e69a",
+      "unicode_decimal": 59034
+    },
+    {
+      "icon_id": "20808335",
+      "name": "fire_share",
+      "font_class": "fire_share",
+      "unicode": "e69b",
+      "unicode_decimal": 59035
+    },
+    {
+      "icon_id": "20808336",
+      "name": "fire_vr",
+      "font_class": "fire_vr",
+      "unicode": "e69c",
+      "unicode_decimal": 59036
+    },
+    {
+      "icon_id": "20808338",
+      "name": "fire_mic_on",
+      "font_class": "fire_mic_on",
+      "unicode": "e69d",
+      "unicode_decimal": 59037
+    },
+    {
+      "icon_id": "20808339",
+      "name": "fire_appendix",
+      "font_class": "fire_appendix",
+      "unicode": "e69e",
+      "unicode_decimal": 59038
+    },
+    {
+      "icon_id": "20806963",
+      "name": "fire_119",
+      "font_class": "fire_119",
+      "unicode": "e68f",
+      "unicode_decimal": 59023
+    },
+    {
+      "icon_id": "20806964",
+      "name": "fire_arrow",
+      "font_class": "fire_arrow",
+      "unicode": "e690",
+      "unicode_decimal": 59024
+    },
+    {
+      "icon_id": "20806965",
+      "name": "fire_building",
+      "font_class": "fire_building",
+      "unicode": "e691",
+      "unicode_decimal": 59025
+    },
+    {
+      "icon_id": "20806966",
+      "name": "fire_other",
+      "font_class": "fire_other",
+      "unicode": "e692",
+      "unicode_decimal": 59026
+    },
+    {
+      "icon_id": "20806967",
+      "name": "fire_bus",
+      "font_class": "fire_bus",
+      "unicode": "e693",
+      "unicode_decimal": 59027
+    },
+    {
+      "icon_id": "20806968",
+      "name": "fire_recycle",
+      "font_class": "fire_recycle",
+      "unicode": "e694",
+      "unicode_decimal": 59028
+    },
+    {
+      "icon_id": "20805833",
+      "name": "fire_user",
+      "font_class": "fire_user",
+      "unicode": "e686",
+      "unicode_decimal": 59014
+    },
+    {
+      "icon_id": "20805834",
+      "name": "fire_camera",
+      "font_class": "fire_camera",
+      "unicode": "e687",
+      "unicode_decimal": 59015
+    },
+    {
+      "icon_id": "20805835",
+      "name": "log_eye_normal",
+      "font_class": "log_eye_normal",
+      "unicode": "e688",
+      "unicode_decimal": 59016
+    },
+    {
+      "icon_id": "20805836",
+      "name": "log_eye_selected",
+      "font_class": "log_eye_selected",
+      "unicode": "e689",
+      "unicode_decimal": 59017
+    },
+    {
+      "icon_id": "20805837",
+      "name": "fire_home",
+      "font_class": "fire_home",
+      "unicode": "e68a",
+      "unicode_decimal": 59018
+    },
+    {
+      "icon_id": "20805838",
+      "name": "fire_annex",
+      "font_class": "fire_annex",
+      "unicode": "e68b",
+      "unicode_decimal": 59019
+    },
+    {
+      "icon_id": "20805839",
+      "name": "fire_management ",
+      "font_class": "fire_management",
+      "unicode": "e68c",
+      "unicode_decimal": 59020
+    },
+    {
+      "icon_id": "20805840",
+      "name": "fire_study",
+      "font_class": "fire_study",
+      "unicode": "e68d",
+      "unicode_decimal": 59021
+    },
+    {
+      "icon_id": "20805841",
+      "name": "fire_scenes",
+      "font_class": "fire_scenes",
+      "unicode": "e68e",
+      "unicode_decimal": 59022
+    }
+  ]
+}

File diff suppressed because it is too large
+ 101 - 0
src/assets/icon/iconfont.svg


BIN
src/assets/icon/iconfont.ttf


BIN
src/assets/icon/iconfont.woff


BIN
src/assets/icon/iconfont.woff2


BIN
src/assets/image/close.png


BIN
src/assets/image/code.png


BIN
src/assets/image/decoration_collect@2x.png


BIN
src/assets/image/edit_type_panorama.png


BIN
src/assets/image/goto.png


BIN
src/assets/image/home_bg.png


BIN
src/assets/image/icon_add.png


BIN
src/assets/image/img_login_logo.png


BIN
src/assets/image/img_loginbg.png


BIN
src/assets/image/list_arrow.png


BIN
src/assets/image/logo_big.png


BIN
src/assets/image/menu_vrhouse.png


BIN
src/assets/image/page_arrow.png


BIN
src/assets/image/photoview.png


BIN
src/assets/image/scene.png


BIN
src/assets/image/scene_error.png


BIN
src/assets/image/scene_map_3d.png


BIN
src/assets/image/scene_share.png


BIN
src/assets/image/top-text.png


BIN
src/assets/image/top_exit.png


BIN
src/assets/image/top_my.png


BIN
src/assets/image/top_set.png


BIN
src/assets/image/vrmodel-calc.png


BIN
src/assets/image/vrmodel-err.png


+ 601 - 0
src/assets/style/public.scss

@@ -0,0 +1,601 @@
+/* 改变主题色变量 */
+$--color-primary: #26559B;
+
+/* 改变 icon 字体路径变量,必需 */
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+@import "~element-ui/packages/theme-chalk/src/index";
+@import "../icon/iconfont.css";
+
+* {
+  margin: 0;
+  padding: 0;
+}
+
+html {
+  font-size: 14px;
+}
+
+html, body {
+  width: 100%;
+  height: 100%;
+}
+
+body {
+  --primaryColor: #D8000A;
+  --colorColor: #303133;
+  --bgColor: #f0f2f5;
+
+  font-family: 'Microsoft YaHei';
+  color: var(--colorColor);
+}
+
+.fill.el-button {
+  width: 100%;
+}
+
+.slide {
+  .el-menu {
+    border-right: none;
+    background: #fff;
+
+    i {
+      color: inherit;
+      margin-right: 8px;
+    }
+  }
+  
+  .el-menu-item:hover, 
+  .el-menu-item:focus,
+  .el-submenu__title:hover {
+    color: var(--primaryColor) !important;
+    background: #fff !important;
+
+  }
+
+  .el-menu-item.is-active {
+    background: #FFF1F0 !important;
+    color: var(--primaryColor) !important;
+    position: relative;
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      width: 2px;
+      right: 0;
+      background-color: var(--primaryColor);
+    }
+  }
+
+  
+  .el-menu-item,
+  .el-menu-item i {
+    transition: none;
+  }
+
+  .el-submenu__title {
+    background-color: #fff !important;
+  }
+
+  .el-submenu__title,
+  .el-submenu__title i{
+    color: rgb(191,191,191) !important;
+  }
+
+  .el-submenu i {
+    vertical-align: baseline;
+  }
+}
+
+.head-layer {
+  flex: 0 0 auto;
+}
+
+.body-layer {
+  flex: 1;
+  overflow-y: auto;
+  margin-top: 8px;
+  background-color: #fff;
+  border-radius: 4px;;
+  padding: 0 24px;
+  display: flex;
+  flex-direction: column;
+
+  .body-head {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    h3 {
+      font-size: 1.14rem;
+      font-weight: normal;
+      padding: 17px 0;
+      color: var(--colorColor);
+    }
+  }
+
+  > * {
+    flex: 0 0 auto;
+  }
+  > .el-table {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    border: 1px solid #D9D9D9;
+
+    .el-table__header-wrapper {
+      background-color: #fafafa;
+    }
+    > * {
+      flex: 0 0 auto;
+    }
+
+    .el-table__body-wrapper {
+      flex: 1;
+      overflow-y: auto;
+    }
+  }
+}
+
+.el-table th {
+  background: #FAFAFA;
+  .cell {
+    font-size: 0.825rem;
+    color: var(--colorColor);
+    font-weight: normal;
+  }
+}
+
+
+.pag-block {
+  margin-top: 16px;
+  margin-bottom: 16px;
+  .el-pagination {
+    display: flex;
+  }
+
+  &.no-sizes {
+     .el-pagination__total {
+        flex: 1;
+        text-align: left;
+     }
+  }
+  .el-pagination__sizes {
+    flex: 1;
+    text-align: left;
+  }
+
+}
+
+.head-layer .el-tabs__nav-wrap {
+  padding: 0 24px;
+  
+}
+
+.el-tabs__item {
+  font-size: 16px;
+  line-height: 56px;
+  height: 56px;
+}
+
+.el-tabs__nav-wrap::after {
+  height: 1px;
+}
+
+
+.el-button--primary:not(.is-plain,.is-disabled) {
+  
+  background-color: #26559B !important;
+  border-color: #26559B !important;
+}
+.stop-psw {
+  position: absolute;
+  left: -9999999999px;
+  top: -999999999px;
+}
+
+
+.oper-span {
+  color: $--color-primary;
+  font-size: 0.825rem;
+
+  margin: 0 8px;
+
+  &:first-child {
+    margin-left: 0;
+  }
+  &.disable {
+    color: #BFBFBF !important;
+  }
+
+  &:not(.disable) {
+    cursor: pointer;
+  }
+}
+
+.el-dropdown-menu {
+  position: initial;
+  transform: none;
+  margin: 0;
+}
+
+.el-dropdown-menu__item {
+  word-break: keep-all;
+}
+
+.head-content {
+  .el-form {
+    display: grid;
+    grid-gap: 20px;
+    grid-template-columns: repeat(3, 1fr) 160px;
+  }
+  .el-form-item {
+    display: flex;
+    flex: 0 0 auto;
+  }
+  .el-form-item__label {
+    flex: 0 0 auto;
+  }
+  .el-form-item__content {
+    flex: 1;
+  }
+
+  .el-select {
+    display: block;
+  }
+
+  .el-date-editor--daterange.el-input__inner,
+  .el-form-item__content {
+    width: 100%;
+    max-width: 300px;
+  }
+
+  
+  
+
+  .searh-btns {
+    min-width: 153px;
+    grid-area: 1 / 4 / 2 / 5;
+
+    .el-form-item__content {
+      width: 100%;
+      max-width: inherit;
+    }
+  }
+}
+
+
+.mandatory {
+  .el-form-item__label {
+    position: relative;
+
+    &::before {
+      content: '*';
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.el-autocomplete-suggestion,
+.el-select-dropdown {
+  margin-top: -10px;
+}
+
+.info-from {
+  width: 380px;
+  margin: 0 auto;
+}
+
+.loading {
+  i,
+  .el-loading-text {
+    color: inherit;
+  }
+}
+
+/*滚动条整体部分,必须要设置*/
+::-webkit-scrollbar{
+  width: 5px;
+  height: 5px;
+}
+
+
+/*滚动条的滑块按钮*/
+::-webkit-scrollbar-thumb{
+  border-radius: 10px;
+  background: var(--bgColor);
+}
+
+/*滚动条的上下两端的按钮*/
+::-webkit-scrollbar-button{
+  display: none;
+}
+
+
+.el-message-box__message {
+  min-height: 50px;
+  padding: 0 30px;
+}
+
+.el-message-box__title span::before {
+  content: "\e7a3";
+  font-size: 1.5rem;
+  margin-right: 5px;
+  color: #FAAD14;
+  font-family: "element-icons" !important;
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  vertical-align: text-bottom;
+  display: inline-block;
+  -webkit-font-smoothing: antialiased;
+}
+
+.click-row .el-table__body td:not(:first-child) {
+  cursor: pointer;
+}
+
+
+.stop-psw {
+  position: absolute;
+  left: -9999999999px;
+  top: -999999999px;
+}
+
+.el-popper.is-dark {
+  position: absolute;
+  border-radius: 4px;
+  padding: 10px;
+  z-index: 2000;
+  font-size: 12px;
+  line-height: 1.2;
+  min-width: 10px;
+  word-wrap: break-word;
+  background: #303133;
+  color: #fff;
+
+  .el-popper__arrow {
+    
+    position: absolute;
+    display: block;
+    width: 0;
+    height: 0;
+    border-color: transparent;
+    border-style: solid;
+    border-width: 6px;
+    bottom: -6px;
+    border-top-color: #303133;
+    border-bottom-width: 0;
+  }
+}
+
+.el-popper:not(.el-cascader__dropdown,.el-picker__popper,.el-dropdown__popper)  {
+  max-width: 500px;
+  background: #EFEFEF;
+  box-shadow: 0px 2px 4px 0px #e4e4e4;
+  border-radius: 2px;
+  border: 1px solid #D7D7D7;
+  padding: 2px 6px;
+  word-break: break-all;
+}
+// .is-light.el-popper{
+//   max-width: 500px;
+//   background: #EFEFEF;
+//   box-shadow: 0px 2px 4px 0px #e4e4e4;
+//   border-radius: 2px;
+//   border: 1px solid #D7D7D7;
+//   padding: 2px 6px;
+// }
+
+.oper-link {
+  cursor: pointer;
+
+  &:hover {
+    color: #3366FF;
+  }
+}
+
+.el-table::before {
+  background: none;
+}
+.el-popper__arrow {
+  border-width: 6px;
+  filter: drop-shadow(0 2px 12px rgba(0,0,0,.03));
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+  
+  margin-right: 3px;
+  border-top-width: 0;
+  border-bottom-color: #fff;
+  top: -5px;
+  // pointer-events: none;
+}
+
+.el-popper__arrow::after {
+  top: 1px;
+  margin-left: -6px;
+  border-top-width: 0;
+  border-bottom-color: #fff;
+  content: " ";
+  border-width: 6px;
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+.el-date-editor--daterange {
+  position: relative;
+  .el-range-separator {
+    display: none;
+  }
+
+  &::after {
+    content: "\e6d2";
+    font-size: 14px;
+    color: #303133;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%) rotate(90deg);
+    margin-left: -10px;
+    font-family: element-icons!important;
+    speak: none;
+    font-style: normal;
+    font-weight: 400;
+    font-variant: normal;
+    text-transform: none;
+    line-height: 1;
+    vertical-align: baseline;
+  }
+}
+
+.el-input {
+  position: relative;
+  .input-inner-btn {
+    position: absolute;
+    right: 0;
+    top: 50%;
+    height: 36px;
+    line-height: 36px;
+    font-size: 14px;
+    padding: 0 15px;
+    text-align: center;
+    transform: translateY(-50%);
+
+    background: rgba(201, 29, 40, 0.05);
+    border-color: #C91D28;
+    color: #C91D28;
+  }
+
+  .el-button--primary.is-plain:hover, .el-button--primary.is-plain:focus {
+    background: #C91D28;
+    border-color: #C91D28;
+    color: #FFFFFF;
+  }
+}
+
+.head-content .el-date-editor--daterange.el-input__inner {
+  min-width: 260px;
+}
+
+.el-message-box__header {
+  padding: 34px;
+  padding-bottom: 0;
+}
+
+.el-message-box__headerbtn .el-message-box__close {
+  display: none;
+}
+
+.el-message-box__title {
+  color: #000;
+  font-size: 16px;
+}
+
+.el-message-box__content {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: rgba(0, 0, 0, 0.65);
+}
+
+.el-message-box {
+  padding-bottom: 24px;
+}
+
+.el-message-box__content {
+  padding: 12px 32px 24px 62px;
+}
+
+.el-message-box__message {
+  padding: 0;
+}
+
+.el-message-box__btns {
+  padding: 0 16px;
+}
+
+.el-button--primary.is-plain:focus {
+  background: #FFFFFF;
+  border-color: #26559B;
+  color: #26559B;
+
+}
+.el-button--primary.is-plain:hover {
+  background: #26559B;
+  border-color: #26559B;
+  color: #FFFFFF;
+}
+.el-date-editor--date {
+
+  &.el-input--prefix .el-input__inner{
+    padding-left: 15px;
+  }
+
+  .el-input__prefix {
+    position: absolute;
+    height: 100%;
+    right: 5px;
+    top: 0;
+    left: inherit;
+    text-align: center;
+    color: #C0C4CC;
+    transition: all 0.3s;
+    pointer-events: none;
+
+  }
+}
+
+.el-date-editor--daterange {
+  padding-right: 28px !important;
+
+  input {
+    width: 50% !important;
+  }
+}
+.el-date-editor--daterange .el-range__icon {
+  position: absolute;
+  right: 5px;
+}
+
+.el-date-editor--daterange .el-range__close-icon {
+  display: none;
+}
+
+
+.el-upload-list__item-name .el-icon-document{
+  display: none;
+}
+
+.el-upload-list__item-name {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #323233;
+  line-height: 20px;
+  margin-right: 20px;
+}
+
+
+.el-upload-list__item .el-icon-close {
+  display: inherit;
+}
+
+.el-upload-list__item .el-upload-list__item-status-label {
+  display: none !important;
+}
+
+.el-table th .cell .el-checkbox {
+  margin-left: 4px;
+}

+ 146 - 0
src/components/company-select/index.vue

@@ -0,0 +1,146 @@
+<template>
+  <!-- <el-select v-model="value" placeholder="全部">
+  </el-select> -->
+  
+    <el-cascader
+      style="width: 100%"
+      v-model="threeValue"
+      :disabled="disabled"
+      placeholder="承办单位:"
+      :options="threeOptions"
+      :props="{ expandTrigger: 'hover', checkStrictly: true }">
+    
+      <template v-slot:default="{ data }">
+        <span :class="{aaa: notUpdate}">{{data.label}}</span>
+      </template>
+      
+    </el-cascader>
+</template>
+
+<script>
+import selectComponentJs from '../select'
+import { getCompanyList } from '@/request/config'
+import axios from 'axios'
+import { computed, ref } from 'vue'
+
+const extObj = selectComponentJs()
+
+const analyData = (data) => {
+  return data.map(citem => {
+    let item = {label: citem.name, value: citem.id, level: citem.level}
+    if (citem.childList.length) {
+      item.children = analyData(citem.childList)
+    }
+    return item
+  })
+}
+
+const findData = (data, id, ret = []) => {
+  for (let item of data) {
+    let aret = [...ret, item.value]
+    if (item.value === id) {
+      return aret
+    } else if (item.children) {
+      let temp = findData(item.children, id, aret)
+      if (temp) {
+        return temp
+      }
+    }
+  }
+  return false
+}
+const findDataItem = (data, id) => {
+  for (let item of data) {
+    if (item.value === id) {
+      return item
+    } else if (item.children) {
+      let temp = findDataItem(item.children, id)
+      if (temp) {
+        return temp
+      }
+    }
+  }
+  return false
+}
+
+const findItemSelect = (data, id) => {
+  for (var i = 0; i < data.length; i++) {
+    let item = data[i]
+    if (item.value === id) {
+      delete item.children
+      break
+    } else if (item.children) {
+      let citem = findItemSelect(item.children, id)
+      if (citem) {
+        item.children = citem
+        
+        break
+      }
+    }
+
+    data.splice(i--, 1)
+  }
+
+  if (i === data.length) {
+    return false
+  } else {
+    data = [data[i]]
+    return data
+  }
+  
+}
+
+extObj.watch = {
+  ...extObj.watch,
+  threeValue() {
+    this.$emit('update:data', findDataItem(this.threeOptions, this.value))
+  }
+}
+
+
+export default {
+  ...extObj,
+  props: [...extObj.props, 'notUpdate'],
+  setup(...props) {
+    let ret = extObj.setup(...props)
+    let threeOptions = ref([])
+    let threeValue = computed({
+      get() {
+        return findData(threeOptions.value, ret.value.value)
+      },
+      set(val) {
+        ret.value.value = val[val.length - 1]
+      }
+    })
+
+    return {...ret, threeOptions, threeValue}
+  },
+  async mounted() {
+    let res = await axios.post(getCompanyList)
+    let coms = Array.isArray(res.data) ? res.data : [res.data]
+
+    if (this.showAll) {
+      this.threeOptions = [{label: '全部', value: ''}].concat(analyData(coms))
+    } else if (this.notUpdate){
+      console.log(findItemSelect(analyData(coms), this.value))
+      this.threeOptions = findItemSelect(analyData(coms), this.value)
+      // this.threeOptions = []
+    } else {
+      this.threeOptions = analyData(coms)
+    }
+    this.options = coms.map(item => ({...item, id: item.id.toString(), sort: item.sort }))
+    extObj.mounted.call(this)
+  }
+}
+</script>
+
+<style scoped>
+.aaa::after {
+  content: '';
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+</style>

+ 59 - 0
src/components/dialog/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <teleport to="#dialog">
+    <div class="dialog-bg" v-if="dialogVisible">
+      <div class="dialog" :style="{width: (width ? width : 680) + 'px'}">
+        <div class="head">
+          <h3>{{title}} </h3>  
+          <i class="el-icon-close" @click="closeHandle" v-if="showClose || showClose === void 0"></i>
+        </div>
+        <div class="content">
+          <slot />
+        </div>
+        <div class="floot" v-if="showFloor">
+          <el-button @click="closeHandle" v-if="showClose || showClose === void 0">取 消</el-button>
+          <el-button type="primary" @click="enterHandle">{{enterText || '保 存'}}</el-button>
+        </div>
+      </div>
+    </div>
+  </teleport>
+</template>
+
+<script>
+import { computed, ref } from "vue";
+
+export default {
+  props: ["show", "title", "hideFloor", 'enterText', 'width', 'showClose'],
+  setup(props) {
+    const dialogVisible = ref(props.show);
+    const showFloor = computed(() => !(props.hideFloor || props.hideFloor === ''))
+
+    return { dialogVisible, showFloor };
+  },
+  watch: {
+    dialogVisible() {
+      if (this.show !== this.dialogVisible) {
+        this.$emit("update:show", this.dialogVisible);
+      }
+    },
+    show() {
+      if (this.show !== this.dialogVisible) {
+        this.dialogVisible = this.show;
+      }
+    }
+  },
+  methods: {
+    closeHandle() {
+      this.$emit('quit')
+      this.dialogVisible = false;
+    },
+    enterHandle() {
+      this.$emit('submit')
+      // this.dialogVisible = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./style.scss";
+</style>

+ 51 - 0
src/components/dialog/style.scss

@@ -0,0 +1,51 @@
+.dialog-bg {
+  background-color: rgba(0,0,0,0.5);
+  position: fixed;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 99;
+
+}
+
+.dialog {
+  $padding: 16px;
+  width: 680px;
+  background-color: #fff;
+  border-radius: 4px;
+
+  .head {
+    padding: $padding;
+    display: flex;
+    justify-content: space-between;
+
+    h3 {
+      font-size: 16px;
+      color: var(--colorColor);
+      font-weight: normal;
+    }
+
+    i {
+      font-size: 2rem;
+      color: rgb(145,148,154);
+      cursor: pointer;
+    }
+  }
+
+  .content {
+    padding: 24px 30px;
+    border-top: 1px solid var(--bgColor);
+    border-bottom: 1px solid var(--bgColor);
+    max-height: 600px;
+    overflow-y: auto;
+  }
+
+  .floot {
+    float: right;
+    padding: $padding;
+  }
+}

+ 125 - 0
src/components/head/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="head-layer">
+    <el-tabs v-model="activeValue">
+      <el-tab-pane 
+        v-for="item in options" 
+        :key="item.value"
+        :label="item.name" 
+        :name="item.value" >
+      </el-tab-pane>
+    </el-tabs>
+    <div class="head-content-layer" :class="{show: show}" v-if="notContent === undefined">
+      <div class="head-content">
+        <slot />
+      </div>
+      <div class="display" @click="show = !show" v-if="showCtrl !== undefined">
+        <template v-if="show">
+          <span>收起</span>
+          <i class="el-icon-arrow-up"></i>
+        </template>
+        <template v-else>
+          <span>展开</span>
+          <i class="el-icon-arrow-down"></i>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref, computed } from 'vue'
+
+export default {
+  props: ['options', 'modelValue', 'showCtrl', 'notContent'],
+  setup(props) {
+    let currItem = props.options.find(({value}) => value === props.modelValue) || props.options[0]
+
+    let active = ref(currItem)
+    let show = ref(true)
+    let activeValue = computed({
+      get: () => active.value.value,
+      set: val => {
+        if (val !== active.value.value) {
+          active.value = props.options.find(({value}) => Number(value) === Number(val))
+        }
+      }
+    })
+
+    return { active, activeValue, show }
+  },
+  watch: {
+    active() {
+      if (this.modelValue !== this.active.value) {
+        this.$emit('update:modelValue', this.active.value)
+      }
+    },
+    modelValue(newVal) {
+      this.active = this.options.find(({value}) => value === newVal)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.head-layer {
+  background-color: #fff;
+  border-radius: 4px;
+  // padding-top: 8px;
+}
+
+.head-content-layer {
+  padding: 16px 154px 16px 16px;
+  position: relative;
+
+  .head-content {
+    overflow: hidden;
+    height: 42px;
+  }
+  
+  
+  &.show {
+    padding-bottom: 16px;
+
+    .head-content {
+      height: auto;
+    }
+  }
+
+  .display {
+    position: absolute;
+    top: 16px;
+    right: 16px;
+    color: #3366FF;
+    font-size: 0.85rem;
+    cursor: pointer;
+
+    i {
+      color: currentColor;
+      font-size: 1em;
+      margin-left: 5px;
+    }
+  }
+}
+</style>
+
+<style>
+.head-layer .el-tabs__nav-wrap {
+  padding: 0 16px;
+}
+
+.head-layer .el-tabs__header {
+  margin-bottom: 0;
+}
+
+.head-layer .el-form-item {
+  margin-bottom: 0;
+}
+
+.head-layer .el-tabs__item.is-active {
+  color: #303133;
+}
+
+.head-layer .el-tabs__active-bar {
+  display: none;
+}
+</style>

+ 136 - 0
src/components/pagination/index.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="pag-block no-sizes">
+    <el-pagination
+      layout="total, prev, pager, next"
+      @current-change="data => $emit('current-change', data)"
+      :current-page="currPage"
+      :page-size="currSize"
+      background
+      :total="total"
+    >
+    </el-pagination>
+
+    <div class="sizes-down" :style="{left: 40 + total.toString().length * 10 + 'px'}" v-if="!hideSizes">
+      <span class="el-dropdown-link" @click.stop="show = !show">
+        {{currSize}} 条/页
+        <i :class="show ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
+      </span>
+      <ul @click.stop v-if="show">
+        <li
+          v-for="num in sizes"
+          :key="num"
+          :class="{selected: num === currSize}"
+          @click="clickHandle(num)">
+          {{num}}条/页
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref, watch } from 'vue'
+export default {
+  props: ['currPage', 'size', 'total', 'hideSizes', 'csizes'],
+  setup(props, content) {
+    let sizes = props.csizes ? ref(props.csizes):ref([12, 24]) 
+    let currSize = ref(props.size || sizes.value[0])
+    let show = ref(false)
+
+    watch(currSize, () => content.emit('size-change', currSize))
+    
+    return { sizes, currSize, show }
+  },
+  methods: {
+    clickHandle(num) {
+      this.currSize = num
+      this.show = false
+    }
+  },
+  mounted() {
+    this.docClick = () => this.show = false
+    document.documentElement.addEventListener('click', this.docClick, false)
+  },
+  unmounted() {
+    document.documentElement.removeEventListener('click', this.docClick, false)
+  }
+}
+</script>
+
+<style lang="scss" scope>
+.pag-block {
+  position: relative;
+  z-index: 88;
+    
+  .sizes-down {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+
+    span {
+      box-sizing: border-box;
+      font-size: 13px;
+      appearance: none;
+      color: rgb(96, 98, 102);
+      display: inline-block;
+      height: 30px;
+      line-height: 30px;
+      min-width: 100px;
+      border: 1px solid rgb(220, 223, 230);
+      border-radius: 4px;
+      outline: none;
+      padding: 0px 35px 0 15px;
+      cursor: pointer;
+
+      i {
+        position: absolute;
+        right: 15px;
+        top: 50%;
+        transform: translateY(-50%);
+        color: inherit;
+      }
+    }
+
+    ul {
+      width: 100%;
+      bottom: 100%;
+
+      position: absolute;
+      z-index: 1001;
+      border: solid 1px #E4E7ED;
+      border-radius: 4px;
+      background-color: #FFFFFF;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+      box-sizing: border-box;
+      margin: 5px 0;
+      list-style: none;
+      padding: 6px 0;
+      margin: 0;
+      box-sizing: border-box;
+
+      li {
+        font-size: 14px;
+        padding: 0 20px;
+        position: relative;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        color: #606266;
+        height: 34px;
+        line-height: 34px;
+        box-sizing: border-box;
+        cursor: pointer;
+
+        &:hover {
+          background-color: #F5F7FA;
+        }
+
+        &.selected {
+          color: #D6000F;
+          font-weight: bold;
+        }
+      }
+    }
+  }
+}
+</style>

+ 77 - 0
src/components/record/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <com-dialog
+    v-if="show"
+    title="审核记录"
+    :show="show"
+    hideFloor
+    @update:show="val => $emit('update:show', val)">
+    
+    <div style="width: 100%; overflow: hidden">
+      <el-table ref="multipleTable" :data="dataList.state" style="width: 100%" >
+        <el-table-column label="审核人" prop="auditorName"></el-table-column>
+        <el-table-column label="审核时间" prop="auditTime"></el-table-column>
+        <el-table-column label="审核结果" prop="auditStatus"  v-slot:default="{ row }">
+          <span>{{getExamineName(row.auditStatus)}}</span>
+        </el-table-column>
+        <el-table-column label="审核备注" prop="auditRemark"  v-slot:default="{ row }">
+          <div class="remark">
+            {{row.auditRemark}}
+          </div>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <div class="pag-block no-sizes">
+      <el-pagination
+        @current-change="pag.currentChange"
+        :current-page="pag.state.currPage"
+        :page-size="pag.state.size"
+        layout="total, prev, pager, next, jumper"
+        :total="pag.state.total">
+      </el-pagination>
+    </div>
+
+  </com-dialog>
+</template>
+
+<script>
+import getTableState from '@/state/tableRef'
+import comDialog from "@/components/dialog";
+import { getExamineList } from '@/request/config'
+import { 
+  EXAMINE_VIEW_SELECT,
+  getName 
+} from '@/constant'
+
+export default {
+  props: ['show', 'data'],
+  setup(props) {
+    const state = getTableState({
+      getUrl: getExamineList,
+      searchAttr: { houseId: props.data }
+    })
+
+    state.pag.value.sizeChange(8)
+
+    return { ...state }
+  },
+  methods: {
+    getExamineName: getName(EXAMINE_VIEW_SELECT),
+  },
+  watch: {
+    data() {
+      this.search.state.houseId = this.props.data
+    }
+  },
+  components: {
+    "com-dialog": comDialog
+  }  
+}
+</script>
+
+<style lang="scss" scoped>
+.remark {
+  max-height: 100px;
+  overflow-y: auto;
+}
+</style>

+ 37 - 0
src/components/role-select/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <el-select v-model="value" :placeholder="allText ? allText : '全部'" :disabled="disabled">
+    <el-option :label="allText ? allText : '全部'" :value="''" v-if="showAll"></el-option>
+    <el-option v-for="item in filterOptions" :key="item.id"  :label="item.roleName" :value="item.id"></el-option>
+  </el-select>
+</template>
+
+<script>
+import selectComponentJs from '../select'
+import { getRoleList } from '@/request/config'
+import axios from 'axios'
+
+const extObj = selectComponentJs()
+
+extObj.props = [...extObj.props, 'maxLeave', 'disabled']
+
+extObj.computed = {
+  ...extObj.computed,
+  filterOptions() {
+    if (!this.maxLeave) {
+      return this.options
+    } else {
+      return this.options.filter(({id}) => Number(id) >= Number(this.maxLeave))
+    }
+  }
+}
+
+export default {
+  ...extObj,
+
+  async mounted() {
+    let res = await axios.post(getRoleList, {})
+    this.options = res.data.map(item => ({...item, id: item.id.toString()}))
+    extObj.mounted.call(this)
+  }
+}
+</script>

+ 52 - 0
src/components/select.js

@@ -0,0 +1,52 @@
+
+import { ref, watch } from 'vue'
+
+export default () => {
+  let cache = []
+
+  return {
+    props: ['modelValue', 'label', 'hideAll', 'allText', 'notDefault', 'disabled'],
+    setup(props) {
+      let options = ref(cache)
+      let value = ref(props.modelValue || '')
+  
+      watch(options, () => cache = options.value)
+
+      return {
+        options,
+        value
+      }
+    },
+    watch: {
+      value() {
+        this.$emit('update:modelValue', this.value)
+        this.$emit('update:label', this.getLabel())
+      },
+      modelValue() {
+        this.value = this.modelValue
+      }
+    },
+    computed:{
+      showAll() {
+        return this.hideAll !== '' || this.hideAll
+      }
+    }, 
+    methods: {
+      getLabel() {
+        let item = this.options.find(item => item.id == this.value)
+        if (item) {
+          return item.name
+        } else {
+          return ''
+        }
+      }
+    },
+    mounted() {
+      if (!this.modelValue && this.hideAll === '' && !this.notDefault) {
+        this.value = this.options[0].id
+      }
+      this.$emit('update:modelValue', this.value)
+      this.$emit('update:label', this.getLabel())
+    }
+  }
+}

+ 161 - 0
src/components/share/index.vue

@@ -0,0 +1,161 @@
+<template>
+  <com-dialog
+    title="分享"
+    :show="show"
+    hideFloor
+    @update:show="val => $emit('update:show', val)">
+
+    <div class="share-layer">
+
+      <div class="code">
+        <div class="share-code">
+          <img :src="codeImg + '?m=' + Date.now()" class="code" />
+          <img :src="_logo" class="logo" v-if="_logo">
+        </div>
+        <div class="share-upload" v-if="setLogo === '' || setLogo">
+          <span class="makre">LOGO</span>
+          <el-button type="primary" class="file-upload">
+            <label for="file-upload">自定义上传</label>
+          </el-button>
+          <input type="file" id="file-upload" @change="fileChange">
+          <span class="size">120*120px,jpg/png格式,不超过1M</span>
+        </div>
+      </div>
+
+      <div class="copy">
+        <label>分享链接:</label>
+        <el-input placeholder="输入链接" v-model="_link" />
+        <el-button type="primary" class="btn" @click="copyLink">复制</el-button>
+      </div>
+
+    </div>
+  </com-dialog>
+</template>
+
+<script>
+import comDialog from "@/components/dialog";
+import {ref} from 'vue'
+import { copyText } from '@/util'
+import { getApp } from '@/app'
+
+export default {
+  props: ['show', 'link', 'logo', 'setLogo', 'codeImg'],
+  setup(props, context) {
+    const _link = ref(props.link)
+    const _logo = ref(props.logo)
+
+    const fileChange = (ev) => {
+      let file = ev.target.files[0]
+
+      if (file.type !== 'image/png' && file.type !== 'image/jpeg') return getApp().$message.error('请上传jpg/png格式', '提示')
+      if (file.size / 1024 / 1024 > 1)  return getApp().$message.error('请上传1M以内的图片', '提示')
+      
+      let render = new FileReader()
+
+      render.onload = ev => {
+        _logo.value = ev.target.result
+      }
+
+      render.readAsDataURL(file)
+      context.emit('changeLogo', file)
+    }
+    return { _link, fileChange, _logo }
+  },
+  methods: {
+    copyLink() {
+      copyText(this._link)
+      this.$message('链接复制成功');
+    }
+  },
+  components: {
+    "com-dialog": comDialog
+  }  
+}
+</script>
+
+<style lang="scss" scoped>
+.share-layer {
+  margin: 0 68px;
+}
+
+.code {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 24px;
+
+  .share-code {
+    width: 160px;
+    height: 160px;
+    position: relative;
+
+    .code {
+      width: 100%;
+      height: 100%;
+    }
+
+    .logo {
+      width: 40px;
+      height: 40px;
+      position: absolute;
+      z-index: 1;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+
+  .share-upload {
+    align-self: stretch;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-end;
+    font-size: 0.825rem;
+    margin-left: 8px;
+  }
+
+  .makre {
+    margin-bottom: 12px;
+  }
+
+  .size {
+    margin-top: 8px;
+    color: #909399;
+  }
+
+  .file-upload {
+    padding: 0;
+    max-width: 120px;
+
+    label {
+      display: inline-block;
+      padding: 12px 20px;
+    }
+  }
+
+  input[type='file'] {
+    display: none;
+  }
+}
+
+.copy {
+  padding: 0 80px 0 70px;
+  position: relative;
+
+  label,
+  .btn {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+  }
+
+  label {
+    left: 0;
+    font-size: 0.825rem;
+  }
+
+  .btn {
+    right: 0;
+  }
+}
+</style>

+ 16 - 0
src/constant/REG.js

@@ -0,0 +1,16 @@
+// 至少8-16个字符,至少1个小写字母和1个数字,其他可以是任意字符:
+export const PSW ={
+  REG: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/,
+  tip: '请输入8-16位数字、英文大小写组合密码'
+}
+
+// 手机号校验
+export const PHONE = {
+  REG: /^((13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\d{8})|(8){11}$/,
+  tip: '手机号格式不正确!'
+}
+
+export const EPSW = {
+  REG: /^[0-9a-zA-Z]*$/,
+  tip: '仅支持数字或字母组合密码'
+}

+ 526 - 0
src/constant/index.js

@@ -0,0 +1,526 @@
+export const UN_REQ_NUM = -99999
+
+// 弹框请求审核
+export const EXAMINE_REQ_SELECT = [
+  { name: '通过', value: 1 },
+  { name: '不通过', value: -1 },
+]
+
+export const EXAMINE_VIEW_SELECT = [
+  ...EXAMINE_REQ_SELECT,
+  { name: '待审核', value: 0 },
+]
+
+// 全部状态(搜索用)
+export const EXAMINE_ALL_STATUS = -2
+// 草稿状态
+export const EXAMINE_DRAFT_STATUS = 0
+// 待审核状态
+export const EXAMINE_NO_AUDIT_STATUS = 1
+// 审核不通过
+export const EXAMINE_NO_ADOPT_STATUS = -1
+// 审核通过状态
+export const EXAMINE_ADOPT_STATUS = 2
+
+// VR项目审核与审核页面公用
+export const EXAMINE_PUBLIC_STATUS = [
+  { name: "待审核", value: EXAMINE_NO_AUDIT_STATUS },
+  { name: "已审核", value: EXAMINE_ADOPT_STATUS },
+  { name: "未通过", value: EXAMINE_NO_ADOPT_STATUS },
+]
+
+// 审核结果,审核页面不展示草稿
+export const EXAMINE_SHOW_STATUS = [
+  { name: "全部", value: EXAMINE_ALL_STATUS },
+  ...EXAMINE_PUBLIC_STATUS
+]
+
+// VR项目审核页面,展示草稿
+export const EXAMINE_SHOW_SELECT_STATUS = [
+  { name: "全部", value: UN_REQ_NUM },
+  { name: "草稿", value: EXAMINE_DRAFT_STATUS },
+  ...EXAMINE_PUBLIC_STATUS,
+]
+
+
+// 房间类型
+export const HOUSE_TYPE = [
+  { name: "新房", value: 1 },
+  // { name: "二手房", value: 2 }
+]
+
+// 请求展示需要带全部
+export const HOUSE_TYPE_SELECT = [
+  { name: '全部', value: UN_REQ_NUM },
+  ...HOUSE_TYPE
+]
+
+// 场景类型
+export const SCENE_TYPE = [
+  { name: '其他', value: '0' },
+  { name: '文博', value: 1 },
+  { name: '地产', value: 2 },
+  { name: '电商', value: 3 },
+  { name: '餐饮', value: 4 },
+  { name: '家居', value: 5 }
+]
+
+export const getName = (options) => {
+  return (val) => {
+    let item = options.find(({ value }) => Number(value) === Number(val))
+    return item ? item.name : ''
+  }
+}
+
+// 起火场所
+export const PLACE = [
+  {
+    label: '非建构筑物',
+    value: '非建构筑物',
+    children: [
+      { label: '室外集贸市场', value: '室外集贸市场' },
+      { label: '露天堆垛', value: '露天堆垛' },
+      {
+        label: '露天农副业场所', value: '露天农副业场所', children: [
+          { label: '农田', value: '农田' },
+          { label: '经济林木(含果园)', value: '经济林木(含果园)' },
+          { label: '苗圃', value: '苗圃' },
+          { label: '花卉', value: '花卉' },
+          { label: '养殖场', value: '养殖场' },
+          { label: '大棚', value: '大棚' },
+          { label: '其他', value: '其他' },]
+      },
+      {
+        label: '室外独立生产设施设备', value: '室外独立生产设施设备', children: [
+          { label: '风力发电设施', value: '风力发电设施' },
+          { label: '光伏发电设施', value: '光伏发电设施' },
+          { label: '小型变压器、配电柜等室外输配电设备', value: '小型变压器、配电柜等室外输配电设备' },
+          { label: '变电站', value: '变电站' },
+          { label: '通讯基站', value: '通讯基站' },
+          { label: '其他室外独立式生产设施', value: '其他室外独立式生产设施' },]
+      },
+      { label: '公园', value: '公园' },
+      { label: '森林', value: '森林' },
+      { label: '草原', value: '草原' },
+      { label: '道路绿化带、隔离带', value: '道路绿化带、隔离带' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    label: '交通工具', value: '交通工具', children: [
+      {
+        label: '客车', value: '客车', children: [
+          { label: '燃油车', value: '燃油车' },
+          { label: '燃气车', value: '燃气车' },
+          { label: '电动车', value: '电动车' },
+          { label: '混合电动车', value: '混合电动车' },]
+      },
+      {
+        label: '货车', value: '货车', children: [
+
+          { label: '燃油车', value: '燃油车' },
+          { label: '燃气车', value: '燃气车' },
+          { label: '电动车', value: '电动车' },
+          { label: '混合电动车', value: '混合电动车' },]
+      },
+      {
+        label: '轿车', value: '轿车', children: [
+
+          { label: '燃油车', value: '燃油车' },
+          { label: '燃气车', value: '燃气车' },
+          { label: '电动车', value: '电动车' },
+          { label: '混合电动车', value: '混合电动车' },]
+      },
+      {
+        label: '特种车', value: '特种车', children: [
+
+          { label: '燃油车', value: '燃油车' },
+          { label: '燃气车', value: '燃气车' },
+          { label: '电动车', value: '电动车' },
+          { label: '混合电动车', value: '混合电动车' },]
+      },
+      { label: '摩托车', value: '摩托车' },
+      { label: '铁路列车', value: '铁路列车' },
+      { label: '船舶', value: '船舶' },
+      { label: '航空(天)', value: '航空(天)' },
+      { label: '城市轨道交通工具', value: '城市轨道交通工具' },
+      { label: '农用机械', value: '农用机械' },
+      { label: '电动助力车(三轮车、自行车)', value: '电动助力车(三轮车、自行车)' },
+      { label: '其他', value: '其他' },]
+  },
+  {
+    label: '建构筑物',
+    value: '建构筑物',
+    children: [
+      {
+        label: '居住场所', value: '居住场所', children: [
+          { label: '自建住宅', value: '自建住宅' },
+          {
+            label: '非自建住宅', value: '非自建住宅', children: [
+              { label: '商品房	', value: '商品房	' },
+              { label: '经适房	', value: '经适房	' },
+              { label: '集资房	', value: '集资房	' },
+              { label: '公寓房	', value: '公寓房	' },]
+          },
+          { label: '员工集体宿舍', value: '员工集体宿舍' },]
+      },
+      { label: '办公场所', value: '办公场所' },
+      {
+        label: '学校', value: '学校', children: [
+
+          {
+            label: '幼儿园托儿所', value: '幼儿园托儿所', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },
+          {
+            label: '小学', value: '小学', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },
+          {
+            label: '中学', value: '中学', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },
+          {
+            label: '大学', value: '大学', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },
+          {
+            label: '校外培训机构', value: '校外培训机构', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },
+          {
+            label: '成人教育机构', value: '成人教育机构', children: [
+              { label: '教学场所	', value: '教学场所	' },
+              { label: '实验场所	', value: '实验场所	' },
+              { label: '学生集体宿舍	', value: '学生集体宿舍	' },
+              { label: '食堂	', value: '食堂	' },
+              { label: '礼堂等室内活动场馆	', value: '礼堂等室内活动场馆	' },
+              { label: '其他	', value: '其他	' },]
+          },]
+      },
+      {
+        label: '商业场所', value: '商业场所', children: [
+          { label: '商场', value: '商场' },
+          { label: '超市', value: '超市' },
+          { label: '室内市场', value: '室内市场' },
+          { label: '经营性小场所', value: '经营性小场所' },
+          { label: '其他', value: '其他' },]
+      },
+      {
+        label: '宾馆、饭店、招待所', value: '宾馆、饭店、招待所', children: [
+
+
+          { label: '客房', value: '客房' },
+          { label: '餐饮', value: '餐饮' },
+          { label: '办公室', value: '办公室' },
+          { label: '会议室', value: '会议室' },
+          { label: '民宿', value: '民宿' },]
+      },
+      {
+        label: '纯餐饮场所', value: '纯餐饮场所', children: [
+          { label: '餐馆酒家', value: '餐馆酒家' },
+          { label: '农家乐', value: '农家乐' },
+          { label: '露天大排档', value: '露天大排档' },
+          { label: '私房菜馆', value: '私房菜馆' },]
+      },
+      {
+        label: '医疗机构', value: '医疗机构', children: [
+
+          { label: '医院', value: '医院' },
+          { label: '诊所', value: '诊所' },
+          { label: '康复中心', value: '康复中心' },
+          { label: '精神病院', value: '精神病院' },]
+      },
+      {
+        label: '养老院', value: '养老院', children: [
+          { label: '公办养老院', value: '公办养老院' },
+          { label: '私营养老院', value: '私营养老院' },]
+      },
+      { label: '福利院', value: '福利院' },
+      {
+        label: '公共娱乐场所', value: '公共娱乐场所', children: [
+          { label: '影剧院、礼堂等演出、放映场所', value: '影剧院、礼堂等演出、放映场所' },
+          { label: '舞厅、卡拉OK厅等歌舞娱乐场所', value: '舞厅、卡拉OK厅等歌舞娱乐场所' },
+          { label: '具有娱乐功能的夜总会、音乐茶座场所', value: '具有娱乐功能的夜总会、音乐茶座场所' },
+          { label: '保龄球馆、旱冰场、按摩、沐足、棋牌、桑拿浴室等营业性健身、休闲场所', value: '保龄球馆、旱冰场、按摩、沐足、棋牌、桑拿浴室等营业性健身、休闲场所' },
+          { label: '儿童娱乐、活动场所', value: '儿童娱乐、活动场所' },
+          { label: '游艺等其他公共娱乐场所', value: '游艺等其他公共娱乐场所' },]
+      },
+      {
+        label: '体育场馆', value: '体育场馆', children: [
+          { label: '室内部分', value: '室内部分' },
+          { label: '露天部分', value: '露天部分' },]
+      },
+      {
+        label: '金融交易场所', value: '金融交易场所', children: [
+
+          { label: '证券交易所', value: '证券交易所' },
+          { label: '银行网点', value: '银行网点' },]
+      },
+      {
+        label: '交通枢纽(站)', value: '交通枢纽(站)', children: [
+          { label: '候机场所', value: '候机场所' },
+          { label: '候船场所', value: '候船场所' },
+          {
+            label: '候车场所', value: '候车场所', children: [
+              { label: '汽车客运站	', value: '汽车客运站	' },
+              { label: '铁路、高铁、城轨候车场所	', value: '铁路、高铁、城轨候车场所	' },]
+          },
+          { label: '地铁站', value: '地铁站' },
+          { label: '公交站', value: '公交站' },
+          { label: '交通综合枢纽', value: '交通综合枢纽' },]
+      },
+      {
+        label: '文物古建筑', value: '文物古建筑', children: [
+          { label: '国家级', value: '国家级' },
+          { label: '省级', value: '省级' },
+          { label: '市、县级', value: '市、县级' },
+          { label: '其他', value: '其他' },]
+      },
+      { label: '文博馆(图书馆、博物馆、档案馆等)', value: '文博馆(图书馆、博物馆、档案馆等)' },
+      { label: '科研试验场所', value: '科研试验场所' },
+      { label: '广播电视中心', value: '广播电视中心' },
+      {
+        label: '通信场所', value: '通信场所', children: [
+          { label: '营业厅', value: '营业厅' },
+          { label: '机房', value: '机房' },
+          { label: '其他', value: '其他' },]
+      },
+      {
+        label: '宗教场所', value: '宗教场所', children: [
+
+          { label: '寺庙', value: '寺庙' },
+          { label: '教堂', value: '教堂' },
+          { label: '道观', value: '道观' },
+          { label: '其他民间信仰场所', value: '其他民间信仰场所' },]
+      },
+      { label: '会议、展览中心', value: '会议、展览中心' },
+      {
+        label: '物资仓储场所', value: '物资仓储场所', children: [
+          {
+            label: '危险品', value: '危险品', children: [
+              { label: '甲类	', value: '甲类	' },
+              { label: '乙类	', value: '乙类	' },
+              { label: '丙类	', value: '丙类	' },
+              { label: '丁类	', value: '丁类	' },
+              { label: '戊类	', value: '戊类	' },]
+          },
+          { label: '棉花', value: '棉花' },
+          { label: '粮食', value: '粮食' },
+          { label: '百货', value: '百货' },
+          { label: '综合', value: '综合' },
+          { label: '物流快递', value: '物流快递' },
+          { label: '三合一、多合一', value: '三合一、多合一' },
+          { label: '经营性小场所', value: '经营性小场所' },]
+      },
+      {
+        label: '厂房', value: '厂房', children: [
+          { label: '甲乙类生产', value: '甲乙类生产' },
+          { label: '丙类生产', value: '丙类生产' },
+          { label: '丁戊类生产', value: '丁戊类生产' },
+          { label: '劳动密集型', value: '劳动密集型' },
+          { label: '三合一、多合一', value: '三合一、多合一' },
+          { label: '经营性小场所', value: '经营性小场所' },
+          { label: '其他', value: '其他' },]
+      },
+      {
+        label: '加油加气站充电站', value: '加油加气站充电站', children: [
+          { label: '加油站', value: '加油站' },
+          { label: '加气站', value: '加气站' },
+          { label: '充电站', value: '充电站' },
+          { label: '加油加气站', value: '加油加气站' },
+          { label: '其他', value: '其他' },]
+      },
+      {
+        label: '汽车库', value: '汽车库', children: [
+          { label: '高层', value: '高层' },
+          { label: '多层', value: '多层' },
+          { label: '地下', value: '地下' },
+          { label: '半地下', value: '半地下' },
+          { label: '机械', value: '机械' },
+          { label: '敞开式', value: '敞开式' },]
+      },
+      {
+        label: '修车库', value: '修车库', children: [
+          { label: '简易修理厂', value: '简易修理厂' },
+          { label: '临时搭建修理厂', value: '临时搭建修理厂' },]
+      },
+      {
+        label: '工地', value: '工地', children: [
+          {
+            label: '建设工地', value: '建设工地', children: [
+              { label: '建筑物、构筑物、设施等	', value: '建筑物、构筑物、设施等	' },
+              { label: '临时宿舍	', value: '临时宿舍	' },
+              { label: '临时办公场所	', value: '临时办公场所	' },
+              { label: '临时食堂	', value: '临时食堂	' },
+              { label: '临时室内仓库	', value: '临时室内仓库	' },
+              { label: '临时建材堆放场所	', value: '临时建材堆放场所	' },]
+          },
+          { label: '装修、改造工地', value: '装修、改造工地' },
+          { label: '动拆迁工地', value: '动拆迁工地' },]
+      },
+      {
+        label: '室内农副业场所', value: '室内农副业场所', children: [
+          { label: '温室', value: '温室' },
+          { label: '养殖场', value: '养殖场' },]
+      },
+      {
+        label: '石油化工企业', value: '石油化工企业', children: [
+          { label: '生产装置', value: '生产装置' },
+          { label: '储罐区', value: '储罐区' },
+          { label: '生活区', value: '生活区' },
+          { label: '办公区', value: '办公区' },
+        ]
+      },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    label: '垃圾及废弃物', value: '垃圾及废弃物', children: [
+      { label: '垃圾箱', value: '垃圾箱' },
+      { label: '垃圾堆', value: '垃圾堆' },
+      { label: '垃圾场', value: '垃圾场' },
+      { label: '垃圾中转站', value: '垃圾中转站' },
+      { label: '废品回收场所', value: '废品回收场所' },
+      { label: '其他', value: '其他' },
+    ]
+  }
+]
+
+
+export const getPlaceCode = (val) => {
+  let rets = ''
+  let vals = val.split('>')
+  let searchData = PLACE
+
+  for (let val of vals) {
+    let item = searchData.find(({value}) => value === val)
+
+    if (item) {
+      rets += searchData.indexOf(item) + 1
+      searchData = item.children
+    } else {
+      rets += '0';
+      break;
+    }
+  }
+
+  return rets
+}
+
+
+
+// 火灾原因
+export const REASON = [
+  {
+    label: '放火',
+    value: '放火',
+    children: [
+      { value: '刑事防火', label: '刑事防火' },
+      { value: '精神病、智障等人士防火', label: '精神病、智障等人士防火' },
+      { value: '自焚', label: '自焚' },
+      { value: '其他', label: '其他' }
+    ]
+  },
+  {
+    value: '电气火灾',
+    label: '电气火灾',
+    children: [
+      { label: '蓄电池故障(热失控)', value: '蓄电池故障(热失控)' },
+      { label: '电气线路故障(线路接插件、配电盘、控制装置等) ', value: '电气线路故障(线路接插件、配电盘、控制装置等) ' },
+      { label: '电动机及其他工业设备', value: '电动机及其他工业设备' },
+      { label: '电器设备故障', value: '电器设备故障' },
+      { label: '电加热器具火灾', value: '电加热器具火灾' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    value: '产作业',
+    label: '产作业',
+    children: [
+      { label: '焊割', value: '焊割' },
+      { label: '熬炼', value: '熬炼' },
+      { label: '化工生产', value: '化工生产' },
+      { label: '机械设备类故障', value: '机械设备类故障' },
+      { label: '烘烤', value: '烘烤' },
+      { label: '接触高温', value: '接触高温' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    value: '用火不慎',
+    label: '用火不慎',
+    children: [
+      { label: '余火复燃', value: '余火复燃' },
+      { label: '照明不慎', value: '照明不慎' },
+      { label: '烘烤不慎', value: '烘烤不慎' },
+      { label: '敬神祭祖', value: '敬神祭祖' },
+      { label: '油锅起火', value: '油锅起火' },
+      { label: '燃气炉具故障及使用不当', value: '燃气炉具故障及使用不当' },
+      { label: '燃油炉具故障及使用不当', value: '燃油炉具故障及使用不当' },
+      { label: '其他炉具故障及使用不当', value: '其他炉具故障及使用不当' },
+      { label: '烟道过热窜火、飞火等', value: '烟道过热窜火、飞火等' },
+      { label: '烧荒、野外生火不慎', value: '烧荒、野外生火不慎' },
+      { label: '使用蚊香不慎', value: '使用蚊香不慎' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    value: '吸烟',
+    label: '吸烟',
+    children: [
+      { label: '违章吸烟', value: '违章吸烟' },
+      { label: '乱扔烟头、火柴等', value: '乱扔烟头、火柴等' },
+      { label: '卧床吸烟', value: '卧床吸烟' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  {
+    value: '玩火',
+    label: '玩火',
+    children: [
+      { label: '小孩玩火', value: '小孩玩火' },
+      { label: '其他', value: '其他' },
+    ]
+  },
+  { label: '燃放烟花爆竹', value: '燃放烟花爆竹' },
+  { label: '自燃', value: '自燃' },
+  { label: '雷击', value: '雷击' },
+  { label: '静电', value: '静电' },
+  { label: '遗留火种', value: '遗留火种' },
+  { label: '其他', value: '其他' },
+  { label: '不排除原因', value: '不排除原因' },
+]
+
+export const types = [
+  { name: '现场调查证据材料', value: 0 },
+  { name: '火灾事故认定', value: 1 },
+  { name: '其他', value: 2 }
+]
+
+export const ADMIN_USER_ID = 'USER00011379381288039055360'

+ 7 - 0
src/constant/view.js

@@ -0,0 +1,7 @@
+
+// 服务端权限对应的router
+export const keyViewMap = {
+}
+
+// 如果不加入菜单,router依附到哪里
+export const attach = { housing: 'estate' }

+ 13 - 0
src/main.js

@@ -0,0 +1,13 @@
+import { createApp } from 'vue';
+import App from './App.vue'
+import router from './router'
+import ElementPlus from 'element-plus'
+import locale from 'element-ui/lib/locale/lang/zh-CN'
+import '@/request/setupAxios'
+import '@/assets/style/public.scss'
+
+
+const app = createApp(App);
+app.use(router)
+app.use(ElementPlus, { locale })
+app.mount('#app')

+ 25 - 0
src/request/authentication.js

@@ -0,0 +1,25 @@
+import crypto from 'crypto'
+
+
+export const getHdConfig = (data = {}) => {
+  const client_secret = process.env.VUE_APP_HD_SECRENT
+  const config = {
+    client_code: process.env.VUE_APP_HD_CODE,
+    req_time: Date.now().toString().substr(0, 10),
+    ...data
+  }
+  const authData = {
+    ...config,
+    client_secret
+  }
+  const keys = Object.keys(authData).sort((a, b) => a.localeCompare(b))
+
+  const authStr = keys.map(key => authData[key]).join('')
+  const md5 = crypto.createHash('md5')
+  const authcode = md5.update(authStr).digest('hex').toLowerCase()
+
+  return {
+    ...config,
+    authcode
+  }
+}

+ 187 - 0
src/request/config.js

@@ -0,0 +1,187 @@
+/**  ----------------角色接口----------------   */
+export const getRoleList = '/web/user/getAllRoleList'
+
+/** ------------------------------------------ */
+
+
+
+
+/**  ----------------用户接口----------------   */
+// 登录
+export const userLogin = '/web/user/login'
+// 注册
+export const userReg = '/web/user/register'
+// 发送注册短信
+export const sendUserMsg = '/web/user/getMsgAuthCode'
+// 修改密码
+export const updatePsw = '/web/user/changePassword'
+
+// 登出
+export const userLogout = '/web/user/logout'
+// 获取用户列表
+export const getUserList = '/web/user/getUserList'
+// 修改用户信息
+export const updateUser = '/web/user/changDeptAndRole'
+// 获取验证码
+export const getCode = '/web/user/getRandCode'
+// 修改用户状态
+export const changeUserStatus = '/web/user/changeStatus'
+export const updateUserPWD = ''
+/** ------------------------------------------ */
+
+
+
+/**  ----------------楼盘接口----------------   */
+// 根据条件拉取所有楼盘
+export const getEstateList = '/fcb/project/project/queryOrSearchList'
+// 修改楼盘信息
+export const updateEstate = '/fcb/project/project/updateEstate'
+// 增加楼盘信息
+export const insertEstate = '/fcb/project/project/addEstate'
+// 删除楼盘信息
+export const deleteEstate = '/fcb/project/project/deleteEstate'
+// 通过房源id获取楼盘信息
+export const getEstateByHouseId = '/fcb/project/house/getEstateDetail'
+/** ------------------------------------------ */
+
+
+/**  ----------------房源接口----------------   */
+// 根据条件拉取所有房源
+export const getHouseList = '/fcb/project/house/queryOrSearchList'
+// 修改房源信息
+export const updateHouse = '/fcb/project/house/updateHouse'
+// 删除房源信息
+export const deleteHouse = '/fcb/project/house/deleteHouse'
+// 提审房源
+export const auditHouse = '/fcb/project/house/submitAudit'
+// 撤回审核房源
+export const dismissHouse = '/fcb/project/house/dismissAudit'
+// 添加房源
+export const insertHouse = '/fcb/project/house/addHouse'
+// 上传logo制作二维码
+export const uploadHouseLogo = '/fcb/project/house/uploadImage'
+// 获取房源二维码
+export const getHouseQrImg = '/fcb/project/house/getQrImage'
+// 获取房源所有分享链接
+export const getHouseShares = '/fcb/project/house/getShareLinks'
+/** ------------------------------------------ */
+
+
+/**  ----------------审核接口----------------   */
+// 根据条件拉取所有审核记录
+export const getExamineList = '/fcb/project/audit/queryOrSearchList'
+// 获取审核页面的审核记录
+export const getExamineViewList = '/fcb/project/house/getAuditHouseList'
+// 审核
+export const estateExamine = '/fcb/project/audit/doAudit'
+// 上下线
+export const estateOnline = '/fcb/project/audit/online'
+/** ------------------------------------------ */
+
+
+/** ----------------部门接口---------------- */
+// 获取公司列表
+export const getCompanyList = '/web/department/getAll'
+/** ------------------------------------------ */
+
+
+
+
+/** ----------------VR模型接口---------------- */
+// 获取场景列表
+export const getSceneList = '/web/scene/getScenePage'
+// 通过场景标题模糊搜索场景
+export const getSceneByTitle = '/web/scene/getLocalScenes'
+// 通过相机获取场景列表
+export const getSceneListByCamera = '/web/scene/getSceneList'
+// 同步场景
+export const asyncScene = '/web/scene/synchronizeScene'
+// 删除场景
+export const deleteScene = '/web/scene/delete'
+/** ------------------------------------------ */
+
+
+
+
+/** ----------------相机接口---------------- */
+// 获取相机列表
+export const getCameraList = '/web/camera/getUserCameraList'
+// 获取相机选项
+export const getCameraOptions = '/web/camera/getUserCameraList?pageNum=1&pageSize=100000'
+// 添加相机
+export const insertCamera = '/web/camera/bindNew'
+// 修改相机
+export const updateCamera = '/api/scene/camera/update'
+// 删除相机
+export const deleteCamera = '/api/scene/camera/delete'
+// 相机绑定设备
+export const bindCamera = '/api/scene/camera/bindCamera'
+// 相机解除绑定设备
+export const unbindCamera = '/web/camera/unbind'
+/** ------------------------------------------ */
+
+
+
+/** ----------------火调项目---------------- */
+// 获取火调列表
+export const getFireList = '/web/fireProject/queryProject'
+// 新增火调
+export const insertFire = '/web/fireProject/addNewProject'
+export const getFirePsw = '/web/fireProject/getRandCode'
+// 修改火调
+export const updateFire = '/web/fireProject/updateProject'
+// 火调设置为教学
+export const setTeach = '/web/fireProject/setTeach'
+// 火调链接地址设置密码
+export const fireSetPsw = '/web/fireProject/updateRandomCode'
+// 获取火调详情
+export const fireDetail = '/web/fireProject/getProjectDetail'
+// 获取火调详情
+// export const fireDetailByPsw = '/web/fireProject/getDetailWithoutAuth'
+
+// 获取火调详情
+export const fireDetailByPsw = '/web/fireProject/wxAnonGetDetail'
+/** ------------------------------------------ */
+
+
+/** ----------------留言管理---------------- */
+// 获取留言列表
+export const getMessageList = '/web/message/getList'
+// 新增火调
+export const insertMessage = '/web/message/addNew'
+
+
+/** ----------------火调附件---------------- */
+// 获取火调列表
+export const getAttachList = '/web/attachment/getListByProject'
+// export const getAttachListByPsw = '/web/attachment/getListByAnon'
+
+export const getAttachListByPsw = '/web/attachment/wxAnonGetList'
+
+// 新增火调
+export const insertAttach = '/web/attachment/addNew'
+// 删除附件
+export const deleteAttachFile = '/web/attachment/deleteById'
+// 火调设置为教学
+export const deleteAttach = '/web/fireProject/setTeach'
+// 取消教学
+export const setUnTeach = '/web/fireProject/setUnTeach'
+// 火调上传word,pdf文件
+export const uploadAttachFile = '/web/fireProject/uploadFile'
+// 火调上传jpg,png文件
+export const uploadAttachImage = '/web/fireProject/uploadImage'
+/** ------------------------------------------ */
+
+
+// 不需要登录就能请求的接口
+export const notLoginUrls = [userLogin, getCode, sendUserMsg, userReg, updatePsw, getCompanyList, fireDetailByPsw, getAttachListByPsw]
+// 需要用表单提交的数据
+export const fromUrls = [auditHouse, dismissHouse]
+// 带文件的请求
+export const fileUrls = [uploadHouseLogo, uploadAttachFile, uploadAttachImage]
+// 需要限定卫GET请求方式的url
+export const GetUrls = [getRoleList, getCompanyList]
+// 需要限定请求方式的url
+export const PostUrls = [getFireList, getMessageList ]
+// 恒大接口
+export const HdUrls = []

+ 18 - 0
src/request/errorMsg.js

@@ -0,0 +1,18 @@
+import {getApp} from '@/app'
+
+const STOP = 2000
+let showIng = false
+
+
+export const openErrorMsg = (msg) => {
+  if (showIng) return;
+
+  showIng = true
+  getApp().$message({
+    type: 'error',
+    duration: STOP,
+    message: msg
+  });
+
+  setTimeout(() => showIng = false, STOP)
+}

+ 27 - 0
src/request/loading.js

@@ -0,0 +1,27 @@
+
+import { getApp } from '@/app'
+
+const notOpenUrls = [
+]
+
+let loading
+
+export const openLoading = (url) => {
+  // if (loading) return;
+  if (loading || ~notOpenUrls.indexOf(url)) return;
+
+  loading = getApp().$loading({
+    lock: true,
+    text: '加载中',
+    spinner: 'el-icon-loading',
+    background: 'rgba(255, 255, 255, 0.4)',
+    customClass: 'loading'
+  });
+}
+
+export const closeLoading = () => {
+  if (loading) {
+    loading.close()
+    loading = void 0
+  }
+}

+ 83 - 0
src/request/setupAxios.js

@@ -0,0 +1,83 @@
+import axios from 'axios'
+import qs from 'qs'
+import user from '@/state/user'
+import { getApp } from '@/app'
+import { openLoading, closeLoading } from './loading'
+import { openErrorMsg } from './errorMsg'
+import { fromUrls, fileUrls, GetUrls, PostUrls, notLoginUrls } from './config'
+
+const gotoLoginCodes = [3004]
+
+axios.defaults.baseURL = process.env.VUE_APP_PREFIX
+
+axios.interceptors.request.use(async config => {
+  const app = getApp()
+
+  if (!user.value.token && !~notLoginUrls.indexOf(config.url)) {
+    app.$router.replace({name: 'login'})
+    throw '用户未登录'
+  }
+
+  config.headers.token = user.value.token
+  config.headers.userid = user.value.info.id
+
+
+  if (~GetUrls.indexOf(config.url)) {
+    config.method = 'GET'
+  } else if (~PostUrls.indexOf(config.url)) {
+    config.method = 'POST'
+    if (!config.data && config.params) {
+      config.data = config.params 
+    }
+  }
+
+  // 处理需要用表单上传的请求
+  if (~fromUrls.indexOf(config.url)) {
+
+    config.data = qs.stringify(config.data)
+    config.headers['Content-Type'] = "application/x-www-form-urlencoded; charset=utf-8;"
+
+  } else if (~fileUrls.indexOf(config.url)) {
+    const fromData = new FormData()
+
+    Object.keys(config.data).forEach(key => {
+      fromData.append(key, config.data[key])
+    })
+    config.data = fromData
+    config.headers['Content-Type'] = "multipart/form-data"
+  }
+
+  openLoading(config.url)
+  return config
+})
+
+const resInterceptor = res => {
+  const app = getApp()
+  
+  closeLoading()
+
+  if (res.data.code !== 0 && res.data.code !== "000000") {
+    let errMsg = res.data.msg || res.data.message
+    openErrorMsg(errMsg)
+
+    if (~gotoLoginCodes.indexOf(res.data.code) || errMsg === 'token已经失效,请重新登录') {
+      app.$router.replace({name: 'login'})
+      user.value.token = ''
+    }
+    throw res.data.msg
+  }
+  
+  return res.data
+}
+
+axios.interceptors.response.use(resInterceptor, (error) => {
+  closeLoading()
+  if (error.response && error.response.data) {
+    return resInterceptor(error.response)
+  } else {
+    openErrorMsg(typeof error === 'string' ? error : '请求失败,服务端发生了点小故障!')
+    return Promise.reject(error);
+  }
+  
+})
+

+ 133 - 0
src/router/index.js

@@ -0,0 +1,133 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import LoginView from '@/view/system'
+import RegisterView from '@/view/system'
+import ForgetView from '@/view/system'
+import ViewLayout from '@/view/layout'
+import Scene from '@/view/scene'
+import DispatchView from '@/view/dispatch'
+import HomeView from '@/view/home'
+import VRModelView from '@/view/vrmodel'
+// import TeachingView from '@/view/teaching'
+import CameraView from '@/view/camera'
+import UserView from '@/view/user'
+
+export const system = [
+  {
+    name: 'login',
+    path: '/login',
+    component: LoginView,
+    meta: { title: '登录' }
+  },
+  {
+    name: 'register',
+    path: '/register',
+    component: RegisterView,
+    meta: { title: '注册' }
+  },
+  {
+    name: 'forget',
+    path: '/forget',
+    component: ForgetView,
+    meta: { title: '重置密码' }
+  }
+]
+
+export const routes = [
+  {
+    name: 'viewLayout',
+    path: '/',
+    component: ViewLayout,
+    meta: { title: 'VR看房管理' },
+    children: [
+      ...system,
+      {
+        name: 'home',
+        path: 'home',
+        component: HomeView,
+        meta: { title: '首页' }
+      },
+      {
+        name: 'vrmodel',
+        path: 'vrmodel',
+        component: VRModelView,
+        meta: { title: '场景管理' }
+      },
+      {
+        name: 'camera',
+        path: 'camera',
+        component: CameraView,
+        meta: { title: '相机管理' }
+      },
+      {
+        name: 'dispatch',
+        path: 'dispatch',
+        component: DispatchView,
+        meta: { title: '火调管理' }
+      },
+      {
+        name: 'teaching',
+        path: 'teaching',
+        component: DispatchView,
+        meta: { title: '教学平台' }
+      },
+      {
+        name: 'user',
+        path: 'user',
+        component: UserView,
+        meta: { title: '用户管理' }
+      }
+    ]
+  },
+  {
+    name: 'scene',
+    path: '/scene/:projectId',
+    component: Scene,
+    meta: { title: '火调场景' }
+
+  }
+]
+
+const router = createRouter({ 
+  history: createWebHashHistory(),
+  routes,
+
+})
+
+router.beforeEach((to, from, next) => {
+  if (!to.name || to.name === 'viewLayout') {
+    router.replace({name: 'home'})
+    return;
+  }
+  next()
+})
+
+const getConfig = (routes) => {
+  let config = []
+
+  for (let item of routes) {
+    if (!system.includes(item)) {
+      config.push({
+        ...item,
+        children: item.children && getConfig(item.children)
+      })
+    }
+  }
+
+  return config
+}
+
+export const routeIsSystem = ($route, proutes = routes) => {
+  for (let item of proutes) {
+    if (!system.includes(item)) {
+      if (item.name === $route.name) {
+        return false
+      } else if (item.children && !routeIsSystem($route, item.children)) {
+        return false
+      }
+    }
+  }
+
+  return true
+}
+export const config = getConfig(routes)
+export default router

+ 69 - 0
src/state/navs.js

@@ -0,0 +1,69 @@
+import { config } from '@/router'
+import { attach } from '@/constant/view'
+import user from './user'
+import { watch, ref } from 'vue'
+
+// 所有权限都有的router
+export const MUST_JOIN_NAVS = ['viewLayout', 'login', 'register', 'forget', 'scene']
+// router对应的icon
+const ICON_MAP = { 
+  estate: 'iconfire_scenes', 
+  home: 'iconfire_home', 
+  vrmodel: 'iconfire_scenes', 
+  camera: 'iconfire_camera', 
+  dispatch: 'iconfire_management', 
+  teaching: 'iconfire_study',
+  user: 'iconfire_user'
+}
+
+const getNames = (config) => {
+  let names = []
+  config.forEach(item => {
+    names.push(item.name)
+    item.children && names.push(...getNames(item.children))
+  })
+  return names
+}
+
+const _getNavs = (items, notJoinNavs) => {
+  let ret = []
+  for (let i = 0; i < items.length; i++) {
+    if (~notJoinNavs.indexOf(items[i].name)) {
+      continue
+    } else {
+      if (items[i].children) {
+        ret.push(..._getNavs(items[i].children, notJoinNavs))
+      } else if (ICON_MAP[items[i].name]){
+        let item = {
+          ...items[i], 
+          icon: ICON_MAP[items[i].name]
+        }
+        ret.push(item)
+        // delete item.component
+      }
+    }
+  }
+  return ret
+}
+
+const getNavs = () => {
+  let names = getNames(config)
+  let showNames = user.value.permission.map(item => item.viewName)
+  let notJoinNavs = names.filter(name => 
+    ~Object.keys(attach).indexOf(name) || (
+      !~showNames.indexOf(name) && 
+      !~MUST_JOIN_NAVS.indexOf(name)
+    )
+  )
+  return _getNavs(config, notJoinNavs)
+}
+
+const navs = ref([])
+
+watch(
+  () => user.value.permission, 
+  () => navs.value = getNavs()
+)
+setTimeout(() => navs.value = getNavs())
+
+export default navs

+ 246 - 0
src/state/tableRef.js

@@ -0,0 +1,246 @@
+import axios from 'axios'
+import { ref, watch, onActivated, onMounted } from 'vue'
+import { getApp } from '@/app'
+import { UN_REQ_NUM } from '@/constant'
+import { throttle } from '@/util'
+
+const getOperState = (attr) => {
+  const operState = ref({
+    state: {
+      show: false,
+      id: 0, 
+      ...JSON.parse(JSON.stringify(attr)) 
+    },
+    _readyUpdate(item) {
+      operState.value.state.id = item.id
+      operState.value.state.show = true
+  
+      Object.keys(attr).forEach(key => {
+        operState.value.state[key] = attr[key]
+      })
+
+      Object.keys(attr).forEach(key => {
+        if (item[key] !== void 0) {
+          operState.value.state[key] = item[key]
+        }
+      })
+    },
+    readyInsert() {
+      operState.value.reset()
+      operState.value.state.show = true
+    },
+    reset() {
+      Object.keys(attr).forEach(key => {
+        operState.value.state[key] = attr[key]
+      })
+      operState.value.state.show = false
+      operState.value.state.id = void 0
+    },
+    quit() {
+      operState.value.reset()
+    }
+  })
+
+  return operState
+}
+
+const getSearchState = (attr) => {
+  const searchState = ref({
+    state: { ...JSON.parse(JSON.stringify(attr)) },
+    reset() {
+      Object.keys(attr).forEach(key => {
+        searchState.value.state[key] = attr[key]
+      })
+    }
+  })
+
+  return searchState
+}
+
+
+const getDataListState = (isSelect) => {
+  const listStateState = ref({
+    state: [],
+  })
+
+  if (isSelect) {
+    listStateState.value.selectRows = []
+    listStateState.value.changeSelectRows = (data) => {
+      listStateState.value.selectRows = data
+    }
+  }
+
+  return listStateState
+}
+
+const getPagState = () => {
+  const pagState = ref({
+    state: {
+      currPage: 1,
+      size: 12,
+      total: 0
+    },
+    sizeChange(attr) {
+      pagState.value.state.size = attr
+    },
+    currentChange(attr) {
+      pagState.value.state.currPage = attr
+    }
+  })
+
+  return pagState
+}
+
+const referListData = async (ret, getUrl, pagAttr = {}) => {
+  let params = {
+    pageNum: ret.pag.value.state.currPage,
+    pageSize: ret.pag.value.state.size
+  }
+  if (ret.search) {
+    Object.keys(ret.search.value.state).forEach(key => {
+      if (UN_REQ_NUM !== ret.search.value.state[key]) {
+        if (typeof ret.search.value.state[key] === 'string') {
+          params[key] = ret.search.value.state[key].trim()
+        } else {
+          params[key] = ret.search.value.state[key]
+        }
+      }
+    })
+  }
+  
+  try {
+    let res = await axios.get(getUrl,  { params })
+    ret.pag.value.state.total = pagAttr['totalNum'] ? res.data[pagAttr['totalNum']] : res.data.totalNum
+    ret.dataList.value.state = res.data.list = 'list'in pagAttr ?  pagAttr['list'] ? res.data[pagAttr['list']] : res.data : res.data.list
+  } catch {
+    ret.dataList.value.state = []
+    ret.pag.value.state.total = 0
+  }
+  
+}
+
+
+export default ({operAttr, searchAttr, pagAttr, isSelect = true, getUrl, updateUrl, insertUrl, delUrl, delMsg, delAttr } = {}) => {
+  let ret = {
+    dataList: getDataListState(isSelect),
+    pag: getPagState()
+  }
+  let watchItems = [
+    () => ret.pag.value.state.currPage,
+    () => ret.pag.value.state.size,
+  ]
+
+  if (operAttr) {
+    const oper = getOperState(operAttr)
+
+    oper.value.readyUpdate = function(...args) {
+      oper.value._readyUpdate(...args)
+      oper.value.state.__oldData = ret.dataList.value.state.find(({id}) => id === oper.value.state.id)
+    }
+
+    updateUrl && (
+      oper.value.update = async (data) => {
+        let target = data || oper.value.state
+        let updateIndex = ret.dataList.value.state.findIndex(({id}) => id === target.id)
+        let item = { ...ret.dataList.value.state[updateIndex] }
+
+        if (item) {
+          Object.keys(operAttr).forEach(key => item[key] = target[key])
+        }
+        
+        await axios.post(updateUrl, data || item)
+        
+        ret.dataList.value.state[updateIndex] = item
+        oper.value.reset()
+        ret.dataList.value.refer()
+      }
+    )
+
+    insertUrl && (
+      oper.value.insert = async (data) => {
+        let target = data || oper.value.state
+        console.log(oper.value.state)
+        let item = {}
+        Object.keys(operAttr).forEach(key => item[key] = target[key])
+
+        await axios.post(insertUrl, data || item)
+
+        ret.dataList.value.state.push(item)
+        oper.value.reset()
+        ret.dataList.value.refer()
+      }
+    )
+
+    ret.oper = oper
+  }
+
+  if (searchAttr) {
+    const search = getSearchState(searchAttr)
+
+    search.value.submit = () => {
+      ret.dataList.value.refer()
+    }
+    ret.search = search
+
+    watchItems.push(search.value.state)
+  }
+
+
+  if (updateUrl || delUrl) {
+    if (delUrl) {
+      ret.dataList.value._delete = async (data) => {
+        let reqData = {}
+        if (delAttr) {
+          for (let [k, v] of Object.entries(delAttr)) {
+            reqData[k] = data[v]
+          }
+
+          console.log(delAttr, data, reqData)
+        } else {
+          reqData.id = data.id
+        }
+        await axios.post(delUrl, reqData)
+      }
+    } else {
+      ret.dataList.value._delete = async (data) => {
+        data.isDelete = 1
+        await axios.post(updateUrl, data)
+      }
+    }
+
+    ret.dataList.value.delete = async (data) => {
+      if (await getApp().$confirm(delMsg || '确定要删除此数据吗?', '提示')) {
+        await ret.dataList.value._delete(data)
+        getApp().$message({message: '删除成功', type: 'success'});
+        ret.dataList.value.refer()
+      }
+    }
+
+    if (isSelect) {
+      ret.dataList.value.deleteSelect = async () => {
+        const rows = ret.dataList.value.selectRows
+        if (rows.length === 0) {
+          getApp().$message.error('请勾选数据后再删除数据!', '提示')
+        } else if (await getApp().$confirm(`确定要删除这${rows.length}条数据吗?`, '提示')) {
+          await Promise.all(rows.map(item => ret.dataList.value._delete(item)))
+          getApp().$message({message: '删除成功', type: 'success'});
+          ret.dataList.value.refer()
+        }
+      }
+    }
+  }
+
+  ret.dataList.value.refer = throttle(
+    () => {
+      referListData(ret, getUrl, pagAttr)
+    },
+    100
+  )
+
+  // 刷新数据
+  watch( watchItems, ret.dataList.value.refer )
+  // 激活再刷新一次
+  onActivated(() => referListData(ret, getUrl, pagAttr))
+  onMounted(() => referListData(ret, getUrl, pagAttr))
+  return ret
+}

+ 84 - 0
src/state/user.js

@@ -0,0 +1,84 @@
+import { ref, watch } from 'vue'
+import { keyViewMap } from '@/constant/view'
+
+
+const strToJson = (str, def) => {
+  try {
+    str = JSON.parse(str)
+  } catch {
+    str = def
+  }
+  return str || def
+}
+
+const user = ref({
+  token: localStorage.getItem('token'),
+  info: strToJson(localStorage.getItem('info'), {}),
+  permission: strToJson(localStorage.getItem('permission'), [])
+})
+
+watch(
+  () => user.value.token,
+  () => localStorage.setItem('token', user.value.token)
+)
+
+watch(
+  () => user.value.info,
+  () => localStorage.setItem('info', JSON.stringify(user.value.info))
+)
+
+watch(
+  () => user.value.permission,
+  () => localStorage.setItem('permission', JSON.stringify(user.value.permission))
+)
+
+export const setPermission = val => {
+  if (val !== void 0){
+    let pubPermission = ['home', 'vrmodel', 'camera', 'teaching', 'dispatch'].map(key => ({
+      children: [
+        { resourceKey: key + ':select' }, 
+        { resourceKey: key + ':update' }, 
+        { resourceKey: key + ':delete' }, 
+        { resourceKey: key + ':add' }
+      ],
+      resourceKey: key
+    }))
+
+    if (val === '1') {
+      pubPermission[pubPermission.length - 1].children.push(
+        { resourceKey: 'dispatch:setting' }
+      )
+      pubPermission[pubPermission.length - 2].children.push(
+        { resourceKey: 'teaching:setting' }
+      )
+
+      pubPermission.push({children: [
+          { resourceKey: 'user:select' }, 
+          { resourceKey: 'user:update' }, 
+          { resourceKey: 'user:delete' }, 
+          { resourceKey: 'user:add' }
+        ],
+        resourceKey: 'user'
+      })
+    }
+    
+    val = pubPermission.map(item => ({
+      ...item,
+      viewName: keyViewMap[item.resourceKey] || item.resourceKey
+    }))
+  } else {
+    val = []
+  }
+
+  user.value.permission = val
+}
+
+export const setToken = val => {
+  user.value.token = val
+}
+
+export const setInfo = val => {
+  user.value.info = val
+}
+
+export default user

+ 42 - 0
src/state/viewAuth.js

@@ -0,0 +1,42 @@
+import { keyViewMap, attach } from '@/constant/view'
+import { ref, watch } from 'vue'
+import router from '@/router'
+import user from './user'
+import navs, {MUST_JOIN_NAVS} from './navs'
+
+const auth = ref({})
+
+
+const setCurrentAuth = () => {
+  let viewName = router.currentRoute.value.name
+  
+  viewName = attach[viewName] ? attach[viewName] : viewName
+
+  let key = Object.keys(keyViewMap).find(key => keyViewMap[key] === viewName) || viewName
+  let authParent = user.value.permission.find(item => item.resourceKey === key)
+
+  if (!navs.value.some(item => item.name !== viewName) && !MUST_JOIN_NAVS.includes(viewName)) {
+    router.replace({name: 'login'})
+  }
+
+  if (authParent) {
+    let viewAuths = authParent.children.map(item => item.resourceKey.substr(key.length + 1))
+    let newValue = {}
+    viewAuths.forEach(key => newValue[key] = true)
+    auth.value = newValue
+  } else {
+    auth.value = {}
+  }
+}
+
+setTimeout(() => {
+  watch(
+    router.currentRoute, 
+    setCurrentAuth, 
+    { immediate: true }
+  )
+})
+
+watch(() => user.value.permission, setCurrentAuth)
+
+export default auth

+ 104 - 0
src/util/index.js

@@ -0,0 +1,104 @@
+import Base64 from 'Base64'
+
+export const dateFormat = (date, fmt) => {
+  var o = {
+    "M+": date.getMonth() + 1,                 //月份 
+    "d+": date.getDate(),                    //日 
+    "h+": date.getHours(),                   //小时 
+    "m+": date.getMinutes(),                 //分 
+    "s+": date.getSeconds(),                 //秒 
+    "q+": Math.floor((date.getMonth() + 3) / 3), //季度 
+    "S": date.getMilliseconds()             //毫秒 
+  };
+  if (/(y+)/.test(fmt)) {
+    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
+  }
+  for (var k in o) {
+    if (new RegExp("(" + k + ")").test(fmt)) {
+      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
+    }
+  }
+  return fmt;
+}
+
+export const copyText = (text) => {
+  const input = document.createElement('input')
+  document.body.appendChild(input);
+
+  input.setAttribute('value', text);
+  input.select()
+
+  document.execCommand('copy')
+
+  document.body.removeChild(input)
+}
+
+
+export const throttle = (fn, mis = 500) => {
+  let time
+
+  return (...args) => {
+    clearTimeout(time)
+    time = setTimeout(() => fn(...args), mis)
+  }
+}
+
+export function randomWord(randomFlag, min, max) {
+  let str = ''
+  let range = min
+  let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min
+  }
+  for (var i = 0; i < range; i++) {
+    let pos = Math.round(Math.random() * (arr.length - 1))
+    str += arr[pos]
+  }
+  return str
+}
+
+export const encryption = (str, strv = '') => {
+  str = Base64.btoa(str)
+  const NUM = 2
+  const front = randomWord(false, 8)
+  const middle = randomWord(false, 8)
+  const end = randomWord(false, 8)
+
+  let str1 = str.substring(0, NUM)
+  let str2 = str.substring(NUM)
+
+  if (strv) {
+    let strv1 = strv.substring(0, NUM)
+    let strv2 = strv.substring(NUM)
+    return [front + str2 + middle + str1 + end, front + strv2 + middle + strv1 + end]
+  }
+
+  return front + str2 + middle + str1 + end
+}
+
+
+/**
+ *获取id
+ */
+export const guid = () => {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+    let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+    return v.toString(16);
+  });
+}
+
+export const downloadFile = (url, fileName) => {
+  var x = new XMLHttpRequest();
+  x.open("GET", url, true);
+  x.responseType = 'blob';
+  x.onload = function(e) {
+      var url = window.URL.createObjectURL(x.response)
+      var a = document.createElement('a');
+      a.href = url;
+      console.log(e)
+      a.setAttribute('download', fileName)
+      a.click();
+  }
+  x.send();
+}

+ 185 - 0
src/view/camera/index.vue

@@ -0,0 +1,185 @@
+<template>
+  <com-head :options="headList" >
+    <el-form label-width="84px" inline="true">
+      <el-form-item label="S/N码:">
+        <el-input v-model="search.state.snCode" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="所属架构:">
+        <com-select v-model="search.state.deptId" />
+      </el-form-item>
+      <el-form-item class="searh-btns" style="grid-area: 1 / 3 / 2 / 4;">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="search.reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <div class="body-head">
+      <h3 style="visibility: hidden;">相机列表</h3>
+      <div class="table-ctrl-right">
+        <!-- <el-input placeholder="关键词" suffix-icon="el-icon-search" class="search-scene" v-model="search.state.searchKey" style="width: auto" /> -->
+        <el-button
+          type="primary"
+          @click="oper.readyInsert"
+          v-if="auth.add"
+          >绑定相机</el-button
+        >
+      </div>
+    </div>
+    <el-table
+      ref="multipleTable"
+      :data="dataList.state"
+      tooltip-effect="dark"
+      style="width: 100%"
+      @row-click="dataList.selectRow"
+    >
+      <el-table-column label="序号" width="55" v-slot:default="{ $index }">
+        <div style="text-align: center">{{ pag.state.size * (pag.state.currPage - 1) + $index + 1 }}</div>
+      </el-table-column>usedSpace totalSpace
+      <el-table-column label="S/N码" prop="snCode"></el-table-column>
+      <el-table-column label="所属架构" prop="deptName"></el-table-column>
+      <el-table-column label="云容量使用情况" v-slot:default="{ row }">
+        {{row.usedSpaceStr}} / {{row.totalSpaceStr}}
+      </el-table-column>
+      <el-table-column label="操作" v-slot:default="{ row }" v-if="auth.unbind || auth.update">
+        <!-- <span class="oper-span" @click="oper.readyUpdate(row)" v-if="auth.update">编辑</span> -->
+        <span class="oper-span" 
+          @click="unbindCamrea(row)" 
+          v-if="auth.delete" 
+          style="color: var(--primaryColor)">
+         解绑
+        </span>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+  </div>
+  
+  <com-dialog
+    :title="(oper.state.id ? '编辑' : '绑定') + '相机'"
+    v-model:show="oper.state.show"
+    enterText="确 定"
+    @submit="submit"
+    width="480"
+  >
+    <el-form ref="form" :model="form" label-width="68px" class="camera-from">
+      <el-form-item label="SN码:" class="mandatory">
+        <el-input :modelValue="oper.state.snCode" placeholder="请输入相机底部SN码,如214D5RE2G8" @update:modelValue="val => oper.state.snCode = val.trim()"></el-input>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+
+</template>
+
+<script>
+import { ref, watch } from "vue";
+import getTableState from "@/state/tableRef";
+import auth from "@/state/viewAuth";
+import comDialog from "@/components/dialog";
+import comPagination from "@/components/pagination";
+import comSelect from "@/components/company-select";
+import comHead from "@/components/head";
+import { dateFormat } from '@/util'
+import {
+  getCameraList,
+  insertCamera,
+  deleteCamera,
+  unbindCamera,
+  updateCamera
+} from '@/request/config'
+import axios from 'axios';
+
+export default {
+  name: 'camera',
+  setup() {
+    const state = getTableState({
+      insertUrl: insertCamera,
+      updateUrl: updateCamera,
+      getUrl: getCameraList,
+      delUrl: deleteCamera,
+      searchAttr: { deptId: '', snCode: '' },
+      operAttr: { snCode: '', cameraSn: '' },
+      delMsg: '解绑相机,该相机拍摄的场景也将一并解绑(场景在云端存储,不会删除)确定要解绑吗?'
+    });
+    const headList = ref([{ name: "相机管理", value: 2 }]);
+    const time = ref(null)
+
+    watch(state.oper.value.state, () => {
+      state.oper.value.state.cameraSn = state.oper.value.state.snCode
+    })
+
+    watch(time, () => {
+      if (time.value) {
+        state.search.value.state.startTime = dateFormat(new Date(time.value[0]), 'yyyy-MM-dd hh:mm:ss')
+        state.search.value.state.endTime = dateFormat(new Date(time.value[1]), 'yyyy-MM-dd 23:59:59')
+      } else {
+        state.search.value.state.startTime = state.search.value.state.endTime = ''
+      }
+    })
+    watch(
+      [() => state.search.value.state.startTime, () => state.search.value.state.endTime],
+      () => {
+        if (!state.search.value.state.startTime || !state.search.value.state.endTime) {
+          time.value = null
+        }
+      }
+    )
+
+    return { ...state, headList, time, auth };
+  },
+  methods: {
+    async unbindCamrea(data) {
+      if (await this.$confirm('解绑相机,该相机拍摄的场景也将一并解绑(场景在云端存储,不会删除)确定要解绑吗?', '提示')) {
+        await axios.post(unbindCamera, {cameraSn: data.snCode})
+        this.dataList.refer()
+        
+      }
+    },
+    submit() {
+      if (!this.oper.state.snCode.trim()) {
+        return this.$message.error('S/N码不能为空!', '提示')
+      }
+
+      this.oper.state.id ? this.oper.update() : this.oper.insert()
+    }
+  },
+  components: {
+    "com-dialog": comDialog,
+    "com-head": comHead,
+    "com-pagination": comPagination,
+    "com-select": comSelect
+  },
+};
+
+
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+// .camera-from {
+//   width: 420px;
+//   margin: 0 auto;
+// }
+</style>

+ 254 - 0
src/view/dispatch/archives.vue

@@ -0,0 +1,254 @@
+<template>
+  <com-dialog
+    title="火调档案管理"
+    enterText="上传附件"
+    :hideFloor="user.info.departmentId != organizerDeptId"
+    :show="show"
+    @submit="oper.readyInsert"
+    @update:show="val => $emit('update:show', val)"
+  >
+    <div class="body-layer" style="padding: 0">
+      <el-table
+        ref="multipleTable"
+        :data="tableData"
+        tooltip-effect="dark"
+        style="width: 100%;min-height:540px;max-height:540px"
+        :span-method="arraySpanMethod"
+      >
+        <el-table-column label="附件标题" v-slot:default="{row}">
+          <span :class="{title: row._title}">{{row._title ? row._title : row.fileName}}</span>
+        </el-table-column>
+        <el-table-column label="创建人" prop="creatorName"></el-table-column>
+        <el-table-column label="创建日期" prop="updateTime" width="200" v-slot:default="{row}">
+          {{row.updateTime ? row.updateTime.toString() : ''}}
+        </el-table-column>
+        <el-table-column label="操作" v-slot:default="{ row }" width="150">
+          <a class="oper-span" :href="row.fileOssUrl" target="_blank">查看</a>
+          <span class="oper-span" @click="downloadFile(row.fileOssUrl, row.fileName)">下载</span>
+          <span class="oper-span" 
+            :class="{disable: user.info.departmentId != organizerDeptId}" 
+            @click="user.info.departmentId == organizerDeptId && deleteItem(row)" 
+            style="color: var(--primaryColor)">
+          删除
+          </span>
+        </el-table-column>
+      </el-table>
+    </div>
+  </com-dialog>
+
+  
+  <com-dialog
+    title="上传附件"
+    v-model:show="oper.state.show"
+    width="540"
+    enterText="确 定"
+    @submit="submit"
+  >
+    <el-form ref="form" :model="form" label-width="90px" class="camera-from dispatch-file-from">
+      <el-form-item label="附件标题:" class="mandatory">
+        <el-input v-model="oper.state.fileName" class="arch-name" placeholder="请输入最多不能超过50字" maxlength="50" show-word-limit></el-input>
+      </el-form-item>
+      <el-form-item label="附件类型:" class="mandatory">
+        <el-select v-model="oper.state.attachmentType" placeholder="请选择">
+            <el-option
+              v-for="item in types"
+              :key="item.name"
+              :label="item.name"
+              :value="item.name">
+            </el-option>
+          </el-select>
+      </el-form-item>
+      <el-form-item label="附件:" class="mandatory">
+        <el-upload
+          class="upload-demo"
+          :http-request="uploadFile"
+          :multiple="false"
+          :limit="1"
+          :before-upload="beforeUpload"
+          :before-remove="beforeRemove"
+          :file-list="oper.state.fileList">
+          <el-button size="medium" type="primary" :disabled="!!oper.state.fileOssUrl" @click="ev => oper.state.fileOssUrl && ev.stopPropagation()"><i class="el-icon-upload2 el-icon--left"></i>上传</el-button>
+          <template v-slot:tip>
+            <div class="el-upload__tip">注:可上传100M以内的pdf、jpg、word文件</div>
+          </template>
+        </el-upload>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+
+</template>
+
+<script>
+import getTableState from "@/state/tableRef";
+import {
+  getAttachList,
+  unbindCamera,
+  insertAttach,
+  uploadAttachFile,
+  uploadAttachImage,
+  deleteAttachFile
+} from '@/request/config'
+import comDialog from "@/components/dialog";
+import user from '@/state/user'
+import axios from 'axios';
+import { watch } from 'vue';
+import { downloadFile } from '@/util'
+const fileTypes = ['pdf', 'word']
+const imgTypes = ['jpeg', 'jpg']
+import { types } from '@/constant'
+
+
+export default {
+  name: 'archives',
+  props: ['show', 'data', 'organizerDeptId'],
+  setup(props) {
+    const state = getTableState({
+      getUrl: getAttachList,
+      insertUrl: insertAttach,
+      delUrl: deleteAttachFile,
+      delAttr: {ids: 'ids'},
+      pagAttr: {list: ''},
+      searchAttr: {pageSize: 12, projectId: props.data},
+      operAttr: {
+        attachmentType: '',
+        fileOssUrl: '',
+        fileName: '',
+        projectId: '',
+        type: '0'
+      },
+    });
+
+    watch(props, () => {
+      if (props.data === state.search.value.state.projectId) return
+      state.search.value.state.projectId = props.data
+      state.dataList.value.refer()
+    })
+
+    
+    return { ...state, types, user };
+  },
+  methods: {
+    async deleteItem(row) {
+      await this.dataList.delete({ids: row.id})
+      this.$emit('referList')
+    },
+    async unbindCamrea(data) {
+      if (data.departmentId && (await this.$confirm('解绑相机,该相机拍摄的场景也将一并解绑(场景在云端存储,不会删除)确定要解绑吗?', '提示'))) {
+        await axios.post(unbindCamera, data)
+        data.departmentName = data.departmentId = null
+      }
+
+      this.dataList.refer()
+    },
+    arraySpanMethod({ row, columnIndex  }) {
+      if (row._title ) {
+        return columnIndex === 0 ? [1, 4] : [0, 0]
+      }
+    },
+    async uploadFile(data) {
+      let url;
+
+      if (imgTypes.some(type => ~data.file.type.indexOf(type))) {
+        url = uploadAttachImage
+        this.oper.state.type = 1
+      } else {
+        url = uploadAttachFile
+        this.oper.state.type = 2
+      } 
+      try {
+        let res = await axios.post(url, {file: data.file})
+        this.oper.state.fileOssUrl = res.data.ossUrl
+        data.onSuccess()
+      } catch {
+        data.onError()
+      }
+    },
+    beforeUpload(file) {
+      let successTypes = [...imgTypes, ...fileTypes]
+      let maxSize = 100 * 1024 * 1024
+
+      if (!successTypes.some(type => ~file.type.indexOf(type))) {
+        this.$message.error(`请上传${successTypes.join('、')}等格式的文件`, '提示')
+        return false
+      } else if (file.size > maxSize) {
+        this.$message.error('请上传100M以内的文件', '提示')
+        return false
+      } else {
+        return true
+      }
+    },
+    beforeRemove() {
+      this.oper.state.fileOssUrl = void 0
+    },
+    submit() {
+      this.oper.state.projectId = this.search.state.projectId
+      if (!this.oper.state.attachmentType) {
+        return this.$message.error('附件类型不能为空!', '提示')
+      } else if (!this.oper.state.fileOssUrl || !this.oper.state.fileOssUrl.trim()) {
+        return this.$message.error('请上传附件!', '提示')
+      } else if (!this.oper.state.fileName || !this.oper.state.fileName.trim()) {
+        return this.$message.error('附件标题不能为空!', '提示')
+      }
+
+      this.oper.state.id ? this.oper.update() : this.oper.insert()
+
+      this.$emit('referList')
+    },
+    downloadFile
+  },
+  computed: {
+    tableData() {
+      let dataList = this.dataList.state || []
+      let data = this.types.reduce((t, c) => 
+        t.concat(
+          {_title: c.name},
+          dataList.filter(item => c.name === item.attachmentType)
+        )
+      , [])
+
+      
+      data = data.concat(dataList.filter(item => !data.includes(item)))
+      return data
+    }
+  },
+  components: {
+    "com-dialog": comDialog,
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+
+.title {
+  font-weight: bold;
+  color: #26559B;
+  line-height: 19px;
+  font-size: 14px;
+}
+</style>
+
+<style>
+.upload-demo .el-upload {
+  line-height: initial;
+}
+
+.arch-name .el-input__inner{
+  padding-right: 50px;
+}
+</style>

+ 789 - 0
src/view/dispatch/index.vue

@@ -0,0 +1,789 @@
+<template>
+  <com-head :options="headList" showCtrl>
+    <el-form label-width="84px" inline="true" >
+      <el-form-item label="项目编号:">
+        <el-input v-model="search.state.projectSn" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="起火对象:">
+        <el-input v-model="search.state.projectName" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="起火地址:">
+        <el-input v-model="search.state.projectAdrress" placeholder="请输入起火地址"></el-input>
+      </el-form-item>
+      <el-form-item label="起火场所:">
+        <el-cascader
+          style="width: 100%"
+          v-model="sprojectSite"
+          placeholder="起火场所"
+          :options="[{label: '全部', value: ''}].concat(place)"
+          :props="{ expandTrigger: 'hover', checkStrictly: true }"></el-cascader>
+      </el-form-item>
+      <el-form-item label="承办单位:">
+        <com-company v-model="search.state.organizerDeptId" />
+      </el-form-item>
+      <el-form-item label="事故日期:">
+        <el-date-picker type="date" v-model="queryAccidentDate" style="width: 100%"></el-date-picker>
+      </el-form-item>
+      <el-form-item label="火灾原因:">
+        <el-cascader
+          style="width: 100%"
+          v-model="sfireReason"
+          placeholder="火灾原因:"
+          :options="[{label: '全部', value: ''}].concat(reason)"
+          :props="{ expandTrigger: 'hover', checkStrictly: true }"></el-cascader>
+      </el-form-item>
+      <el-form-item label="项目状态:">
+        <el-select placeholder="请选择" v-model="search.state.status" showAll>
+          <el-option value="" label="全部"></el-option>
+          <el-option value="1" label="已认定"></el-option>
+          <el-option value="0" label="未认定"></el-option>
+        </el-select>
+      </el-form-item>
+
+      <el-form-item class="searh-btns" style="grid-area:1/4/4/5">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="search.reset">重置</el-button>
+      </el-form-item>
+      <el-form-item class="searh-btns">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+  <div class="body-layer">
+    <div class="body-head">
+      <h3 style="visibility: hidden;">项目列表</h3>
+      <div class="table-ctrl-right">
+        <template v-if="isTeaching">
+          <el-button type="primary" @click="revokeTeaching()" v-if="auth.setting" :disabled="!dataList.selectRows.length">撤销教学</el-button>
+        </template>
+        <template v-else>
+          <el-button type="primary" @click="insertProject">新增火调项目</el-button>
+          <el-button type="primary" @click="setTeaching" v-if="auth.setting" :disabled="!dataList.selectRows.length">设为教学项目</el-button>
+        </template>
+      </div>
+    </div>
+    <el-table
+      ref="multipleTable"
+      :data="dataList.state"
+      id="multipleTable"
+      tooltip-effect="light"
+      style="width: 100%"
+      class="table"
+      @selection-change="dataList.changeSelectRows"
+    >
+      <el-table-column type="selection" width="50"></el-table-column>
+      <el-table-column label="序号" width="50" v-slot:default="{ $index }">
+        <div style="text-align: center">{{ pag.state.size * (pag.state.currPage - 1) + $index + 1 }}</div>
+      </el-table-column>
+      <el-table-column label="项目编号" prop="projectSn"></el-table-column>
+      <el-table-column label="起火对象" prop="projectName" v-slot:default="{ row }" >
+        <el-tooltip class="item" effect="light" :content="row.projectName" placement="bottom-start" v-if="row.projectName && row.projectName.length > 15">
+          <p class="tip oper-user">{{row.projectName}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{row.projectName}}</p>
+      </el-table-column>
+      <el-table-column label="起火地址" prop="projectAddress" v-slot:default="{ row }" >
+        <el-tooltip class="item" effect="light" :content="row.projectAddress" placement="bottom-start" v-if="row.projectAddress && row.projectAddress.length > 15">
+          <p class="tip oper-user">{{row.projectAddress}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{row.projectAddress}}</p></el-table-column>
+      <el-table-column label="起火场所" prop="projectSite" v-slot:default="{ row }" >
+        <el-tooltip class="item" effect="light" :content="row.projectSite" placement="bottom-start" v-if="row.projectSite && row.projectSite.length > 15">
+          <p class="tip oper-user">{{row.projectSite}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{row.projectSite}}</p></el-table-column>
+      <el-table-column label="承办单位" prop="organizerDeptName"></el-table-column>
+      <el-table-column label="事故日期" prop="accidentDate"></el-table-column>
+      <el-table-column label="火灾原因" prop="fireReason" v-slot:default="{ row }" >
+        <el-tooltip class="item" effect="light" :content="row.fireReason" placement="bottom-start" v-if="row.fireReason && row.fireReason.length > 15">
+          <p class="tip oper-user">{{row.fireReason}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{row.fireReason}}</p></el-table-column>
+      <el-table-column label="项目状态" v-slot:default="{ row }" >
+        {{row.status === 0 ? '未认定' : '已认定'}}
+      </el-table-column>
+      <el-table-column label="操作" v-slot:default="{ row }" :width="isTeaching ? 240 : 280">
+        <span class="oper-span" @click="queryDetail(row)">详情</span>
+        <span class="oper-span" @click="archivesHandle(row)">档案</span>
+        <span class="oper-span" @click="queryScene(row)">场景</span>
+        <template v-if="isTeaching">
+          <span class="oper-span" @click="leaveMsgHandle(row)">留言</span>
+          <span class="oper-span" 
+            @click=" user.info.id === ADMIN_USER_ID && revokeTeaching(row)" 
+            :class="{disable: user.info.id !== ADMIN_USER_ID}"
+            style="color: var(--primaryColor)">
+            撤销
+          </span>
+        </template>
+        <template v-else>
+          
+          <span class="oper-span" @click="row.vrLink && shareHandle(row)" :class="{disable: !row.vrLink}">分享</span>
+          <span class="oper-span" @click="auth.update && user.info.departmentId == row.organizerDeptId && editInfo(row)" :class="{disable: !(auth.update && user.info.departmentId == row.organizerDeptId)}">编辑</span>
+          <span class="oper-span" 
+            @click="auth.delete && user.info.departmentId == row.organizerDeptId && dataList.delete(row)" 
+            :class="{disable: !(auth.delete && user.info.departmentId == row.organizerDeptId)}"
+            style="color: var(--primaryColor)">
+          删除
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+  </div>
+  
+  <com-archives @referList="dataList.refer()" v-model:show="showArchives.show" v-if="showArchives.data && showArchives.show" :organizerDeptId="showArchives.organizerDeptId" :data="showArchives.data" />
+  
+  <com-dialog
+    title="分享"
+    enterText="复制链接及密码"
+    v-model:show="share.show"
+    @submit="copyShare"
+  >
+    <el-form ref="form" :model="form" label-width="80px" class="share-from">
+      <el-form-item label="链接">
+        <el-input :modelValue="share.shareLink" placeholder="请输入项目编号" disabled></el-input>
+      </el-form-item>
+      <el-form-item label="密码">
+        <el-input :modelValue="share.data.randCode" @input="val => filterPSW(val)" placeholder="请输入密码" style="width: 120px"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <p class="maker">注:链接可发送给有需要的人员,需使用密码访问。有效期<b style="color: #26559B">14</b>天,过期后不可访问。</p>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+
+  <com-dialog
+    :title="(oper.state.id ? '编辑' : '新增') + '火调项目'"
+    v-model:show="oper.state.show"
+    @submit="submit"
+  >
+    <el-form ref="form" :model="form" label-width="84px" class="camera-from">
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="项目编号" class="mandatory">
+            <el-input v-model="oper.state.projectSn" maxlength="18" placeholder="请输入项目编号"></el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="起火对象" class="mandatory">
+            <el-input v-model="oper.state.projectName" maxlength="50" placeholder="请输入起火对象"></el-input>
+          </el-form-item>
+        </el-col>
+      </div>
+      <el-form-item label="起火地址" class="mandatory">
+        <el-input v-model="oper.state.projectAddress" maxlength="50" placeholder="请输入起火地址"></el-input>
+      </el-form-item>
+      <el-form-item label="起火场所" class="mandatory">
+        <el-cascader
+          style="width: 100%"
+          v-model="projectSite"
+          placeholder="起火场所"
+          :options="place"
+          :props="{ expandTrigger: 'hover' }"></el-cascader>
+      </el-form-item>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="承办单位" class="mandatory">
+            <com-company :modelValue="oper.state.organizerDeptId" hideAll :notUpdate="true" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="承办人员" class="mandatory" placeholder="请输入承办人员">
+            <el-input v-model="oper.state.organizerUsers" maxlength="50"></el-input>
+          </el-form-item>
+        </el-col>
+      </div>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="事故日期" class="mandatory" placeholder="请选择事故日期">
+            <el-date-picker type="date" v-model="oper.state.accidentDate" style="width: 100%"></el-date-picker>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="火灾原因" class="mandatory">
+          <el-cascader
+            style="width: 100%"
+            v-model="fireReason"
+            placeholder="火灾原因:"
+            :options="reason"
+            :props="{ expandTrigger: 'hover' }"></el-cascader>
+          </el-form-item>
+        </el-col>
+      </div>
+      <el-form-item label="火调场景">
+        <el-select
+          v-model="oper.state.sceneNum"
+          filterable
+          remote
+          placeholder="请输入关键词"
+          :remote-method="searchScene"
+          clearable
+          :loading="searchLoading">
+          <el-option
+            v-for="item in searchOptions"
+            :key="item.sceneNum"
+            :label="item.title"
+            :value="item.sceneNum">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <p class="maker">注:请确保已在在“场景管理”制作火调场景</p>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+
+
+  <com-dialog
+    title="火调项目详情"
+    v-model:show="detail.show"
+    :hideFloor="true"
+    v-if="detail.data"
+    @submit="detail.show = false"
+  >
+    <el-form ref="form" :model="form" label-width="100px" class="dispatch-detial">
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="项目编号">
+            <el-tooltip class="item" effect="light" :content="detail.data.projectSn" placement="bottom-start" v-if="detail.data.projectSn.length > 15">
+              <p class="tip oper-user">{{detail.data.projectSn}}</p>
+            </el-tooltip>
+            <p class="tip" v-else>{{detail.data.projectSn}}</p>
+            
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="起火对象">
+            <el-tooltip class="item" effect="light" :content="detail.data.projectName" placement="bottom-start" v-if="detail.data.projectName.length > 15">
+              <p class="tip oper-user">{{detail.data.projectName}}</p>
+            </el-tooltip>
+            <p class="tip" v-else>{{detail.data.projectName}}</p>
+          </el-form-item>
+        </el-col>
+      </div>
+      <el-form-item label="起火地址">
+        <el-tooltip class="item" effect="light" :content="detail.data.projectAddress" placement="bottom-start" v-if="detail.data.projectAddress.length > 38">
+          <p class="tip oper-user">{{detail.data.projectAddress}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{detail.data.projectAddress}}</p>
+      </el-form-item>
+      <el-form-item label="起火场所">
+        <el-tooltip class="item" effect="light" :content="detail.data.projectSite" placement="bottom-start" v-if="detail.data.projectSite.length > 38">
+          <p class="tip oper-user">{{detail.data.projectSite}}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{detail.data.projectSite}}</p>
+      </el-form-item>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="承办单位">
+            <el-tooltip class="item" effect="light" :content="detail.data.organizerDeptName" placement="bottom-start" v-if="detail.data.organizerDeptName.length > 15">
+              <p class="tip oper-user">{{detail.data.organizerDeptName}}</p>
+            </el-tooltip>
+            <p class="tip" v-else>{{detail.data.organizerDeptName}}</p>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="承办人员">
+            <el-tooltip class="item" effect="light" :content="detail.data.organizerUsers" placement="bottom-start" v-if="detail.data.organizerUsers.length > 15">
+              <p class="tip oper-user">{{detail.data.organizerUsers}}</p>
+            </el-tooltip>
+            <p class="tip" v-else>{{detail.data.organizerUsers}}</p>
+          </el-form-item>
+        </el-col>
+      </div>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="事故日期">
+            <p class="tip">{{detail.data.accidentDate}}</p>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="火灾原因">
+            <el-tooltip class="item" effect="light" :content="detail.data.fireReason" placement="bottom-start" v-if="detail.data.fireReason.length > 15">
+              <p class="tip oper-user">{{detail.data.fireReason}}</p>
+            </el-tooltip>
+            <p class="tip" v-else>{{detail.data.fireReason}}</p>
+          </el-form-item>
+        </el-col>
+      </div>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="项目状态">
+            {{detail.data.status === 0 ? '未认定' : '已认定'}}
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="是否教学项目">
+            {{detail.data.isTeached ? '是' : '否'}}
+          </el-form-item>
+        </el-col>
+      </div>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="创建人">
+            {{detail.data.creatorName}}
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="创建时间">
+            {{detail.data.createTime}}
+          </el-form-item>
+        </el-col>
+      </div>
+      <div class="el-form-item">
+        <el-col :span="12">
+          <el-form-item label="编辑人">
+            {{detail.data.editorName}}
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="最新编辑时间">
+            {{detail.data.editTime}}
+          </el-form-item>
+        </el-col>
+      </div>
+      <el-form-item label="火调场景">
+        <a :href="detail.data.vrLink" target="_blank">{{detail.data.sceneName}}</a>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+  
+  <com-leave-msg v-if="showLeaveMsg.data && showLeaveMsg.show" v-model:show="showLeaveMsg.show" :data="showLeaveMsg.data" />
+  
+</template>
+
+<script>
+import { computed, reactive, ref, watch } from "vue";
+import getTableState from "@/state/tableRef";
+import user from "@/state/user";
+import auth from "@/state/viewAuth";
+import comDialog from "@/components/dialog";
+import comPagination from "@/components/pagination";
+import comLeaveMsg from "./leaveMsg";
+import comCompany from "@/components/company-select";
+import comHead from "@/components/head";
+import comArchives from "./archives";
+import { copyText, dateFormat } from '@/util'
+import {
+  getFireList,
+  insertFire,
+  updateFire,
+  setTeach,
+  fireSetPsw,
+  setUnTeach,
+  fireDetail,
+  getSceneByTitle,
+  getFirePsw
+} from '@/request/config'
+import {PLACE, REASON, ADMIN_USER_ID, getPlaceCode} from '@/constant'
+import {EPSW} from '@/constant/REG'
+
+import axios from 'axios';
+import { getApp } from '../../app';
+
+export default {
+  name: 'camera',
+  setup() {
+    const state = getTableState({
+      updateUrl: updateFire,
+      insertUrl: insertFire,
+      getUrl: getFireList,
+      searchAttr: { 
+        projectSn: '', projectName: '',
+        __projectSite: '', projectAdrress: '',
+        organizerDeptId: '', accidentDate: null,
+        status: '', projectSiteCode: '',
+        fireReason: '', sceneNum: '', organizerUsers: '',
+        isTeached: '', queryType: ''
+      },
+      operAttr: {
+        projectSn: '', projectName: '',
+        projectSite: '', projectAddress: '',
+        organizerDeptId: '', accidentDate: '',
+        projectSiteCode: '',
+        fireReason: '', sceneNum: '', organizerUsers: ''
+      },
+      delMsg: '删除火调项目,相关档案也会一并删除,确定要删除吗?'
+    });
+    const types = ref([{ name: "我的相机", value: 2 }])
+    const time = ref(null)
+    const showArchives = ref({show: false, data: 0})
+    const showLeaveMsg = ref({show: false, data: 0})
+    const share = ref({show: false, data: {randCode: ''} })
+    const detail = reactive({show: false, data: null})
+    const queryAccidentDate = ref(null)
+
+
+    watch(queryAccidentDate, () => {
+      if (queryAccidentDate.value) {
+        state.search.value.state.accidentDate = dateFormat(new Date(queryAccidentDate.value), 'yyyy-MM-dd')
+      } else {
+        state.search.value.state.accidentDate = null
+      }
+    })
+
+    watch(state.search.value, () => {
+      if (!state.search.value.state.accidentDate) {
+        queryAccidentDate.value = null
+      }
+      if (state.search.value.state.__projectSite) {
+        state.search.value.state.projectSiteCode = getPlaceCode(state.search.value.state.__projectSite)
+      } else {
+        state.search.value.state.projectSiteCode = '0'
+      }
+    })
+
+    const fireReason = computed({
+      get() {
+        return state.oper.value.state.fireReason.split('>')
+      },
+      set(val) {
+        state.oper.value.state.fireReason = val.join('>')
+      }
+    })
+    const projectSite = computed({
+      get() {
+        return state.oper.value.state.projectSite.split('>')
+      },
+      set(val) {
+        state.oper.value.state.projectSite = val.join('>')
+      }
+    })
+    const sfireReason = computed({
+      get() {
+        return state.search.value.state.fireReason.split('>')
+      },
+      set(val) {
+        state.search.value.state.fireReason = val.join('>')
+      }
+    })
+    const sprojectSite = computed({
+      get() {
+        return state.search.value.state.__projectSite.split('>')
+      },
+      set(val) {
+        state.search.value.state.__projectSite = val.join('>')
+      }
+    })
+
+
+    watch(time, () => {
+      if (time.value) {
+        state.search.value.state.startTime = dateFormat(new Date(time.value[0]), 'yyyy-MM-dd hh:mm:ss')
+        state.search.value.state.endTime = dateFormat(new Date(time.value[1]), 'yyyy-MM-dd 23:59:59')
+      } else {
+        state.search.value.state.startTime = state.search.value.state.endTime = ''
+      }
+    })
+    watch(
+      [() => state.search.value.state.startTime, () => state.search.value.state.endTime],
+      () => {
+        if (!state.search.value.state.startTime || !state.search.value.state.endTime) {
+          time.value = null
+        }
+      }
+    )
+    watch(state.oper.value, () => {
+      if (new Date(state.oper.value.state.accidentDate).getTime() > Date.now()) {
+        state.oper.value.state.accidentDate = null
+        return getApp().$message.error('事故日期不正确,必须在当日之前!')
+      }
+    })
+
+    
+
+    return { 
+      ...state, time, auth, types,  user,
+      showArchives, showLeaveMsg, sfireReason, 
+      sprojectSite, share, fireReason, ADMIN_USER_ID,
+      projectSite, place: PLACE, reason: REASON, queryAccidentDate,
+      detail, __init: {...state.search.value.state}
+    };
+  },
+  data() {
+    return {
+      searchLoading: false, 
+      searchOptions: []
+    }
+  },
+  methods: {
+    filterPSW(val) {
+      if (val.length > 4) {
+        return;
+      } else if(EPSW.REG.test(val.substr(val.length - 1))) {
+        this.share.data.randCode = val
+      } else {
+        this.$message.error(EPSW.tip, '提示')
+      }
+      
+    },
+    editInfo(row) {
+      this.searchScene('')
+      this.oper.readyUpdate(row)
+      if (row.sceneNum) {
+        this.searchOptions = [
+          {
+            sceneNum: row.sceneNum,
+            title: row.sceneName,
+            vrLink: row.vrLink
+          }
+        ]
+      }
+    },
+    insertProject() {
+      this.oper.readyInsert()
+      this.oper.state.organizerDeptId = user.value.info.departmentId
+      // 
+      this.searchScene('')
+    },
+    async searchScene(query) {
+      this.searchLoading = true
+      try {
+        let res = await axios.get(getSceneByTitle, {
+          params: {
+            sceneTitle: query, 
+            pageNum: 1,
+            pageSize: 20
+          }
+        })
+
+        this.searchOptions = res.data.list
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.searchLoading = false
+      }
+    },
+    archivesHandle(row) {
+      this.showArchives.data = row.id
+      this.showArchives.organizerDeptId = row.organizerDeptId
+      this.showArchives.show = true
+    },
+    submit() {
+      if (!this.oper.state.projectAddress || !this.oper.state.projectAddress.trim()) {
+        return this.$message.error('起火地址不能为空!', '提示')
+      } else if (!this.oper.state.projectSn || !this.oper.state.projectSn.trim()) {
+        return this.$message.error('项目编号不能为空!', '提示')
+      } else if (!this.oper.state.projectName || !this.oper.state.projectName.trim()) {
+        return this.$message.error('起火对象不能为空!', '提示')
+      } else if (!this.oper.state.projectSite || !this.oper.state.projectSite.trim()) {
+        return this.$message.error('起火场所不能为空!', '提示')
+      } else if (!this.oper.state.organizerDeptId || !this.oper.state.organizerDeptId.trim()) {
+        return this.$message.error('承办单位不能为空!', '提示')
+      } else if (!this.oper.state.organizerUsers || !this.oper.state.organizerUsers.trim()) {
+        return this.$message.error('承办人员不能为空!', '提示')
+      } else if (!this.oper.state.accidentDate) {
+        return this.$message.error('事故日期不能为空!', '提示')
+      } else if (!this.oper.state.fireReason || !this.oper.state.fireReason.trim()) {
+        return this.$message.error('火灾原因不能为空!', '提示')
+      }
+
+      if (typeof this.oper.state.accidentDate === 'object') {
+        this.oper.state.accidentDate = dateFormat(this.oper.state.accidentDate, 'yyyy-MM-dd')
+      }
+
+      this.oper.state.projectSiteCode = getPlaceCode(this.oper.state.projectSite)
+
+      let add = {...this.oper.state}
+      if (this.oper.state.sceneNum) {
+        let item = this.searchOptions.find(({sceneNum}) => sceneNum === this.oper.state.sceneNum)
+        if (item) {
+          add.sceneNum = item.sceneNum
+          add.vrLink = item.vrLink
+        }
+      }
+
+      this.oper.state.id ? this.oper.update(add) : this.oper.insert(add)
+    },
+    leaveMsgHandle(row) {
+      this.showLeaveMsg.data = row.id
+      this.showLeaveMsg.show = true
+    },
+    async queryDetail(data) {
+      let res = await axios.get(fireDetail, {params: {projectId: data.id}})
+      this.detail.data = res.data
+      this.detail.show = true
+    },
+    async setTeaching() {
+      if (!(await this.$confirm('将火调场景设为教学项目后,所有用户均可查看。设置后可在教学平台取消设置。', '设为教学项目'))) {
+        return
+      }
+      let items = this.dataList.selectRows
+      await axios.post(setTeach, {ids: items.map(({id}) => id).join(',')})
+      this.dataList.refer()
+      this.$message({
+        type: 'success',
+        message: '已成功设置' +items.length +'个火调项目到教学平台,已设置的项目不重复设置。',
+      })
+    },
+    async revokeTeaching(data) {
+      if (!(await this.$confirm('撤销教学,火调项目将不再显示在教学平台。(火调项目不会删除)', '撤销教学'))) {
+        return
+      }
+      let items = data ? [data] : this.dataList.selectRows
+
+      console.log(this.dataList.selectRows)
+      await axios.post(setUnTeach, {ids: items.map(({id}) => id).join(',')})
+      this.dataList.refer()
+      this.$message({
+        type: 'success',
+        message: items.map(({projectSn}) => projectSn).join(',')+ '已成功从教学项目撤销',
+      })
+    },
+    queryScene(item) {
+      if (!item.vrLink) {
+        return this.$message.error('当前火调项目暂未录入场景链接', '提示')
+      } else {
+      window.open(process.env.VUE_APP_DOMAIN + '/spc.html?m=' + item.sceneNum)
+        // this.$router.push({name: 'scene', params: {projectId: item.id}})
+      }
+    },
+    async copyShare() {
+      if (!this.share.data.randCode || this.share.data.randCode.length !== 4) {
+        return this.$message.error('请输入四位数得密码!', '提示')
+      }
+      if (this.randCode !== this.share.data.randCode) {
+        await axios.post(fireSetPsw, {projectId: this.share.data.id, randCode: this.share.data.randCode})
+      }
+      copyText(`链接:${this.share.shareLink} 密码:${this.share.data.randCode}`)
+      this.$message('链接复制成功');
+      this.share.show = false
+    },
+    async shareHandle(item) {
+      let res = await axios.get(getFirePsw, {params: {projectId: item.id}})
+      
+      this.share.show = true
+      this.share.data = item
+      this.share.data.randCode = res.data.randCode
+      this.randCode = res.data.randCode
+      this.share.shareLink = location.origin + location.pathname + '#scene/' + item.id
+    },
+    reset() {
+      let queryType = this.search.state.queryType
+      let isTeached = this.search.state.isTeached
+      this.search.reset()
+      this.search.state.queryType = queryType
+      this.search.state.isTeached = isTeached
+    }
+  },
+  computed: {
+    isTeaching() {
+      return this.$route.name === 'teaching'
+    },
+    headList() {
+      return this.isTeaching ? [{ name: "教学平台", value: 2 }] : [{ name: "火调管理", value: 2 }]
+    }
+  },
+  mounted() {
+    document.querySelector('#multipleTable .el-table__body-wrapper')
+      .addEventListener('scroll', () => {
+        Array.from(document.querySelectorAll('.table .tip'))
+          .forEach(dom => {
+            let $tip = document.querySelector('#' + dom.getAttribute('ariadescribedby'))
+
+            if ($tip) {
+              $tip.style.display = 'none'
+            }
+          })
+      })
+  },
+  watch: {
+    isTeaching: {
+      immediate: true,
+      handler(newV, oldV) {
+        console.log('---', this.isTeaching, newV)
+        let newKey = newV ? '__teaching': '__project'
+        let oldKey = oldV ? '__teaching': '__project'
+
+        this[oldKey] = { ...this.search.state }
+        let oldData = this[newKey] || this.__init
+        for (let [key, val] of Object.entries(oldData)) {
+          this.search.state[key] = val
+        }
+        this.search.state.queryType = this.isTeaching ? 2 : 1
+        this.search.state.isTeached = this.isTeaching ? 1 : ''
+      }
+    }
+  },
+  components: {
+    "com-dialog": comDialog,
+    "com-company": comCompany,
+    "com-head": comHead,
+    "com-leave-msg": comLeaveMsg,
+    "com-pagination": comPagination,
+    "com-archives": comArchives
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.camera-from {
+  margin: 0 50px;
+}
+.share-from {
+  width: 500px;
+  margin: 0 auto;
+}
+.maker {
+  font-weight: 400;
+  color: #969799;
+  line-height: 20px;
+}
+.oper-user {
+  height: 40px;
+  overflow:hidden; //超出的文本隐藏
+  text-overflow:ellipsis; //溢出用省略号显示
+  white-space:nowrap; //溢出不换行
+  cursor: pointer;
+}
+</style>
+
+<style>
+.camera-from  .el-select,
+.dispatch-file-from .el-select {
+  width: 100%;
+}
+
+.dispatch-detial .el-form-item__content {
+  color: #323233;
+}
+.dispatch-detial .el-form-item__label {
+  color: #646566;
+}
+
+.table .tip {
+ text-overflow: -o-ellipsis-lastline;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  -webkit-box-orient: vertical;
+  height: 50px;
+  white-space: initial
+}
+</style>

+ 171 - 0
src/view/dispatch/leaveMsg.vue

@@ -0,0 +1,171 @@
+<template>
+  <com-dialog
+    title="留言"
+    enterText="发表留言"
+    :show="show"
+    @submit="oper.readyInsert"
+    @update:show="val => $emit('update:show', val)"
+  >
+    <div class="body-layer" style="padding: 0">
+      <el-table
+        ref="multipleTable"
+        :data="dataList.state"
+        tooltip-effect="dark"
+        style="width: 100%;min-height:540px;max-height:540px"
+        :span-method="arraySpanMethod"
+      >
+        <el-table-column label="留言内容"  prop="content" v-slot:default="{ row }">
+          
+          <el-tooltip class="item" effect="light" :content="row.content" placement="bottom-start" v-if="row.content && row.content.length > 30">
+            <a class="msg-content">
+              {{row.content.substr(0, 30)}}...
+            </a>
+          </el-tooltip>
+          <a class="msg-content" v-else>
+            {{row.content}}
+          </a>
+          
+          
+        </el-table-column>
+        <el-table-column label="创建人" prop="userName" width="150"></el-table-column>
+        <el-table-column label="创建日期" prop="createTime" v-slot:default="{row}" width="150">
+          {{dateFormat(row.createTime)}}
+        </el-table-column>
+      </el-table>
+
+      <com-pagination
+        @size-change="pag.sizeChange"
+        @current-change="pag.currentChange"
+        :current-page="pag.state.currPage"
+        :sizes="[10, 20]"
+        :page-size="pag.state.size"
+        :hide-sizes="true"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="pag.state.total"/>
+    </div>
+  </com-dialog>
+
+  
+  <com-dialog
+    title="发表留言"
+    v-model:show="oper.state.show"
+    enterText="确 定"
+    width="540"
+    @submit="submit"
+  >
+    <el-form ref="form" :model="form" label-width="84px" class="leave-from">
+      <el-input
+        type="textarea"
+        placeholder="请输入留言内容,限200字"
+        maxlength="200"
+        show-word-limit
+        v-model="oper.state.content">
+      </el-input>
+    </el-form>
+  </com-dialog>
+
+</template>
+
+<script>
+import getTableState from "@/state/tableRef";
+import {
+  getMessageList,
+  insertMessage,
+} from '@/request/config'
+import comDialog from "@/components/dialog";
+import comPagination from "@/components/pagination";
+import { watch } from 'vue';
+import { dateFormat } from '@/util'
+
+export default {
+  name: 'archives',
+  props: ['show', 'data'],
+  setup(props) {
+    console.log(props)
+    const state = getTableState({
+      getUrl: getMessageList,
+      insertUrl: insertMessage,
+      searchAttr: {pageSize: 10, projectId: props.data},
+      operAttr: {
+        content: '',
+        projectId: props.data
+      },
+    });
+    
+    watch(props, () => {
+      if (props.data === state.search.value.state.projectId) return
+      state.search.value.state.projectId = props.data
+      state.dataList.value.refer()
+    })
+
+    return { ...state, dateFormat(date) {
+      // console.log()
+      return dateFormat(new Date(date), 'yyyy-MM-dd hh:mm')
+    } };
+  },
+  methods: {
+    async submit() {
+        this.oper.state.projectId = this.search.state.projectId
+        if (!this.oper.state.content.trim()) {
+          return this.$message.error('留言内容不能为空!', '提示')
+        }
+        try {
+          await this.oper.state.id ? this.oper.update() : this.oper.insert()
+        } catch {
+          console.error('留言失败')
+        }
+    }
+  },
+  components: {
+    "com-pagination": comPagination,
+    "com-dialog": comDialog,
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.leave-from {
+  // width: 520px;
+  margin: 0 auto;
+}
+
+.title {
+  font-weight: bold;
+  color: #26559B;
+  line-height: 19px;
+  font-size: 14px;
+}
+
+.msg-content {
+  overflow:hidden; //超出的文本隐藏
+  text-overflow:ellipsis; //溢出用省略号显示
+  white-space:nowrap; //溢出不换行
+  position: relative;
+  cursor: pointer;
+}
+</style>
+
+<style>
+.leave-from textarea {
+  height: 200px;
+}
+.el-popper:not(.el-cascader__dropdown) .el-popper__arrow {
+  /* display: none !important; */
+}
+</style>

+ 38 - 0
src/view/home/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <com-head :options="headList" class="frame-head" not-content />
+  <div class="home-layer">
+    <!-- <div class="home-content">
+      <img src="@/assets/image/logo_big.png" alt="">
+      <h1>广东省消防救援总队火场三维数据平台</h1>
+      <p>Three-dimensional data platform of fire scene of Guangdong Fire Resue</p>
+    </div> -->
+    <div class="foot">
+      <p> 广东省消防救援总队火场三维数据平台</p>
+      <p>珠海市四维时代网络科技有限公司</p>
+      <p>技术支持</p>
+    </div>
+  </div>
+</template>
+
+
+<script>
+import { ref } from "vue";
+import comHead from "@/components/head";
+
+
+export default {
+  name: 'framework',
+  setup() {
+    const headList = ref([{ name: "首页", value: 1 }]);
+    
+    return { headList };
+  },
+  components: {
+    "com-head": comHead,
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import "./style.scss"
+</style>

+ 54 - 0
src/view/home/style.scss

@@ -0,0 +1,54 @@
+.home-layer {
+  flex: 1;
+  margin-top: 8px;
+}
+
+.home-layer {
+  position: relative;
+  background: url('~@/assets/image/home_bg.png') no-repeat center center;
+  background-size: cover;
+}
+
+.home-content {
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  top: 80px;
+  text-align: center;
+
+  img {
+    width: 220px;
+    margin-bottom: 60px;
+  }
+
+  h1 {
+    color: #FFFFFF;
+    line-height: 52px;
+    text-shadow: 0px 0px 6px rgba(0, 0, 0, 0.4);
+    font-size: 40px;
+  }
+
+  p {
+    line-height: 31px;
+    text-shadow: 0px 0px 6px rgba(0, 0, 0, 0.4);
+    font-size: 24px;
+    color: #fff;
+    margin-top: 10px;
+  }
+
+}
+
+.foot{
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+
+  p {
+    font-size: 12px;
+    color: #FFFFFF;
+    line-height: 16px;
+    margin-bottom: 10px;
+    text-align: center;
+  }
+}

+ 91 - 0
src/view/layout/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="layer">
+    <ly-top class="top" v-if="!isSystem" />
+    <div class="content">
+      <router-view v-slot="{ Component }" v-if="isSystem">
+        <component :is="Component" />
+      </router-view>
+      <template v-else>
+        <ly-slide class="slide" />
+        <div class="view">
+          <div class="main">
+            <router-view v-slot="{ Component }">
+              <keep-alive >
+                <component :is="Component" />
+              </keep-alive>
+            </router-view>
+          </div>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import lyTop from './top'
+import lySlide from './slide'
+import { routeIsSystem } from '@/router'
+
+
+export default {
+  computed: {
+    isSystem() {
+      return routeIsSystem(this.$route)
+    }
+  },
+  components: {
+    'ly-top': lyTop,
+    'ly-slide': lySlide,
+    
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.layer {
+  position: absolute;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .top {
+    flex: 0 0 auto;
+    height: 5.33rem;
+    box-sizing: border-box;
+  }
+
+  .content {
+    flex: 1 0;
+    display: flex;
+    overflow: hidden;
+
+    .slide {
+      width: 16.66rem;
+      flex: 0 0 auto;
+    }
+
+    .view {
+      flex: 0 0 auto;
+      width: calc(100% - 16.66rem);
+      background-color: var(--bgColor);
+      flex-direction: column;
+      display: flex;
+
+      .player {
+        flex: 0 0 auto;
+        height: 32px;
+      }
+
+      .main {
+        margin: 0 16px 16px;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        flex: 1;
+      }
+    }
+  }
+}
+</style>

+ 64 - 0
src/view/layout/player/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="player">
+    <div
+      v-for="item in records"
+      :key="item.name"
+      :class="{active: item.name === $route.name}"
+      @click="item.name !== $route.name && gotoRecord(item)"
+      >
+      <span>{{item.meta.title}}</span>
+      <i class="el-icon-close" @click.stop="delRecord(item)"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue'
+
+export default {
+  setup() {
+    const records = ref([])
+    return { records }
+  },
+  watch: {
+    '$route': {
+      immediate: true,
+      handler() {
+        if (this.$route.name !== 'viewLayout') {
+          let index = this.records.findIndex(record => record.name === this.$route.name)
+          if (~index) {
+            this.records.splice(index, 1, this.$route)
+          } else {
+            this.records.push(this.$route)
+          }
+        }
+      }
+    }
+  },
+  methods: {
+    delRecord(record) {
+      let index = this.records.indexOf(record)
+      if (!~index) return;
+      this.records.splice(index, 1);
+
+      if (this.records.length <= index) {
+        index = index - 1
+      }
+
+      if (~index) {
+        this.$router.replace({path: this.records[index].path})
+      } else {
+        this.$router.push({name: 'viewLayout'})
+      }
+    },
+    gotoRecord(record) {
+      this.$router.push({path: record.path})
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+@import "./style.scss";
+</style>

+ 27 - 0
src/view/layout/player/style.scss

@@ -0,0 +1,27 @@
+.player {
+  background: #FFFFFF;
+  display: flex;
+
+  > div {
+    height: 100%;
+    font-size: 1rem;
+    color: #909399;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 4px 0 8px;
+    cursor: pointer;
+    border-right: 1px solid var(--bgColor);;
+    border-left: 1px solid var(--bgColor);;
+
+    i {
+      margin-left: 5px;
+      font-size: 1.1em;
+      color: #909399;
+    }
+
+    &.active {
+      background-color: var(--bgColor);;
+    }
+  }
+}

+ 39 - 0
src/view/layout/slide/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="slide">
+    <el-menu :default-active="activeName" @select="selectHandle">
+      <sub-menu :nav="nav" v-for="nav in navs" :key="nav.name" />
+    </el-menu>
+  </div>
+</template>
+
+<script>
+import subMenu from './submenu'
+import navs from '@/state/navs'
+import { attach } from '@/constant/view'
+
+export default {
+  setup() {
+    return { navs }
+  },
+  computed: {
+    activeName() {
+      let routeName = this.$route.name
+      return attach[routeName] ? attach[routeName] : routeName
+    }
+  },
+  methods: {
+    selectHandle(data) {
+      this.$router.push({name: data})
+    }
+  },
+  components: {
+    'sub-menu': subMenu
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.slide {
+  background: #FFFFFF;
+}
+</style>

+ 13 - 0
src/view/layout/slide/submenu.vue

@@ -0,0 +1,13 @@
+<template>
+    <el-menu-item :index="nav.name" :key="nav.name" >
+      <i :class="'iconfont ' + nav.icon" v-if="nav.icon"></i>
+      <span>{{nav.meta.title}}</span>
+    </el-menu-item>
+</template>
+
+<script>
+export default {
+  name: 'nav-sub-menu',
+  props: ['nav']
+}
+</script>

+ 172 - 0
src/view/layout/top/index.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="top" :class="{system: isSystem}">
+    <div class="title">
+      <img src="@/assets/image/img_login_logo.png">
+      <h2>广东省消防救援总队火场三维数据平台<span>Three-dimensional data platform of fire scene of Guangdong Fire Resue</span></h2>
+    </div>
+    <div class="oper-btns" v-if="!isSystem && info">
+        <div class="user-menu">
+          <img :src="info.avatar ? info.avatar : defAvatar">
+          <span>{{info.department && info.department.name}}</span>
+          <el-dropdown>
+            <div style="outline: none;">
+              <span class="oper-down">{{info.nickName}}</span>
+              <i class="el-icon-arrow-down el-icon--right"></i>
+            </div>
+          <template v-slot:dropdown>
+            <el-dropdown-menu class="menu-items-user">
+              <el-dropdown-item @click="data.show = true">修改密码</el-dropdown-item>
+              <el-dropdown-item @click="logout"><span style="color:#FA5555">退出登录</span></el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+        </div>
+
+      <!-- <div class="info">
+        <img src="@/assets/image/top_my.png">
+        <span>{{info.nickName}}</span>
+        <span>{{info.userName}}</span>
+      </div>
+      <span @click="data.show = true"><img src="@/assets/image/top_set.png"></span>
+      <span @click="logout"><img src="@/assets/image/top_exit.png"></span> -->
+    </div>
+  </div>
+  
+  <com-dialog title="修改密码" v-model:show="data.show" @submit="updatePsw" enterText="确 定" width="486">
+    <el-form ref="form" :model="data" label-width="90px">
+      
+      <div class="stop-psw">
+        <input type="text">
+        <input type="password" name="" id="">
+      </div>
+      
+      <el-form-item label="手机号:" class="mandatory">
+        <el-input v-model="data.phone" placeholder="请输入手机号码" disabled></el-input>
+      </el-form-item>
+      <el-form-item label="验证码:" class="mandatory">
+        <el-input v-model="data.code" placeholder="请输入验证码">
+        <template v-slot:suffix>
+          <el-button type="primary" plain class="input-inner-btn" @click="sendCode" :disabled="msgStatus.status !== 0">
+            {{msgStatus.status === 2 ? `${msgStatus.miss}S后可重新发送` : '获取验证码' }}
+          </el-button>
+        </template>
+      </el-input>
+      </el-form-item>
+      <el-form-item label="新密码:" class="mandatory">
+        <el-input v-model="data.password" type="password" placeholder="请输入8-16位数字、英文大小写组合"></el-input>
+      </el-form-item>
+      <el-form-item label="密码确认:" class="mandatory">
+        <el-input v-model="data.confimPsw" type="password" placeholder="请输入8-16位数字、英文大小写组合"></el-input>
+      </el-form-item>
+    </el-form>
+
+  </com-dialog>
+
+</template>
+<script>
+import { encryption } from '@/util'
+import comDialog from "@/components/dialog";
+import axios from 'axios';
+import { updatePsw, userLogout } from '@/request/config'
+import { sendUserMsg } from '@/request/config'
+import { setToken, setPermission, setInfo } from '@/state/user'
+import { computed, ref, reactive, watch } from 'vue'
+import user from '@/state/user'
+import {PSW} from '@/constant/REG'
+import { openErrorMsg } from '@/request/errorMsg.js'
+import defAvatar from '@/assets/image/top_my.png'
+
+export default {
+  props: ['isSystem'],
+  setup() {
+    const info = computed(() => user.value.info)
+    const data = ref({show: false, oldPassword: '', password: '', confimPsw: '', phone: info.value.userName, code: ''})
+    const msgStatus = reactive({status: 0, miss: 0})
+
+    watch(info, () => {
+      data.value.phone = info.value.userName
+    })
+
+    return { data, info, defAvatar, msgStatus }
+  },
+  methods: {
+    async updatePsw() {
+      if (this.data.password !== this.data.confimPsw) {
+        return this.$message.error('两次密码不一致!', '提示')
+      }
+      if (!PSW.REG.test(this.data.password)) {
+        return this.$message.error(PSW.tip, '提示')
+      }
+      
+      let psw = encryption(this.data.password)
+
+      await axios.post(updatePsw, {
+          userName: this.data.phone,
+          code: this.data.code,
+          password: psw,
+          confirmPwd: psw
+      })
+
+      this.data.show = false
+      this.data.oldPassword = this.data.password = this.data.confimPsw = ''
+
+      openErrorMsg('密码修改成功,请重新登录。')
+
+      this._loginout()
+    },
+    async _loginout() {
+      await axios.post(userLogout)
+      setToken('')
+      setPermission()
+      console.log('----------')
+      setInfo({})
+      this.$router.replace({name: 'login'})
+    },
+    async logout() {
+      if (await this.$confirm('确定要退出登录吗?', '提示')) {
+        this._loginout()
+      }
+    },
+    async sendCode() {
+      if (this.msgStatus.status !== 0) {
+        return;
+      }
+
+      this.msgStatus.status = 1
+      try {
+        await axios.get(sendUserMsg, {
+          params: {
+            areaNum: 86,
+            phoneNum: this.data.phone,
+            type: 2
+          }
+        })
+        this.msgStatus.status = 2
+        this.msgStatus.miss = 60
+        let interval = setInterval(() => {
+          if (--this.msgStatus.miss < 0) {
+            clearInterval(interval)
+            this.msgStatus.status = 0
+          }
+        }, 1000)
+
+      } catch {
+        this.msgStatus.status = 0
+      }
+    }
+  },
+  components: {
+    "com-dialog": comDialog,
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './style.scss';
+</style>
+
+<style>
+.menu-items-user {
+  transform: translate(-38px, -15px);
+}
+</style>

+ 91 - 0
src/view/layout/top/style.scss

@@ -0,0 +1,91 @@
+.top {
+  background-color: #C91D28;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 15px 40px;
+
+  &.system {
+    background-color: #fff;
+
+    .title h2 {
+      color: #323233;
+    }
+  }
+
+  .title {
+    display: flex;
+    align-items: center;
+
+    img {
+      width: 50px;
+    }
+
+    h2 {
+      font-size: 1.14rem;
+      color: #fff;
+      margin-left: 0.66rem;
+
+      span {
+        font-size: 0.7rem;
+        font-weight: normal;
+        line-height: 1rem;
+        display: block;
+      }
+    }
+  }
+
+  .oper-btns {
+    .user-menu {
+      display: flex;
+      align-items: center;
+
+      img {
+        width: 30px;
+        
+      }
+      span {
+        margin-left: 5px;
+        outline: none;
+        color: #fff;
+      }
+
+      .oper-down {
+        cursor: pointer;
+        + i {
+          color: #fff;
+        }
+      }
+    }
+
+
+    .info {
+      color: #fff;
+      margin-right: 14px;
+      font-size: 1rem;
+      margin-top: -3px;
+      display: flex;
+      align-items: center;
+
+      span {
+        font-weight: normal;
+        font-size: 0.9rem;
+      }
+
+      span:last-child {
+        margin-left: 8px;
+      }
+    }
+
+    > span {
+      display: inline-block;
+      margin-right: 14px;
+      cursor: pointer;
+
+      img {
+        width: 34px;
+        height: 34px;
+      }
+    }
+  }
+}

+ 180 - 0
src/view/scene/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="scene-layer">
+    <iframe :src="url" ></iframe>
+    <div class="deteil-layer" :class="{hide: !showInfo}">
+      <div class="ctrl" @click="showInfo = !showInfo">
+        <img src="@/assets/image/decoration_collect@2x.png" alt="">
+      </div>
+      
+      <div class="deteil">
+
+
+        <h2>火调详情</h2>
+
+        <div class="block">
+          <h3>基本信息</h3>
+
+          <div class="base-info">
+            <div>
+              <span>项目编号:</span>
+              <p>{{detail.projectSn}}</p>
+            </div>
+            <div>
+              <span>承办单位:</span>
+              <p>{{detail.organizerDeptName}}</p>
+            </div>
+            <div>
+              <span>承办人员:</span>
+              <p>{{detail.organizerUsers}}</p>
+            </div>
+            <div>
+              <span>事故日期:</span>
+              <p>{{detail.accidentDate}}</p>
+            </div>
+            <div>
+              <span>火灾原因:</span>
+              <p>{{detail.fireReason}}</p>
+            </div>
+            <div>
+              <span>项目状态:</span>
+              <p>{{detail.status === 0 ? '未认定' : '已认定'}}</p>
+            </div>
+            <div>
+              <span>是否教学项目:</span>
+              <p>{{detail.isTeached ? '是' : '否'}}</p>
+            </div>
+            <div>
+              <span>创建人:</span>
+              <p>{{detail.creatorName}}</p>
+            </div>
+            <div>
+              <span>编辑人:</span>
+              <p>{{detail.editorName}}</p>
+            </div>
+            <div>
+              <span>创建时间:</span>
+              <p>{{detail.createTime}}</p>
+            </div>
+            <div>
+              <span>最新编辑时间:</span>
+              <p>{{detail.editTime}}</p>
+            </div>
+          </div>
+        </div>
+
+        <div class="block">
+          <h3>火灾档案</h3>
+
+          <div class="attach">
+            <div
+              v-for="item in tableData"
+              :key="item._title"
+              class="type-item">
+              <h4>{{ item.title }}</h4>
+              <div class="addpend">
+                <span v-for="item in item.children" :key="item.id" @click="goto(item)" >
+                  {{ item.fileName }}
+                  <img src="@/assets/image/goto.png" alt="">
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <com-dialog title="访问密码" enterText="确 定" v-model:show="confirm.show" @submit="loadData()" width="480" :showClose="false">
+    <el-input v-model="confirm.psw" placeholder="请输入访问密码" @keydown.enter="loadData"></el-input>
+  </com-dialog>
+
+  <div v-if="showImg" class="img-layer">
+    <i class="el-icon-close" @click="showImg = ''"></i>
+    <img :src="showImg" alt="">
+  </div>
+</template>
+
+<script>
+import { fireDetailByPsw, getAttachListByPsw } from '@/request/config'
+import axios from 'axios';
+import comDialog from "@/components/dialog";
+import { types } from '@/constant'
+
+export default {
+  data() {
+    return { 
+      detail: {},
+      showImg: '',
+      confirm: {show: false, psw: ''},
+      list: [],
+      showInfo: false,
+      loadSuccess: false,
+    }
+  },
+  watch: {
+    'confirm.show'() {
+      console.log(this.confirm.show, !this.showInfo)
+      if (this.confirm.show || !this.showInfo) {
+        this.$router.back()
+      }
+    },
+  },
+  computed: {
+    url() {
+      if (!this.detail.sceneNum) {
+        return 'javascript:void(0)'
+      } else {
+        return process.env.VUE_APP_DOMAIN + '/spc.html?m=' + this.detail.sceneNum
+      }
+    },
+    tableData() {
+      let dataList = this.list || [];
+      let cache = [];
+      let data = types.reduce((t, c) => {
+        let children = dataList.filter(
+          (item) => c.name === item.attachmentType
+        );
+        t.push({ title: c.name, children });
+        cache.push(...children);
+        return t;
+      }, []);
+
+      data[data.length - 1].children.push(
+        ...dataList.filter((item) => !cache.includes(item))
+      );
+      return data;
+    },
+  },
+  methods: {
+    async loadData() {
+      // if (!this.confirm.psw || this.confirm.psw.length === 0) return this.$message.error('请输入访问密码', '提示')
+      let {data: detail} = await axios.get(fireDetailByPsw, {params: {projectId: this.$route.params.projectId, randCode: this.confirm.psw}})
+      this.detail = detail
+
+      let {data: list} = await axios.get(getAttachListByPsw, {params: {projectId: this.$route.params.projectId, randCode: this.confirm.psw}})
+      this.list = list
+
+      this.loadSuccess = true
+      this.confirm.show = false
+      this.showInfo = true
+    },
+    goto(item) {
+      if (item.type !== 1) {
+        window.open(item.fileOssUrl)
+      } else {
+        this.showImg = item.fileOssUrl
+      }
+    }
+  },
+  mounted() {
+    this.loadData()
+  },
+  components: {
+    "com-dialog": comDialog,
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "./style.scss";
+</style>

+ 211 - 0
src/view/scene/style.scss

@@ -0,0 +1,211 @@
+.scene-layer {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+
+  iframe {
+    width: 100%;
+    height: 100%;
+    border: 0;
+    outline: none;
+  }
+
+  .deteil-layer {
+    position: absolute;
+    --bgColor: rgba(0, 0, 0, 0.5);
+    width: 100%;
+    max-width: 380px;
+    height: 100%;
+    left: 0;
+    top: 0;
+    overflow-y: auto;
+    transition: transform .3s linear;
+
+    &::-webkit-scrollbar {
+      display: none;
+    }
+
+    
+    .ctrl {
+      position: absolute;
+      right: 6px;
+      top: 50%;
+      transform: translateY(-50%);
+      background: var(--bgColor);
+      width: 14px;
+      height: 40px;
+      cursor: pointer;
+      
+      img {
+        position: absolute;
+        width: 10px;
+        height: 10px;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        transition: transform .3s linear;
+      }
+
+      &::after,
+      &::before{
+        content: '';
+        position: absolute;
+        width: 0;
+        height: 0;
+        border: 10px solid transparent;
+      }
+
+      &::before {
+        left: -10px;
+        top: -10px;
+        transform: rotate(45deg);
+        border-top-color: var(--bgColor);
+      }
+
+      &::after {
+        left: -10px;
+        bottom: -10px;
+        transform: rotate(-45deg);
+        border-bottom-color: var(--bgColor);
+      }
+    }
+    
+  
+
+    &.hide {
+      // left: -380px;
+      transform: translateX(calc(-100% + 20px));
+
+
+      .deteil {
+        // transform: translateX(-100%);
+      }
+
+      .ctrl img {
+        transform: translate(-50%, -50%) rotateZ(180deg);
+      }
+    }
+  }
+
+  .deteil {
+    position: absolute;
+    left: 0;
+    top: 0;
+    min-height: 100%;
+    padding: 20px;
+    width: calc(100% - 20px);
+    background: var(--bgColor);
+    transition: transform .3s linear;
+    box-sizing: border-box;
+    
+  
+
+
+    h2 {
+      font-size: 20px;
+      font-weight: 400;
+      color: #FFFFFF;
+      line-height: 28px;
+      padding-bottom: 11px;
+    }
+
+    .block {
+      margin-top: 20px;
+      background: rgba(0, 0, 0, 0.5);
+      padding: 15px 20px;
+
+      h3 {
+        font-size: 16px;
+        font-weight: 600;
+        color: #00C8AF;
+        line-height: 22px;
+      }
+
+      > div {
+        margin-top: 10px;
+      }
+    }
+  }
+}
+
+.base-info div {
+  display: flex;
+  margin-top: 12px;
+
+  span {
+    flex: none;
+    width: 94px;
+    font-size: 14px;
+    font-weight: 400;
+    color: rgba(255,255,255,0.5);
+    line-height: 20px;
+  }
+
+  p {
+    font-size: 14px;
+    font-weight: 400;
+    color: #FFFFFF;
+  }
+}
+
+.attach div:not(:last-child) {
+  margin-bottom: 30px;
+}
+
+.attach {
+  margin-top: 24px !important;
+}
+.attach div {
+  h4 {
+    font-size: 14px;
+    color: rgba(255,255,255,0.5);
+    font-weight: 400;
+    line-height: 20px;
+  }
+
+  .addpend {
+    span {
+      margin-top: 20px;
+      display: flex;
+      justify-content: space-between;
+      color: #fff;
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 20px;
+      word-break: break-all;
+
+      img {
+        width: 12px;
+        align-self: center;
+      }
+    }
+  }
+}
+
+.img-layer {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  background: rgba(0,0,0,0.8);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  img {
+    max-width: 100%;
+    max-height: 100%;
+  }
+
+  .el-icon-close {
+    font-size: 2rem;
+    color: #fff;
+    cursor: pointer;
+    position: absolute;
+    top: 10px;
+    right: 10px;
+  }
+}

+ 177 - 0
src/view/system/forget.vue

@@ -0,0 +1,177 @@
+<template>
+  <el-form class="panel" :model="form" @submit.stop label-width="70px">
+    
+    <div class="stop-psw">
+      <input type="text">
+      <input type="password" name="" id="">
+    </div>
+    
+    <h2>重置密码</h2>
+    <el-form-item class="panel-form-item" label="手机号">
+      <p class="err-info">{{verification.phone}}</p>
+      <el-input v-model="form.phone" placeholder="请输入手机号码"></el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="验证码">
+      <p class="err-info">{{verification.code}}</p>
+      <el-input v-model="form.code" placeholder="请输入验证码">
+        <template v-slot:suffix>
+          <el-button type="primary" plain class="input-inner-btn" @click="sendCode" :disabled="msgStatus.status !== 0">
+            {{msgStatus.status === 2 ? `${msgStatus.miss}S后可重新发送` : '获取验证码' }}
+          </el-button>
+        </template>
+      </el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="重置密码">
+      <p class="err-info">{{verification.psw}}</p>
+      <el-input v-model="form.psw" placeholder="请输入8-16位数字、英文大小写组合密码" type="password"></el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="确认密码">
+      <p class="err-info">{{verification.regPsw}}</p>
+      <el-input v-model="form.regPsw" placeholder="请确认密码" type="password"></el-input>
+    </el-form-item>
+    
+    <div class="panel-form-item">
+      <el-button type="primary" class="fill" @click="submitClick">提交</el-button>
+    </div>
+
+    <div class="more">
+      <a @click="$router.replace({name: 'login'})">立即登录</a>
+    </div>
+  </el-form>
+</template>
+
+<script>
+import { reactive, watch } from 'vue'
+import axios from 'axios'
+import { updatePsw, sendUserMsg } from '@/request/config'
+import { PHONE, PSW } from '@/constant/REG'
+import { openErrorMsg } from '@/request/errorMsg.js'
+import { encryption, guid } from '@/util'
+
+export default {
+  name: 'login',
+  setup() {
+    const form = reactive({
+      phone: '',
+      code: '',
+      psw: '',
+      regPsw: '',
+    })
+    const verification = reactive({ 
+      phone: '',
+      code: '',
+      psw: '',
+      regPsw: '',
+    })
+    const msgStatus = reactive({status: 0, miss: 0})
+
+    const checkForm = (isForce) => {
+      verification.phone = ''
+      verification.code = ''
+      verification.psw = ''
+      verification.regPsw = ''
+
+      if (!form.phone) {
+        isForce && (verification.phone = '请输入手机号')
+      } else {
+        verification.phone = PHONE.REG.test(form.phone) ? '': PHONE.tip
+      }
+      
+      if (!form.code) {
+        isForce && (verification.code = '请输入验证码')
+      } else if (form.code.length !== 6){
+        verification.code = '验证码不合法'
+      }
+      
+      if (!form.psw) {
+        isForce && (verification.psw = '请输入密码')
+      } else {
+        verification.psw = PSW.REG.test(form.psw) ? '': PSW.tip
+      }
+      
+      if (!form.regPsw) {
+        isForce && (verification.regPsw = '请输入确认密码')
+      } else if (form.psw !== form.regPsw){
+        verification.regPsw = '密码不一致'
+      }
+    }
+
+    watch(form, () => checkForm())
+
+    return {
+      form,
+      verification,
+      msgStatus,
+      checkForm
+    }
+  },
+  methods: {
+    async submitClick(ev) {
+      this.checkForm(true)
+
+      ev.stopPropagation()
+
+      for (let val of Object.values(this.verification)) {
+        if (val) return openErrorMsg(val, '提示')
+      }
+
+      try {
+        let psw = encryption(this.form.psw)
+        await axios.post(updatePsw, {
+          userName: this.form.phone,
+          key: this.imgKey,
+          code: this.form.code,
+          password: psw,
+          confirmPwd: psw
+        })
+
+        this.$router.replace({ name: 'login' })
+      } catch {
+        // return this.refer()
+      }
+
+
+    },
+    async sendCode() {
+      if (!this.form.phone) {
+        (this.verification.phone = '请输入手机号')
+      } else {
+        this.verification.phone = PHONE.REG.test(this.form.phone) ? '': PHONE.tip
+      }
+
+      if (this.msgStatus.status !== 0) {
+        return;
+      }
+
+      this.msgStatus.status = 1
+      try {
+        await axios.get(sendUserMsg, {
+          params: {
+            areaNum: 86,
+            phoneNum: this.form.phone,
+            type: 2
+          }
+        })
+        this.msgStatus.status = 2
+        this.msgStatus.miss = 60
+        let interval = setInterval(() => {
+          if (--this.msgStatus.miss < 0) {
+            clearInterval(interval)
+            this.msgStatus.status = 0
+          }
+        }, 1000)
+
+      } catch {
+        this.msgStatus.status = 0
+      }
+    },
+    refer() {
+      this.imgKey = guid()
+    }
+  }
+}
+</script>
+
+<style lang="sass">
+@import "./style.scss"
+</style>

+ 55 - 0
src/view/system/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="system-layer">
+    <div class="content">
+      <component :is="$route.name" />
+    </div>
+  </div>
+</template>
+
+<script>
+import login from './login'
+import register from './register'
+import forget from './forget'
+
+export default {
+  components: {
+    login,
+    register,
+    forget
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.system-layer {
+  width: 100%;
+  min-height: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  background: url('~@/assets/image/img_loginbg.png') no-repeat left bottom;
+  background-size: cover;
+}
+
+.content {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+}
+</style>
+
+<style lang="scss">
+.system-layer{
+  .input-inner-btn {
+    border-color: #26559B !important;
+    color: #26559B !important;
+  }
+
+  .el-button--primary.is-plain:hover, .el-button--primary.is-plain:focus {
+    background: #26559B !important;
+    border-color: #26559B !important;
+    color: #FFFFFF !important;
+  }
+}
+</style>

+ 264 - 0
src/view/system/login.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="login-layer">
+    <div class="top-text">
+      <img src="@/assets/image/top-text.png" alt="">
+    </div>
+    <el-form class="panel login" :model="form" @submit.stop>
+      <h2>欢迎登录</h2>
+      <el-form-item class="panel-form-item">
+        <p class="err-info">{{verification.phone}}</p>
+        <el-input v-model="form.phone" placeholder="手机号" @keydown.enter="submitClick"></el-input>
+      </el-form-item>
+      <el-form-item class="panel-form-item">
+        <p class="err-info">{{verification.psw}}</p>
+        <el-input v-model="form.psw" placeholder="密码" type="password" @keydown.enter="submitClick"></el-input>
+      </el-form-item>
+      
+      <el-form-item class="panel-form-item code-form-item">
+        <p class="err-info">{{verification.code}}</p>
+        <el-input v-model="form.code" placeholder="验证码" @keydown.enter="submitClick" class="code-input">
+          <template v-slot:append>
+            <img :src="codeImg" class="code-img"  @click="refer">
+          </template>
+        </el-input>
+      </el-form-item>
+      
+      <el-form-item class="panel-form-item">
+        <el-button type="primary" class="fill" @click="submitClick">登录</el-button>
+      </el-form-item>
+
+      <div class="more">
+        <a @click="$router.push({name: 'forget'})">忘记密码</a>
+        <a @click="$router.push({name: 'register'})">账号注册</a>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { reactive, watch } from 'vue'
+import axios from 'axios'
+import { setToken, setPermission, setInfo } from '@/state/user'
+import { userLogin, getCode } from '@/request/config'
+import {PHONE} from '@/constant/REG'
+import { openErrorMsg } from '@/request/errorMsg.js'
+import { encryption, guid } from '@/util'
+
+export default {
+  name: 'login',
+  setup() {
+    const form = reactive({
+      phone: localStorage.getItem('userName') || '',
+      psw: localStorage.getItem('password') || '',
+      code: '',
+      remember: localStorage.getItem('remember') === '1'
+    })
+    const verification = reactive({ phone: '', psw: '', code: '' })
+
+  
+
+    watch(form, () => {
+      if (!form.phone) {
+        verification.phone = '请输入手机号'
+      } else {
+        verification.phone = PHONE.REG.test(form.phone) ? '': PHONE.tip
+      }
+      if (!form.psw) {
+        verification.psw = '请输入密码'
+      } else {
+        verification.psw = ''
+      }
+      if (!form.code.trim()) {
+        verification.code = '请输入验证码'
+      } else {
+        verification.code = ''
+      }
+    }, {immediate: true})
+
+    return {
+      form,
+      verification
+    }
+  },
+  methods: {
+    async submitClick(ev) {
+      ev.stopPropagation()
+
+      if (this.verification.phone) return openErrorMsg(this.verification.phone, '提示')
+      if (this.verification.psw) return openErrorMsg(this.verification.psw, '提示')
+      if (this.verification.code) return openErrorMsg(this.verification.code, '提示')
+
+      try {
+        // await axios.get(checkCode, {params: { code: this.form.code, key: this.imgKey }})
+        let res = await axios.post(userLogin, {
+          userName: this.form.phone,
+          key: this.imgKey,
+          code: this.form.code,
+          password: encryption(this.form.psw)
+        })
+
+        if (this.form.remember) {
+          localStorage.setItem('userName', this.form.phone)
+          localStorage.setItem('password', this.form.psw)
+          localStorage.setItem('remember', '1')
+        } else {
+          localStorage.setItem('userName', '')
+          localStorage.setItem('password', '')
+          localStorage.setItem('remember', '0')
+        }
+
+        if (!res.data.roles.length) {
+          setToken('')
+          setPermission([])
+          setInfo({})
+          openErrorMsg('当前账号无权限,请联系总队管理员处理。', '提示')
+        } else {
+          res.data.user.department = res.data.department
+          console.log('info', res.data.user)
+          setInfo(res.data.user)
+          setToken(res.data.token)
+          setPermission(res.data.roles[0].id)
+          this.$router.replace({ name: 'home' })
+        }
+      } catch (e) {
+        console.error(e)
+        return this.refer()
+      }
+
+
+    },
+    refer() {
+      this.imgKey = guid()
+    }
+  },
+  data() {
+    let url = process.env.VUE_APP_PREFIX + getCode
+
+    return {
+      baseImg: url,
+      imgKey: guid()
+    }
+  },
+  computed: {
+    codeImg() {
+      return this.baseImg + '?key=' + this.imgKey
+    }
+  }
+}
+</script>
+
+<style lang="sass">
+@import "./style.scss"
+</style>
+
+<style lang="scss" scoped>
+.login-layer {
+  text-align: right;
+}
+.info {
+  color: #fff;
+  margin-right: 143px;
+  padding-top: 80px;
+  padding-bottom: 80px;
+  flex: none;
+
+  img {
+    width: 76px;
+    height: 76px;
+  }
+  h1 {
+    font-size: 2.8rem;
+    line-height: 3.7rem;
+    margin-bottom: 0.7rem;
+  }
+  p {
+    font-size: 2rem;
+    line-height: 2.2rem;
+
+  }
+}
+
+.top-text {
+  margin-bottom: 50px;
+  pointer-events: none;
+  height: 153px;
+  min-width: 1200px;
+  img {
+    position: absolute;
+    right: 0;    
+  }
+}
+.login {
+  width: 320px;
+  padding: 40px 40px 30px;
+  position: relative;
+  display: inline-block;
+
+    
+
+  h2 {
+    padding-left: 0;
+    padding-bottom: 0;
+    border-bottom: none;
+    margin-bottom: 2.14rem;
+
+    span {
+      color: #646566;
+      font-size: 1.33rem;
+      margin-top: 0.71rem;
+      display: block;
+    }
+  }
+
+  
+  .panel-form-item {
+    padding-left: 0;
+    padding-right: 0;
+  }
+  
+  .more a:first-child::after {
+    content: '';
+    position: absolute;
+    right: -5px;
+    width: 1px;
+    height: 8px;
+    background: #DCDEE0;
+    top: 50%;
+    transform: translateY(-50%);
+  }
+}
+
+.code-img {
+  width: 100%;
+  height: 100%;
+  // object-fit: cover;
+}
+
+</style>
+
+<style>
+.login .code-form-item .el-input {
+  display: flex;
+}
+
+.login .code-form-item .el-input-group__append {
+  flex: none;
+  margin-left: 10px;
+  width: 95px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+  
+}
+
+.login .code-form-item .el-input__inner {
+  flex: 1;
+}
+.login .code-form-item .el-input-group__append,
+.login .code-form-item .el-input__inner {
+  border: 1px solid #DCDFE6;
+  border-radius: 4px;
+}
+
+</style>

+ 201 - 0
src/view/system/register.vue

@@ -0,0 +1,201 @@
+<template>
+  <el-form class="panel" :model="form" @submit.stop label-width="70px">
+    <h2>注册</h2>
+    
+    <div class="stop-psw">
+      <input type="text">
+      <input type="password" name="" id="">
+    </div>
+    
+    <el-form-item class="panel-form-item" label="选择架构">
+      <p class="err-info">{{verification.organize}}</p>
+      <com-company v-model="form.organize" hideAll />
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="姓名">
+      <p class="err-info">{{verification.name}}</p>
+      <el-input v-model="form.name" placeholder="填写写姓名,限15字" maxlength="15"></el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="手机号">
+      <p class="err-info">{{verification.phone}}</p>
+      <el-input v-model="form.phone" placeholder="请输入手机号码"></el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="验证码">
+      <p class="err-info">{{verification.code}}</p>
+      <el-input v-model="form.code" placeholder="请输入验证码">
+        <template v-slot:suffix>
+          <el-button type="primary" plain class="input-inner-btn" @click="sendCode" :disabled="msgStatus.status !== 0">
+            {{msgStatus.status === 2 ? `${msgStatus.miss}S后可重新发送` : '获取验证码' }}
+          </el-button>
+        </template>
+      </el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="设置密码">
+      <p class="err-info">{{verification.psw}}</p>
+      <el-input v-model="form.psw" placeholder="请输入8-16位数字、英文大小写组合密码" type="password"></el-input>
+    </el-form-item>
+    <el-form-item class="panel-form-item" label="确认密码">
+      <p class="err-info">{{verification.regPsw}}</p>
+      <el-input v-model="form.regPsw" placeholder="请确认密码" type="password"></el-input>
+    </el-form-item>
+    
+    <div class="panel-form-item">
+      <el-button type="primary" class="fill" @click="submitClick">注册</el-button>
+    </div>
+
+    <div class="more">
+      <a @click="$router.replace({name: 'login'})">已注册,去登录</a>
+    </div>
+  </el-form>
+</template>
+
+<script>
+import { reactive, watch } from 'vue'
+import axios from 'axios'
+import { userReg, sendUserMsg } from '@/request/config'
+import comCompany from "@/components/company-select";
+import { PHONE, PSW } from '@/constant/REG'
+import { openErrorMsg } from '@/request/errorMsg.js'
+import { encryption } from '@/util'
+
+export default {
+  name: 'login',
+  setup() {
+    const form = reactive({
+      organize: '',
+      name: '',
+      phone: '',
+      code: '',
+      psw: '',
+      regPsw: '',
+    })
+    const verification = reactive({ 
+      organize: '',
+      name: '',
+      phone: '',
+      code: '',
+      psw: '',
+      regPsw: '',
+    })
+    const msgStatus = reactive({status: 0, miss: 0})
+
+    const checkForm = (isForce) => {
+      verification.organize = ''
+      verification.name = ''
+      verification.phone = ''
+      verification.code = ''
+      verification.psw = ''
+      verification.regPsw = ''
+
+      if (!form.phone) {
+        isForce && (verification.phone = '请输入手机号')
+      } else {
+        verification.phone = PHONE.REG.test(form.phone) ? '': PHONE.tip
+      }
+      
+      if (!form.name) {
+        isForce && (verification.name = '请输入姓名')
+      } else if (form.name.length > 15) {
+        verification.name = '姓名不合法!'
+      }
+      
+      if (!form.organize) {
+        isForce && (verification.organize = '请选择组织架构')
+      }
+      
+      if (!form.code) {
+        isForce && (verification.code = '请输入验证码')
+      } else if (form.code.length !== 6){
+        verification.code = '验证码不合法'
+      }
+      
+      if (!form.psw) {
+        isForce && (verification.psw = '请输入密码')
+      } else {
+        verification.psw = PSW.REG.test(form.psw) ? '': PSW.tip
+      }
+      
+      if (!form.regPsw) {
+        isForce && (verification.regPsw = '请输入确认密码')
+      } else if (form.psw !== form.regPsw){
+        verification.regPsw = '密码不一致'
+      }
+    }
+
+    watch(form, () => checkForm())
+    
+
+    return {
+      form,
+      verification,
+      checkForm,
+      msgStatus
+    }
+  },
+  methods: {
+    async submitClick(ev) {
+      this.checkForm(true)
+
+      ev.stopPropagation()
+
+      for (let val of Object.values(this.verification)) {
+        if (val) return openErrorMsg(val, '提示')
+      }
+
+      let psw = encryption(this.form.psw)
+      try {
+        await axios.post(userReg, {
+          departmentId: this.form.organize,
+          nickName: this.form.name,
+          userName: this.form.phone,
+          key: this.imgKey,
+          code: this.form.code,
+          password: psw,
+          confirmPwd: psw,
+        })
+
+        this.$router.replace({ name: 'login' })
+      } catch {
+        // return this.refer()
+      }
+    },
+    async sendCode() {
+      for (let val of Object.values(this.verification)) {
+        if (val) return openErrorMsg(val, '提示')
+      }
+
+      if (this.msgStatus.status !== 0) {
+        return;
+      }
+
+      this.msgStatus.status = 1
+      try {
+        await axios.get(sendUserMsg, {
+          params: {
+            areaNum: 86,
+            phoneNum: this.form.phone,
+            type: 1
+          }
+        })
+        this.msgStatus.status = 2
+        this.msgStatus.miss = 60
+        let interval = setInterval(() => {
+          if (--this.msgStatus.miss < 0) {
+            clearInterval(interval)
+            this.msgStatus.status = 0
+          }
+        }, 1000)
+
+      } catch {
+        this.msgStatus.status = 0
+      }
+    }
+  },
+  components: {
+    "com-company": comCompany,
+  }
+}
+</script>
+
+<style lang="sass">
+@import "./style.scss"
+</style>

+ 81 - 0
src/view/system/style.scss

@@ -0,0 +1,81 @@
+.panel {
+  background: rgba(255,255,255,0.7);
+  box-shadow: 0px 2px 20px 0px rgba(5, 38, 38, 0.15);
+  border-radius: 10px;
+  width: 600px;
+  padding: 30px 0 40px;
+  text-align: initial;
+
+  h2 {
+    color: #323233;
+    font-size: 1.85rem;
+    margin-bottom: 2.14rem;
+    font-weight: normal;
+    padding-left: 60px;
+    padding-bottom: 20px;
+    border-bottom: 1px solid #E9E9E9;
+  }
+    
+  .panel-form-item {
+    position: relative;
+    padding-bottom: 2.14rem;
+    margin: 0;
+    padding-left: 90px;
+    padding-right: 90px;
+
+    &.remember {
+      padding: 0;
+    }
+
+    .err-info {
+      position: absolute;
+      top: 100%;
+      left: 20px;
+      font-size: 1rem;
+      line-height: 2.14rem;
+      color: #FA5555;
+    }
+    
+  }
+
+  .more {
+    text-align: center;
+
+    a {
+      color: #323233;
+      line-height: 21px;
+      font-size: 16px;
+      margin: 0 5px;
+      position: relative;
+      text-decoration: none;
+      cursor: pointer;
+    }
+  }
+}
+
+
+.panel-form-item .el-select {
+  width: 100%;
+}
+
+.panel-form-item .el-button,
+.panel-form-item .el-input__inner {
+  height: 50px;
+  font-size: 1.14rem;
+}
+
+.panel-form-item .el-button {
+  line-height: 26px;
+  font-weight: bold;
+  color: #FFFFFF;
+  font-size: 16px;
+}
+
+.panel-form-item .el-form-item__label {
+  line-height: 50px;
+}
+
+.fill.el-button {
+  background: var(--primaryColor) !important;
+  border-color: var(--primaryColor) !important;
+}

+ 218 - 0
src/view/teaching/index.vue

@@ -0,0 +1,218 @@
+<template>
+  <com-head :options="headList" showCtrl>
+    <el-form label-width="84px" inline="true">
+      <el-form-item label="项目编号:">
+        <el-input v-model="search.state.snCode" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="起火对象:">
+        <el-input v-model="search.state.searchKey" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="起火场所:">
+        <el-select v-model="search.state.snCode" placeholder="起火场所:">
+          <el-option label="区域一" value="shanghai"></el-option>
+          <el-option label="区域二" value="beijing"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="承办单位:">
+        <el-select v-model="search.state.searchKey" placeholder="承办单位:">
+          <el-option label="区域一" value="shanghai"></el-option>
+          <el-option label="区域二" value="beijing"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="事故日期:">
+        <el-date-picker type="date" v-model="search.state.snCode" style="width: 100%"></el-date-picker>
+      </el-form-item>
+      <el-form-item class="searh-btns" style="grid-area:1/4/2/5">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="search.reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+  <div class="body-layer">
+    <div class="body-head">
+      <h3>项目列表</h3>
+      <div class="table-ctrl-right">
+        <el-button
+          type="primary"
+          @click="revokeTeaching"
+          >撤销教学</el-button>
+      </div>
+    </div>
+    <el-table
+      ref="multipleTable"
+      :data="dataList.state"
+      tooltip-effect="dark"
+      style="width: 100%"
+      @row-click="dataList.selectRow"
+    >
+      <el-table-column type="selection" width="50"></el-table-column>
+      <el-table-column label="序号" width="50" v-slot:default="{ $index }">
+        <div style="text-align: center">{{ $index + 1 }}</div>
+      </el-table-column>
+      <el-table-column label="项目编号" prop="snCode"></el-table-column>
+      <el-table-column label="起火对象" prop="departmentName"></el-table-column>
+      <el-table-column label="起火地址" prop="departmentName"></el-table-column>
+      <el-table-column label="起火场所" prop="sceneCount"></el-table-column>
+      <el-table-column label="承办单位" prop="sceneCount"></el-table-column>
+      <el-table-column label="事故日期" prop="lastTime"></el-table-column>
+      <el-table-column label="火灾原因" prop="sceneCount"></el-table-column>
+      <el-table-column label="项目状态" prop="sceneCount"></el-table-column>
+      <el-table-column label="操作" v-slot:default="{ row }" width="250">
+        <span class="oper-span" @click="oper.readyUpdate(row)">详情</span>
+        <span class="oper-span" @click="archivesHandle(row)">档案</span>
+        <span class="oper-span" @click="queryScene(row)">场景</span>
+        <span class="oper-span disable" @click="leaveMsgHandle(row)">留言</span>
+        <span class="oper-span" 
+          @click="revokeTeaching(row)" 
+          style="color: var(--primaryColor)">
+         撤销
+        </span>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+  </div>
+  
+  <com-archives v-if="showArchives.show" v-model:show="showArchives.show" :data="showArchives.id" />
+  <com-leave-msg v-if="showLeaveMsg.show" v-model:show="showLeaveMsg.show" :data="showLeaveMsg.id" />
+  
+</template>
+
+<script>
+import { ref, watch } from "vue";
+import getTableState from "@/state/tableRef";
+import auth from "@/state/viewAuth";
+import comPagination from "@/components/pagination";
+import comHead from "@/components/head";
+import comArchives from "../dispatch/archives";
+import comLeaveMsg from "./leaveMsg";
+import { dateFormat } from '@/util'
+import {
+  getCameraList,
+  insertCamera,
+  updateCamera,
+  deleteCamera,
+} from '@/request/config'
+
+export default {
+  name: 'camera',
+  setup() {
+    const state = getTableState({
+      updateUrl: updateCamera,
+      insertUrl: insertCamera,
+      getUrl: getCameraList,
+      delUrl: deleteCamera,
+      searchAttr: { startTime: "", endTime: '', departmentId: '', snCode: '' },
+      operAttr: {
+        wifiName: '',
+        snCode: '',
+        childName: '',
+        departmentId: '',
+      },
+      pagAttr: {totalNum: 'total'},
+      delMsg: '解绑相机,该相机拍摄的场景也将一并解绑(场景在云端存储,不会删除)确定要解绑吗?'
+    });
+    const headList = ref([{ name: "教学平台", value: 2 }]);
+    const types = ref([{ name: "我的相机", value: 2 }])
+    const time = ref(null)
+    const showArchives = ref({show: false, data: 0})
+    const showLeaveMsg = ref({show: false, data: 0})
+    
+    const share = ref({show: false, data: 0})
+    
+
+    watch(time, () => {
+      if (time.value) {
+        state.search.value.state.startTime = dateFormat(new Date(time.value[0]), 'yyyy-MM-dd hh:mm:ss')
+        state.search.value.state.endTime = dateFormat(new Date(time.value[1]), 'yyyy-MM-dd 23:59:59')
+      } else {
+        state.search.value.state.startTime = state.search.value.state.endTime = ''
+      }
+    })
+    watch(
+      [() => state.search.value.state.startTime, () => state.search.value.state.endTime],
+      () => {
+        if (!state.search.value.state.startTime || !state.search.value.state.endTime) {
+          time.value = null
+        }
+      }
+    )
+
+    return { ...state, headList, time, auth, types, showArchives, share, showLeaveMsg };
+  },
+  methods: {
+    archivesHandle(row) {
+      this.showArchives.data = row.id
+      this.showArchives.show = true
+    },
+    leaveMsgHandle(row) {
+      this.showLeaveMsg.data = row.id
+      this.showLeaveMsg.show = true
+    },
+    async revokeTeaching() {
+      if (await this.$confirm('撤销教学,火调项目将不再显示在教学平台。(火调项目不会删除)', '撤销教学')) {
+        console.log('yes')
+      }
+    },
+    queryScene(item) {
+      if (!item.scene) {
+        return this.$message.error('当前火调项目暂未录入场景链接', '提示')
+      }
+    },
+    shareHandle(item) {
+      this.share.show = true
+      this.share.data = item
+    }
+  },
+  components: {
+    "com-leave-msg": comLeaveMsg,
+    "com-head": comHead,
+    "com-pagination": comPagination,
+    "com-archives": comArchives,
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.camera-from {
+  margin: 0 50px;
+}
+.share-from {
+  width: 500px;
+  margin: 0 auto;
+}
+.maker {
+  font-weight: 400;
+  color: #969799;
+  line-height: 20px;
+}
+
+</style>
+
+<style>
+.dispatch-file-from .el-select {
+  width: 100%;
+}
+</style>

+ 289 - 0
src/view/user/index.vue

@@ -0,0 +1,289 @@
+<template>
+  <com-head :options="headList" >
+    <el-form label-width="84px" inline="true">
+      <el-form-item label="用户名称:">
+        <el-input v-model="search.state.nickName" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="所属架构:">
+        <com-company v-model="search.state.departmentId" />
+      </el-form-item>
+      <el-form-item label="状态:">
+        <el-select v-model="search.state.status" placeholder="全部">
+          <el-option label="全部" :value="''"></el-option>
+          <el-option label="可用" :value="1"></el-option>
+          <el-option label="禁用" :value="0"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item class="searh-btns">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="search.reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer" style="padding-top: 8px">
+    <div class="body-head">
+      <!-- <h3 style="visibility: hidden;">用户列表</h3> -->
+    </div>
+
+    <el-table
+      class="user-table"
+      ref="multipleTable"
+      :data="dataList.state"
+      tooltip-effect="dark"
+      style="width: 100%"
+      @row-click="selectRow"
+    >
+      <el-table-column label="序号" width="55" v-slot:default="{ $index }">
+        <div style="text-align: center">{{ pag.state.size * (pag.state.currPage - 1) + $index + 1 }}</div>
+      </el-table-column>
+      <el-table-column label="手机号(账号)" prop="userName"></el-table-column>
+      <el-table-column label="用户名称" prop="nickName"></el-table-column>
+      <el-table-column label="所属架构" prop="departmentName"></el-table-column>
+      <el-table-column label="角色" prop="roleName"></el-table-column>
+      <el-table-column label="状态" v-slot:default="{ row }">
+        {{row.status ? '可用': '禁用'}}
+      </el-table-column>
+      <el-table-column label="操作" v-slot:default="{ row }" v-if="auth.update || auth.updatePwd || auth.delete">
+        <template v-if="row.id !== ADMIN_USER_ID && row.id !== user.info.id">
+          <span class="oper-span" @click="updateInfo(row)" v-if="auth.update">修改</span>
+          <span class="oper-span" @click="changeUserStatus(row)" v-if="auth.delete" style="color: var(--primaryColor)">
+            {{ row.status ? '禁用' : '启用' }}
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+  </div>
+  
+
+  <com-dialog
+    :title="(operRoleId ? '修改架构' : '启用')"
+    v-model:show="oper.state.show"
+    @submit="operItem"
+    width="540"
+  >
+    <el-form ref="form" :model="form" label-width="90px" class="user-from">
+      <el-form-item label="所属架构:" class="mandatory"  v-show="operRoleId">
+        <com-company v-model="oper.state.departmentId" @update:data="({level}) => (oper.state.roleId = level)" style="width: 100%" hideAll />
+      </el-form-item>
+      <el-form-item label="角色:" class="roleName">
+        <com-role v-model="oper.state.roleId" style="width: 100%" allText="请选择" hideAll :notDefault="true" :disabled="true" />
+      </el-form-item>
+      <el-form-item v-if="!operRoleId">
+        <p class="maker">注:首次启用,需设置用户角色;用户被启用后,可正常登录使用。</p>
+      </el-form-item>
+    </el-form>
+  </com-dialog>
+  
+</template>
+
+
+<script>
+import { ref } from "vue";
+import getTableState from "@/state/tableRef";
+import comDialog from "@/components/dialog";
+import comHead from "@/components/head";
+import comCompany from "@/components/company-select";
+import comPagination from "@/components/pagination";
+import roleCompany from "@/components/role-select";
+import auth from "@/state/viewAuth";
+import user from "@/state/user";
+import axios from 'axios'
+import {encryption} from '@/util'
+import {PSW, PHONE} from '@/constant/REG'
+import {ADMIN_USER_ID} from "@/constant";
+
+import {
+  getUserList,
+  updateUser,
+  changeUserStatus
+} from '@/request/config'
+import { getApp } from '../../app';
+
+
+export default {
+  name: 'user',
+  setup() {
+    const state = getTableState({
+      getUrl: getUserList,
+      updateUrl: updateUser,
+      operAttr: {
+        nickName: "",
+        userName: "",
+        departmentId: "",
+        password: '',
+        confirmPwd: '',
+        roleId: "",
+        maxlevel: 1,
+      },
+      searchAttr: { nickName: "", status: '', departmentId: '' },
+    });
+    const headList = ref([{ name: "用户管理", value: 2 }]);
+    const operRoleId = ref('')
+    const updateInfo = (row) => {
+
+      if (!row.status) {
+        return getApp().$message.error('请先启用用户', '提示')
+      }
+      operRoleId.value = row.roleId
+      state.oper.value.readyUpdate(row)
+    }
+    
+    console.log(user)
+    return { ...state, headList, updateInfo, auth, user, operRoleId, ADMIN_USER_ID };
+  },
+  methods: {
+    async operItem() {
+      const updatePhone = this.oper.state.__oldData ? this.oper.state.__oldData.userName !== this.oper.state.userName : true
+      
+      if (updatePhone && !PHONE.REG.test(this.oper.state.userName)) {
+        return this.$message.error(PHONE.tip, '提示')
+      }
+      if (this.oper.state.password !== this.oper.state.confirmPwd) {
+        return this.$message.error('两次密码不一致!', '提示')
+      }
+
+      if (!this.oper.state.roleId) {
+        return this.$message.error('请选择用户角色', '提示')
+      }
+
+      if (!this.operRoleId && !(await this.$confirm('用户被启用后,可正常登录使用。确定要启用吗?', '提示'))) {
+        return;
+      }
+
+      if (this.oper.state.id) {
+        let updateState = {...this.oper.state, password: void 0, confirmPwd: void 0, updatePwd: void 0, userId: this.oper.state.id}
+        if (!updatePhone) {
+          delete updateState.userName
+        }
+        let state = {...this.oper.state}
+        await this.oper.update(updateState)
+        await this.changeUserStatus(state, true)
+      } else if (this.oper.state.password === this.oper.state.confirmPwd) {
+        if (PSW.REG.test(this.oper.state.password)) {
+          let cryPsw = encryption(this.oper.state.password)
+          this.oper.insert({
+            ...this.oper.state,
+            password: cryPsw,  
+            confirmPwd: cryPsw,  
+            updatePwd: void 0
+          })
+        } else {
+          this.$message.error(PSW.tip, '提示')
+        }
+      }
+
+    },
+    async changeUserStatus (row, d) {
+      if (!d && (!row.roleId && !row.status)) {
+        this.operRoleId = row.roleId
+        return this.oper.readyUpdate(row)
+      }
+      let msg = row.status ? `用户被禁用后,无法登录使用。确定要禁用吗?` : `用户被启用后,可正常登录使用。确定要启用吗?`
+      try {
+        if (d || (await this.$confirm(msg, '提示'))) {
+          await axios.post(changeUserStatus, {status: Number(!row.status), userId: row.id})
+          this.dataList.refer()
+        }
+        return true
+      } catch {
+        return false
+      }
+    }
+  },
+  components: {
+    "com-dialog": comDialog,
+    "com-head": comHead,
+    "com-company": comCompany,
+    "com-role": roleCompany,
+    "com-pagination": comPagination
+  },
+};
+
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.user-from {
+  // width: 380px;
+  margin: 0 auto;
+}
+
+.tip {
+  position: fixed;
+  left: 50%;
+  top: 50%;
+  z-index: 999;
+
+  &::before {
+    content: '密码重置为Fcb20210225,可登录修改';
+    position: absolute;
+    width: 205px;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 100%;
+    border-radius: 4px;
+    padding: 10px;
+    z-index: 2000;
+    font-size: 12px;
+    line-height: 1.2;
+    min-width: 10px;
+    word-wrap: break-word;
+    background: #303133;
+    color: #fff;
+    margin-bottom: 6px;
+    
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    display: block;
+    width: 0;
+    height: 0;
+    bottom: 100%;
+    left: 50%;
+    transform: translateX(-50%);
+    border-color: transparent;
+    border-style: solid;
+    border-width: 6px;
+    border-top-color: #303133;
+    border-bottom-width: 0;
+  }
+}
+
+.maker {
+  font-weight: 400;
+  color: #969799;
+  line-height: 20px;
+}
+
+</style>
+
+<style>
+.user-table.el-table .cell {
+  overflow: inherit;
+}
+</style>

+ 82 - 0
src/view/vrmodel/async.vue

@@ -0,0 +1,82 @@
+<template>
+  <div style="width: 100%; overflow: hidden">
+    <el-table ref="multipleTable" :data="dataList.state" style="width: 100%;border: 1px solid #E8E8E8;" >
+      <el-table-column label="场景标题" prop="sceneName"></el-table-column>
+      <el-table-column label="拍摄日期" prop="createTime"></el-table-column>
+      <el-table-column label="状态"  v-slot:default="{ row }">
+        <span>{{row.hasSynchronized ? '已同步' : '未同步'}}</span>
+      </el-table-column>
+      <el-table-column label="操作" prop="auditRemark"  v-slot:default="{ row }">
+        <span class="oper-span" :class="{disable: row.hasSynchronized}" @click="row.hasSynchronized || async(row)">同步</span>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      :hide-sizes="true"
+      :csizes="[8]"
+      layout="total, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+  </div>
+  <p class="remak">注:同步场景是指将拍摄的场景手动同步到火调平台</p>
+</template>
+
+<script>
+import getTableState from '@/state/tableRef'
+import { getSceneListByCamera, asyncScene } from '@/request/config'
+import comPagination from "@/components/pagination";
+import axios from 'axios'
+
+export default {
+  props: ['show', 'data'],
+  setup(props) {
+    console.log(props.data)
+    const state = getTableState({
+      getUrl: getSceneListByCamera,
+      searchAttr: { cameraSn: props.data }
+    })
+    state.pag.value.sizeChange(8)
+    return { ...state }
+  },
+  methods: {
+    async async(data) {
+      try {
+        await axios.post(asyncScene, {sceneNums: data.num})
+
+        this.$message({
+          message: data.sceneName + '同步成功',
+          type: 'success'
+        });
+        data.hasSynchronized = true
+      } catch {
+        this.$message.error(data.sceneName + '同步失败');
+      }
+    }
+  },
+  components: {
+    "com-pagination": comPagination
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.remark {
+  max-height: 100px;
+  overflow-y: auto;
+}
+
+.async {
+  color: #26559B;
+  cursor: pointer;
+}
+
+.remak {
+  font-size: 14px;
+  color: #969799;
+  line-height: 19px;
+  margin-top: 17px;
+}
+</style>

+ 279 - 0
src/view/vrmodel/index.vue

@@ -0,0 +1,279 @@
+<template>
+  <com-head :options="headList" >
+    <el-form label-width="84px" inline="true">
+      <el-form-item label="场景标题:">
+        <el-input v-model="search.state.sceneTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="拍摄时间:">
+        <el-date-picker
+          v-model="time"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item class="searh-btns" style="grid-area: 1 / 3 / 2 / 4;">
+        <el-button type="primary" @click="search.submit">查询</el-button>
+        <el-button type="primary" plain @click="search.reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <div class="body-head">
+      <h3 style="visibility: hidden;">场景管理</h3>
+      <div class="table-ctrl-right">
+        <el-button type="primary" v-if="auth.delete" @click="cameraCompany.show = true">同步场景</el-button>
+      </div>
+    </div>
+
+    <el-table
+      ref="multipleTable"
+      :data="dataList.state"
+      tooltip-effect="dark"
+      style="width: 100%"
+      @row-click="selectRow"
+      @selection-change="dataList.changeSelectRows"
+    >
+      <!-- -1 计算失败  0 计算中 1 计算成功并可以外网访问,不能编辑 2计算成功只能内网,能编辑 -->
+      <el-table-column label="序号" width="55" v-slot:default="{ $index }">
+        <div style="text-align: center">{{ pag.state.size * (pag.state.currPage - 1) + $index + 1 }}</div>
+      </el-table-column>
+      <el-table-column label="场景标题" prop="sceneName"></el-table-column>
+      <el-table-column label="S/N码" prop="snCode"></el-table-column>
+      <el-table-column label="浏览数量" prop="viewCount"></el-table-column>
+      <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }"> {{row.createTime.substr(0, 11)}}</el-table-column>
+      <el-table-column label="操作" v-slot:default="{ row }" >
+        <span class="oper-span" @click="shareHandle(row)">查看</span>
+        <span class="oper-span" @click="auth.update && user.info.id == row.creatorId && editModel(row)" :class="{disable: !(auth.update && user.info.id == row.creatorId)}" v-if="auth.update">编辑</span>
+        <span class="oper-span" @click="auth.delete && user.info.departmentId == row.deptId && dataList.delete(row)" :class="{disable: !(auth.delete && user.info.departmentId == row.deptId)}" style="color: var(--primaryColor)">删除</span>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pag.sizeChange"
+      @current-change="pag.currentChange"
+      :current-page="pag.state.currPage"
+      :page-size="pag.state.size"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="pag.state.total"/>
+    
+  
+    <com-dialog
+      title="场景归属"
+      v-model:show="editCompany.show"
+      @submit="updateItemCompany"
+    >
+      <el-form ref="form" :model="form" label-width="84px" class="vrmodel-from">
+        <el-form-item label="所属架构:" class="mandatory">
+          <com-company v-model="editCompany.data.departmentId" hideAll />
+        </el-form-item>
+      </el-form>
+    </com-dialog>
+
+    
+    <com-dialog
+      title="选择相机"
+      :width="480"
+      v-model:show="cameraCompany.show"
+      @submit="updateCameraCompany"
+      enterText="确 定"
+    >
+      <el-form ref="form" :model="form" label-width="90px" class="vrmodel-from">
+        <el-form-item label="选择相机:" class="mandatory">
+          <el-select v-model="cameraCompany.data" placeholder="请选择">
+            <el-option
+              v-for="item in cameras"
+              :key="item.snCode"
+              :label="item.snCode"
+              :value="item.snCode">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </com-dialog>
+
+    
+    <com-dialog
+      title="同步场景"
+      v-model:show="asyncSceneCompany"
+      @submit="asyncSceneCompany = false"
+      enterText="确 定"
+    >
+      <com-async :data="cameraCompany.data" v-if="cameraCompany.data" />
+    </com-dialog>
+  </div>
+</template>
+
+<script>
+import { ref, watch } from "vue";
+import getTableState from "@/state/tableRef";
+import auth from "@/state/viewAuth";
+import user from '@/state/user'
+import comAsync from "./async";
+import comHead from "@/components/head";
+import comDialog from "@/components/dialog";
+import comPagination from "@/components/pagination";
+import comCompany from "@/components/company-select";
+import { getApp } from '@/app'
+import { dateFormat } from '@/util'
+import {
+  getSceneList,
+  deleteScene,
+  getCameraOptions,
+} from '@/request/config'
+import axios from 'axios';
+
+
+export default {
+  name: 'vrmodel',
+  setup() {
+    const state = getTableState({
+      getUrl: getSceneList,
+      delUrl: deleteScene,
+      delAttr: {'sceneNum': 'num'},
+      searchAttr: { start: "", end: '', sceneTitle: '', snCode: '' }
+    });
+    const editCompany = ref({data: {}, show: false})
+    const cameraCompany = ref({data: null, show: false})
+    const asyncSceneCompany = ref(false)
+    const cameras = ref([])
+    const headList = ref([{ name: "场景管理", value: 2 }]);
+    const currModel = ref(1)
+    const time = ref(null)
+
+    watch(time, () => {
+      if (time.value) {
+        console.log(time.value[0], time.value[1])
+        let start1 = new Date(time.value[0])
+        let start2 = new Date(time.value[1])
+        if (start2.getTime() - start1.getTime() < 0) {
+          getApp().$message.error('结束日期必须大于开始日期', '提示')
+        } else {
+          state.search.value.state.start = dateFormat(start1, 'yyyy-MM-dd')
+          state.search.value.state.end = dateFormat(start2, 'yyyy-MM-dd')
+        }
+      } else {
+        state.search.value.state.start = state.search.value.state.end = ''
+      }
+    })
+    watch(
+      [() => state.search.value.state.start, () => state.search.value.state.end],
+      () => {
+        if (!state.search.value.state.start || !state.search.value.state.end) {
+          time.value = null
+        }
+      }
+    )
+
+    watch(asyncSceneCompany, () => {
+      asyncSceneCompany.value || state.dataList.value.refer()
+    })
+
+    return { ...state, headList, currModel, time, auth, editCompany, cameras, cameraCompany, asyncSceneCompany, user };
+  },
+  methods: {
+    editModel(item) {
+      window.open(process.env.VUE_APP_DOMAIN + '/epc.html?m=' + item.num + '&token=' + user.value.token)
+    },
+    shareHandle(item) {
+      window.open(process.env.VUE_APP_DOMAIN + '/spc.html?m=' + item.num)
+    },
+    updateCameraCompany() {
+      if (!this.cameraCompany.data) {
+        return this.$message.error('请选择相机!', '提示')
+      }
+      this.cameraCompany.show = false
+      this.asyncSceneCompany = true
+    }
+  },
+  async activated() {
+    let res = await axios.get(getCameraOptions)
+    this.cameras = res.data.list
+  },
+  components: {
+    "com-head": comHead,
+    "com-dialog": comDialog,
+    "com-company": comCompany,
+    "com-async": comAsync,
+    "com-pagination": comPagination
+    
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.mode2-layer {
+  flex: 1;
+  overflow-y: auto;
+}
+.grid-data {
+  display: grid;
+  grid: auto-flow / 1fr 1fr 1fr 1fr;
+  grid-column-gap: 40px;
+  grid-row-gap: 40px;
+  padding-right: 30px;
+}
+.table-img {
+  width: 50px;
+  position: relative;
+
+  img {
+    width: 100%;
+  }
+
+  div {
+    position: absolute;
+    z-index: 1;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0,0,0,0.8);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+  }
+}
+.un-data {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  color: #909399;
+}
+
+.oper-depar {
+  cursor: pointer;
+}
+
+.vrmodel-from {
+  // width: 320px;
+  // margin: 0 auto;
+}
+</style>
+
+<style>
+.vrmodel-from .el-select {
+  width: 100%;
+}
+</style>

+ 29 - 0
vue.config.js

@@ -0,0 +1,29 @@
+
+module.exports = {
+  assetsDir: process.env.VUE_APP_STATIC_URL,
+  publicPath: process.env.NODE_ENV === 'production' ? '' : '',
+  productionSourceMap: false,
+  pluginOptions: {
+    'style-resources-loader': {
+      preProcessor: 'scss',
+      patterns: []
+    }
+  },
+  outputDir: 'dist',
+  devServer: {
+    inline: false,
+    hot: false,
+    liveReload: false,
+    // 设置代理proxy
+    proxy: {
+      '/__api': {
+        // target: 'http://192.168.0.98:8285/',
+        target: 'https://testhuodiao.4dkankan.com/',
+        changeOrigin: true,  
+        pathRewrite: {      
+          ['^/__api']: ''
+        }
+      }
+    }
+  }
+}