소스 검색

app-cdfg纳入git

任一存 1 년 전
부모
커밋
4cac1d1498
100개의 변경된 파일17433개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      packages/app-cdfg/.app
  2. 3 0
      packages/app-cdfg/.browserslistrc
  3. 15 0
      packages/app-cdfg/.env
  4. 6 0
      packages/app-cdfg/.env.development
  5. 11 0
      packages/app-cdfg/.env.hongkongdev
  6. 9 0
      packages/app-cdfg/.env.mytest
  7. 0 0
      packages/app-cdfg/.env.prod
  8. 23 0
      packages/app-cdfg/.gitignore
  9. 52 0
      packages/app-cdfg/README.md
  10. 3 0
      packages/app-cdfg/babel.config.js
  11. 1 0
      packages/app-cdfg/jsconfig.json
  12. 32 0
      packages/app-cdfg/package.json
  13. BIN
      packages/app-cdfg/public/editor/favicon.ico
  14. BIN
      packages/app-cdfg/public/editor/static/images/roam/roam_visible.png
  15. 11 0
      packages/app-cdfg/public/editor/static/lib/animate/animate.min.css
  16. 109 0
      packages/app-cdfg/public/editor/static/lib/flexible.min.js
  17. 7 0
      packages/app-cdfg/public/editor/static/lib/flv.min.js
  18. 1 0
      packages/app-cdfg/public/editor/static/lib/jweixin-1.0.0.js
  19. 21 0
      packages/app-cdfg/public/editor/static/lib/mobile-detect.js
  20. 8793 0
      packages/app-cdfg/public/editor/static/lib/quill/quill.min.js
  21. 950 0
      packages/app-cdfg/public/editor/static/lib/quill/quill.snow.css
  22. 10 0
      packages/app-cdfg/public/editor/static/lib/vconsole.js
  23. BIN
      packages/app-cdfg/public/editor/static/music/01.mp3
  24. BIN
      packages/app-cdfg/public/editor/static/music/02.mp3
  25. BIN
      packages/app-cdfg/public/editor/static/music/03.mp3
  26. BIN
      packages/app-cdfg/public/editor/static/music/04.mp3
  27. BIN
      packages/app-cdfg/public/editor/static/music/05.mp3
  28. BIN
      packages/app-cdfg/public/editor/static/music/06.mp3
  29. BIN
      packages/app-cdfg/public/editor/static/music/07.mp3
  30. BIN
      packages/app-cdfg/public/editor/static/music/08.mp3
  31. 27 0
      packages/app-cdfg/public/epg.html
  32. 22 0
      packages/app-cdfg/public/show.html
  33. 58 0
      packages/app-cdfg/scripts/create-apis.js
  34. 88 0
      packages/app-cdfg/scripts/oss-upload-aws.js
  35. 63 0
      packages/app-cdfg/scripts/update-i18n.js
  36. 9 0
      packages/app-cdfg/src/apis/index.js
  37. 103 0
      packages/app-cdfg/src/apis/scene-edit.js
  38. 33 0
      packages/app-cdfg/src/app.js
  39. BIN
      packages/app-cdfg/src/assets/images/cad/style-1.jpg
  40. BIN
      packages/app-cdfg/src/assets/images/cad/style-2.jpg
  41. BIN
      packages/app-cdfg/src/assets/images/cad/style-3.jpg
  42. BIN
      packages/app-cdfg/src/assets/images/cad/style-4.jpg
  43. BIN
      packages/app-cdfg/src/assets/images/floorlogo/0.png
  44. BIN
      packages/app-cdfg/src/assets/images/floorlogo/1.png
  45. BIN
      packages/app-cdfg/src/assets/images/floorlogo/2.png
  46. BIN
      packages/app-cdfg/src/assets/images/floorlogo/en/0.png
  47. BIN
      packages/app-cdfg/src/assets/images/floorlogo/en/1.png
  48. BIN
      packages/app-cdfg/src/assets/images/floorlogo/en/2.png
  49. BIN
      packages/app-cdfg/src/assets/images/floorlogo/enter-style-default.png
  50. BIN
      packages/app-cdfg/src/assets/images/floorlogo/enter-style-down.png
  51. BIN
      packages/app-cdfg/src/assets/images/floorlogo/enter-style-up.png
  52. BIN
      packages/app-cdfg/src/assets/images/floorlogo/icon-corner-24.png
  53. BIN
      packages/app-cdfg/src/assets/images/floorlogo/icon-corner.png
  54. BIN
      packages/app-cdfg/src/assets/images/loading.jpg
  55. BIN
      packages/app-cdfg/src/assets/images/roam/roam_checked.png
  56. BIN
      packages/app-cdfg/src/assets/images/roam/roam_checked_256.png
  57. BIN
      packages/app-cdfg/src/assets/images/roam/roam_invisible.png
  58. BIN
      packages/app-cdfg/src/assets/images/roam/roam_invisible_256.png
  59. BIN
      packages/app-cdfg/src/assets/images/roam/roam_uncheck.png
  60. BIN
      packages/app-cdfg/src/assets/images/roam/roam_uncheck_256.png
  61. BIN
      packages/app-cdfg/src/assets/images/roam/roam_visible.png
  62. BIN
      packages/app-cdfg/src/assets/images/roam/roam_visible_256.png
  63. BIN
      packages/app-cdfg/src/assets/images/tag/style-tag.png
  64. 373 0
      packages/app-cdfg/src/assets/theme.editor.scss
  65. 108 0
      packages/app-cdfg/src/components/Controls/BottomControl.vue
  66. 283 0
      packages/app-cdfg/src/components/Controls/FloorSwitch.vue
  67. 555 0
      packages/app-cdfg/src/components/Controls/LeftButtons.vue
  68. 202 0
      packages/app-cdfg/src/components/Controls/RightButtons.vue
  69. 126 0
      packages/app-cdfg/src/components/Header/Edit.vue
  70. 97 0
      packages/app-cdfg/src/components/Header/Publish.vue
  71. 14 0
      packages/app-cdfg/src/components/Header/index.vue
  72. 368 0
      packages/app-cdfg/src/components/Information/Edit.vue
  73. 187 0
      packages/app-cdfg/src/components/Information/View.vue
  74. 124 0
      packages/app-cdfg/src/components/Information/editView.vue
  75. 81 0
      packages/app-cdfg/src/components/Information/index.vue
  76. 34 0
      packages/app-cdfg/src/components/Menuer/index.vue
  77. 24 0
      packages/app-cdfg/src/components/StorageToast/index.js
  78. 97 0
      packages/app-cdfg/src/components/Tags/constant.js
  79. 400 0
      packages/app-cdfg/src/components/Tags/goods-list.vue
  80. 229 0
      packages/app-cdfg/src/components/Tags/index.vue
  81. 132 0
      packages/app-cdfg/src/components/Tags/link-manage.vue
  82. 100 0
      packages/app-cdfg/src/components/Tags/metas-upload-customized.vue
  83. 106 0
      packages/app-cdfg/src/components/Tags/metas-upload.vue
  84. 232 0
      packages/app-cdfg/src/components/Tags/metas/metas-appletLink.vue
  85. 67 0
      packages/app-cdfg/src/components/Tags/metas/metas-audio.vue
  86. 172 0
      packages/app-cdfg/src/components/Tags/metas/metas-commodity.vue
  87. 49 0
      packages/app-cdfg/src/components/Tags/metas/metas-coupon.vue
  88. 174 0
      packages/app-cdfg/src/components/Tags/metas/metas-exhibits.vue
  89. 375 0
      packages/app-cdfg/src/components/Tags/metas/metas-image.vue
  90. 225 0
      packages/app-cdfg/src/components/Tags/metas/metas-lineScene.vue
  91. 161 0
      packages/app-cdfg/src/components/Tags/metas/metas-linkScene.vue
  92. 182 0
      packages/app-cdfg/src/components/Tags/metas/metas-pointJump.vue
  93. 127 0
      packages/app-cdfg/src/components/Tags/metas/metas-video.vue
  94. 174 0
      packages/app-cdfg/src/components/Tags/metas/metas-waterfall.vue
  95. 161 0
      packages/app-cdfg/src/components/Tags/metas/metas-web.vue
  96. 391 0
      packages/app-cdfg/src/components/Tags/scene-list.vue
  97. 239 0
      packages/app-cdfg/src/components/Tags/show-tag.vue
  98. 344 0
      packages/app-cdfg/src/components/Tags/style-icon.vue
  99. 259 0
      packages/app-cdfg/src/components/Tags/tag-info.vue
  100. 0 0
      packages/app-cdfg/src/components/Tags/tag-view.vue

+ 1 - 0
packages/app-cdfg/.app

@@ -0,0 +1 @@
+中免集团定制项目

+ 3 - 0
packages/app-cdfg/.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 15 - 0
packages/app-cdfg/.env

@@ -0,0 +1,15 @@
+# 场景资源地址
+VUE_APP_RESOURCE_URL=https://eurs3.4dkankan.com/
+# 静态资源地址
+VUE_APP_CDN_URL=https://eurs3.4dkankan.com/v4/cdfg/
+# sdk文件地址
+VUE_APP_SDK_DIR=sdk
+# 静态资源目录
+VUE_APP_STATIC_DIR=editor
+# 登录地址
+VUE_APP_LOGIN_URL=/cdf/backstage/index.html
+# 展示端地址
+VUE_APP_SHOW_URL=https://vr.cdfmembers.com/
+VUE_APP_SHOW_HK_URL=https://vr.dutyzero.com.hk/
+# 云存储环境
+VUE_APP_REGION_URL=aws

+ 6 - 0
packages/app-cdfg/.env.development

@@ -0,0 +1,6 @@
+# 场景资源地址
+VUE_APP_RESOURCE_URL=https://testeurs3.4dkankan.com/
+# 静态资源地址
+VUE_APP_CDN_URL=
+# region
+VUE_APP_REGION_URL=

+ 11 - 0
packages/app-cdfg/.env.hongkongdev

@@ -0,0 +1,11 @@
+# 场景资源地址
+VUE_APP_RESOURCE_URL=https://testeurs3.4dkankan.com/
+# 静态资源地址
+VUE_APP_CDN_URL=
+# sdk文件地址
+VUE_APP_SDK_DIR=https://eurs3.4dkankan.com/v4/cdfg/sdk
+# 展示端地址
+VUE_APP_SHOW_URL=https://zhongmian.4dage.com/
+VUE_APP_SHOW_HK_URL=https://zhongmian.4dage.com/
+# region
+VUE_APP_REGION_URL=

+ 9 - 0
packages/app-cdfg/.env.mytest

@@ -0,0 +1,9 @@
+# 场景资源地址
+VUE_APP_RESOURCE_URL=https://testeurs3.4dkankan.com/
+# 静态资源地址
+VUE_APP_CDN_URL=
+# sdk文件地址
+VUE_APP_SDK_DIR=https://eurs3.4dkankan.com/v4/cdfg/sdk
+# 展示端地址
+VUE_APP_SHOW_URL=https://zhongmian.4dage.com/
+VUE_APP_SHOW_HK_URL=https://zhongmian.4dage.com/

+ 0 - 0
packages/app-cdfg/.env.prod


+ 23 - 0
packages/app-cdfg/.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?

+ 52 - 0
packages/app-cdfg/README.md

@@ -0,0 +1,52 @@
+cdfg就是中免集团的意思。
+
+此代码对应的是大场景编辑器。
+
+由登录后台时使用的账户判断代码运行环境是香港还是澳门。
+
+# 澳门后台帐号
+* 13888888888
+* Aa123456
+
+# 香港后台帐号:
+* 13888888886
+* Aa123456
+
+# 开发环境
+命令:yarn serve
+
+拿的是测试环境的数据
+
+# 测试环境
+
+## 打包
+测试环境对应的环境变量配置文件:.env.mytest
+
+打包命令:yarn build-test
+
+打包生成的包位于顶层。
+
+## url
+https://zhongmian.4dage.com/cdf/backstage/index.html
+
+## 部署位置
+WinSCP连接221.4.210.172,/Default/aws/中免/aws-中免-18.156.200.112/var/www/html/zhongmian-test/dist/cdf/
+
+# 正式环境
+
+## 打包
+正式环境对应的环境变量配置文件:.env.prod
+
+打包命令:yarn build
+
+打包生成的包位于顶层。
+
+## url
+通过管理后台进入
+
+http://vr-admin.cdfmembers.com/cdf/backstage/index.html#/scene
+
+## 部署位置
+html文件放到:WinSCP连接221.4.210.172,/Default/aws/中免/aws-中免-18.156.200.112/home/html/cdf/
+
+editor文件夹交给马瑞,让他去更新`https://eurs3.4dkankan.com/v4/cdfg/editor/`

+ 3 - 0
packages/app-cdfg/babel.config.js

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

+ 1 - 0
packages/app-cdfg/jsconfig.json

@@ -0,0 +1 @@
+{}

+ 32 - 0
packages/app-cdfg/package.json

@@ -0,0 +1,32 @@
+{
+  "name": "@app/cdfg",
+  "private": true,
+  "version": "4.7.0-alpha.21",
+  "scripts": {
+    "oss:upload-aws": "node scripts/oss-upload-aws.js",
+    "serve": "vue-cli-service serve",
+    "serve-hk": "vue-cli-service serve --mode hongkongdev",
+    "serve-prod": "vue-cli-service serve --mode prod",
+    "build": "vue-cli-service build",
+    "build-test": "vue-cli-service build --mode mytest",
+    "deploy": "npm-run-all -s build oss:upload-aws"
+  },
+  "dependencies": {
+    "axios": "^0.21.1",
+    "clipboard": "^2.0.8",
+    "core-js": "^3.6.5",
+    "quill": "^1.3.7",
+    "vue": "^3.2.26",
+    "vue-i18n": "9",
+    "vue-router": "4.0.12",
+    "vue3-infinite-scroll-good": "^1.0.2",
+    "vuex": "^4.0.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "^3.0.0",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2"
+  }
+}

BIN
packages/app-cdfg/public/editor/favicon.ico


BIN
packages/app-cdfg/public/editor/static/images/roam/roam_visible.png


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 11 - 0
packages/app-cdfg/public/editor/static/lib/animate/animate.min.css


+ 109 - 0
packages/app-cdfg/public/editor/static/lib/flexible.min.js

@@ -0,0 +1,109 @@
+(function(win, lib) {
+    var doc = win.document;
+    var docEl = doc.documentElement;
+    var metaEl = doc.querySelector('meta[name="viewport"]');
+    var flexibleEl = doc.querySelector('meta[name="flexible"]');
+    var dpr = 0;
+    var scale = 0;
+    var tid;
+    var flexible = lib.flexible || (lib.flexible = {});
+     
+    if (metaEl) {
+        console.warn('将根据已有的meta标签来设置缩放比例');
+        var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
+        if (match) {
+            scale = parseFloat(match[1]);
+            dpr = parseInt(1 / scale);
+        }
+    } else if (flexibleEl) {
+        var content = flexibleEl.getAttribute('content');
+        if (content) {
+            var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
+            var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
+            if (initialDpr) {
+                dpr = parseFloat(initialDpr[1]);
+                scale = parseFloat((1 / dpr).toFixed(2));   
+            }
+            if (maximumDpr) {
+                dpr = parseFloat(maximumDpr[1]);
+                scale = parseFloat((1 / dpr).toFixed(2));   
+            }
+        }
+    }
+    if (!dpr && !scale) {
+        var isAndroid = win.navigator.appVersion.match(/android/gi);
+        var isIPhone = win.navigator.appVersion.match(/iphone/gi);
+        var devicePixelRatio = win.devicePixelRatio;
+        if (isIPhone) {
+            // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
+            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {               
+                dpr = 3;
+            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
+                dpr = 2;
+            } else {
+                dpr = 1;
+            }
+        } else {
+            // 其他设备下,仍旧使用1倍的方案
+            dpr = 1;
+        }
+        scale = 1 / dpr;
+    }
+    docEl.setAttribute('data-dpr', dpr);
+    if (!metaEl) {
+        metaEl = doc.createElement('meta');
+        metaEl.setAttribute('name', 'viewport');
+        metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
+        if (docEl.firstElementChild) {
+            docEl.firstElementChild.appendChild(metaEl);
+        } else {
+            var wrap = doc.createElement('div');
+            wrap.appendChild(metaEl);
+            doc.write(wrap.innerHTML);
+        }
+    }
+    function refreshRem(){
+        var width = docEl.getBoundingClientRect().width;
+        if (width / dpr > 540) {
+            width = 540 * dpr;
+        }
+        var rem = width / 10;
+        docEl.style.fontSize = rem + 'px';
+        flexible.rem = win.rem = rem;
+    }
+    win.addEventListener('resize', function() {
+        clearTimeout(tid);
+        tid = setTimeout(refreshRem, 300);
+    }, false);
+    win.addEventListener('pageshow', function(e) {
+        if (e.persisted) {
+            clearTimeout(tid);
+            tid = setTimeout(refreshRem, 300);
+        }
+    }, false);
+    if (doc.readyState === 'complete') {
+        doc.body.style.fontSize = 12 * dpr + 'px';
+    } else {
+        doc.addEventListener('DOMContentLoaded', function(e) {
+            doc.body.style.fontSize = 12 * dpr + 'px';
+        }, false);
+    }
+     
+    refreshRem();
+    flexible.dpr = win.dpr = dpr;
+    flexible.refreshRem = refreshRem;
+    flexible.rem2px = function(d) {
+        var val = parseFloat(d) * this.rem;
+        if (typeof d === 'string' && d.match(/rem$/)) {
+            val += 'px';
+        }
+        return val;
+    }
+    flexible.px2rem = function(d) {
+        var val = parseFloat(d) / this.rem;
+        if (typeof d === 'string' && d.match(/px$/)) {
+            val += 'rem';
+        }
+        return val;
+    }
+})(window, window['lib'] || (window['lib'] = {}));

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
packages/app-cdfg/public/editor/static/lib/flv.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
packages/app-cdfg/public/editor/static/lib/jweixin-1.0.0.js


+ 21 - 0
packages/app-cdfg/public/editor/static/lib/mobile-detect.js

@@ -0,0 +1,21 @@
+;(function (win) {
+    var orgLink = win.location.href
+    var newLink = ''
+    if (orgLink.indexOf('&mobile=true') != -1) {
+        Object.defineProperty(navigator, 'userAgent', {
+            value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
+            writable: false,
+        })
+    } else if (/iPhone|iPad|Android/i.test(win.navigator.userAgent)) {
+        if (orgLink.indexOf('pg.html') !== -1) {
+            newLink = orgLink.replace('pg.html', 'mg.html')
+        }
+    } else {
+        if (orgLink.indexOf('mg.html') !== -1) {
+            newLink = orgLink.replace('mg.html', 'pg.html')
+        }
+    }
+    if (newLink) {
+        win.location.href = newLink
+    }
+})(window)

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 8793 - 0
packages/app-cdfg/public/editor/static/lib/quill/quill.min.js


+ 950 - 0
packages/app-cdfg/public/editor/static/lib/quill/quill.snow.css

@@ -0,0 +1,950 @@
+/*!
+ * Quill Editor v1.3.6
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+ .ql-container {
+    box-sizing: border-box;
+    font-family: Helvetica, Arial, sans-serif;
+    font-size: 13px;
+    height: 100%;
+    margin: 0px;
+    position: relative;
+  }
+  .ql-container.ql-disabled .ql-tooltip {
+    visibility: hidden;
+  }
+  .ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+    pointer-events: none;
+  }
+  .ql-clipboard {
+    left: -100000px;
+    height: 1px;
+    overflow-y: hidden;
+    position: absolute;
+    top: 50%;
+  }
+  .ql-clipboard p {
+    margin: 0;
+    padding: 0;
+  }
+  .ql-editor {
+    box-sizing: border-box;
+    line-height: 1.42;
+    height: 100%;
+    outline: none;
+    overflow-y: auto;
+    padding: 12px 15px;
+    tab-size: 4;
+    -moz-tab-size: 4;
+    text-align: left;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+  }
+  .ql-editor > * {
+    cursor: text;
+  }
+  .ql-editor p,
+  .ql-editor ol,
+  .ql-editor ul,
+  .ql-editor pre,
+  .ql-editor blockquote,
+  .ql-editor h1,
+  .ql-editor h2,
+  .ql-editor h3,
+  .ql-editor h4,
+  .ql-editor h5,
+  .ql-editor h6 {
+    margin: 0;
+    padding: 0;
+    counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol,
+  .ql-editor ul {
+    padding-left: 1.5em;
+  }
+  .ql-editor ol > li,
+  .ql-editor ul > li {
+    list-style-type: none;
+  }
+  .ql-editor ul > li::before {
+    content: '\2022';
+  }
+  .ql-editor ul[data-checked=true],
+  .ql-editor ul[data-checked=false] {
+    pointer-events: none;
+  }
+  .ql-editor ul[data-checked=true] > li *,
+  .ql-editor ul[data-checked=false] > li * {
+    pointer-events: all;
+  }
+  .ql-editor ul[data-checked=true] > li::before,
+  .ql-editor ul[data-checked=false] > li::before {
+    color: #777;
+    cursor: pointer;
+    pointer-events: all;
+  }
+  .ql-editor ul[data-checked=true] > li::before {
+    content: '\2611';
+  }
+  .ql-editor ul[data-checked=false] > li::before {
+    content: '\2610';
+  }
+  .ql-editor li::before {
+    display: inline-block;
+    white-space: nowrap;
+    width: 1.2em;
+  }
+  .ql-editor li:not(.ql-direction-rtl)::before {
+    margin-left: -1.5em;
+    margin-right: 0.3em;
+    text-align: right;
+  }
+  .ql-editor li.ql-direction-rtl::before {
+    margin-left: 0.3em;
+    margin-right: -1.5em;
+  }
+  .ql-editor ol li:not(.ql-direction-rtl),
+  .ql-editor ul li:not(.ql-direction-rtl) {
+    padding-left: 1.5em;
+  }
+  .ql-editor ol li.ql-direction-rtl,
+  .ql-editor ul li.ql-direction-rtl {
+    padding-right: 1.5em;
+  }
+  .ql-editor ol li {
+    counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+    counter-increment: list-0;
+  }
+  .ql-editor ol li:before {
+    content: counter(list-0, decimal) '. ';
+  }
+  .ql-editor ol li.ql-indent-1 {
+    counter-increment: list-1;
+  }
+  .ql-editor ol li.ql-indent-1:before {
+    content: counter(list-1, lower-alpha) '. ';
+  }
+  .ql-editor ol li.ql-indent-1 {
+    counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-2 {
+    counter-increment: list-2;
+  }
+  .ql-editor ol li.ql-indent-2:before {
+    content: counter(list-2, lower-roman) '. ';
+  }
+  .ql-editor ol li.ql-indent-2 {
+    counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-3 {
+    counter-increment: list-3;
+  }
+  .ql-editor ol li.ql-indent-3:before {
+    content: counter(list-3, decimal) '. ';
+  }
+  .ql-editor ol li.ql-indent-3 {
+    counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-4 {
+    counter-increment: list-4;
+  }
+  .ql-editor ol li.ql-indent-4:before {
+    content: counter(list-4, lower-alpha) '. ';
+  }
+  .ql-editor ol li.ql-indent-4 {
+    counter-reset: list-5 list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-5 {
+    counter-increment: list-5;
+  }
+  .ql-editor ol li.ql-indent-5:before {
+    content: counter(list-5, lower-roman) '. ';
+  }
+  .ql-editor ol li.ql-indent-5 {
+    counter-reset: list-6 list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-6 {
+    counter-increment: list-6;
+  }
+  .ql-editor ol li.ql-indent-6:before {
+    content: counter(list-6, decimal) '. ';
+  }
+  .ql-editor ol li.ql-indent-6 {
+    counter-reset: list-7 list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-7 {
+    counter-increment: list-7;
+  }
+  .ql-editor ol li.ql-indent-7:before {
+    content: counter(list-7, lower-alpha) '. ';
+  }
+  .ql-editor ol li.ql-indent-7 {
+    counter-reset: list-8 list-9;
+  }
+  .ql-editor ol li.ql-indent-8 {
+    counter-increment: list-8;
+  }
+  .ql-editor ol li.ql-indent-8:before {
+    content: counter(list-8, lower-roman) '. ';
+  }
+  .ql-editor ol li.ql-indent-8 {
+    counter-reset: list-9;
+  }
+  .ql-editor ol li.ql-indent-9 {
+    counter-increment: list-9;
+  }
+  .ql-editor ol li.ql-indent-9:before {
+    content: counter(list-9, decimal) '. ';
+  }
+  .ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+    padding-left: 3em;
+  }
+  .ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+    padding-left: 4.5em;
+  }
+  .ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+    padding-right: 3em;
+  }
+  .ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+    padding-right: 4.5em;
+  }
+  .ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+    padding-left: 6em;
+  }
+  .ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+    padding-left: 7.5em;
+  }
+  .ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+    padding-right: 6em;
+  }
+  .ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+    padding-right: 7.5em;
+  }
+  .ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+    padding-left: 9em;
+  }
+  .ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+    padding-left: 10.5em;
+  }
+  .ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+    padding-right: 9em;
+  }
+  .ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+    padding-right: 10.5em;
+  }
+  .ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+    padding-left: 12em;
+  }
+  .ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+    padding-left: 13.5em;
+  }
+  .ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+    padding-right: 12em;
+  }
+  .ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+    padding-right: 13.5em;
+  }
+  .ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+    padding-left: 15em;
+  }
+  .ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+    padding-left: 16.5em;
+  }
+  .ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+    padding-right: 15em;
+  }
+  .ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+    padding-right: 16.5em;
+  }
+  .ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+    padding-left: 18em;
+  }
+  .ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+    padding-left: 19.5em;
+  }
+  .ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+    padding-right: 18em;
+  }
+  .ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+    padding-right: 19.5em;
+  }
+  .ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+    padding-left: 21em;
+  }
+  .ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+    padding-left: 22.5em;
+  }
+  .ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+    padding-right: 21em;
+  }
+  .ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+    padding-right: 22.5em;
+  }
+  .ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+    padding-left: 24em;
+  }
+  .ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+    padding-left: 25.5em;
+  }
+  .ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+    padding-right: 24em;
+  }
+  .ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+    padding-right: 25.5em;
+  }
+  .ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+    padding-left: 27em;
+  }
+  .ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+    padding-left: 28.5em;
+  }
+  .ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+    padding-right: 27em;
+  }
+  .ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+    padding-right: 28.5em;
+  }
+  .ql-editor .ql-video {
+    display: block;
+    max-width: 100%;
+  }
+  .ql-editor .ql-video.ql-align-center {
+    margin: 0 auto;
+  }
+  .ql-editor .ql-video.ql-align-right {
+    margin: 0 0 0 auto;
+  }
+  .ql-editor .ql-bg-black {
+    background-color: #000;
+  }
+  .ql-editor .ql-bg-red {
+    background-color: #e60000;
+  }
+  .ql-editor .ql-bg-orange {
+    background-color: #f90;
+  }
+  .ql-editor .ql-bg-yellow {
+    background-color: #ff0;
+  }
+  .ql-editor .ql-bg-green {
+    background-color: #008a00;
+  }
+  .ql-editor .ql-bg-blue {
+    background-color: #06c;
+  }
+  .ql-editor .ql-bg-purple {
+    background-color: #93f;
+  }
+  .ql-editor .ql-color-white {
+    color: #fff;
+  }
+  .ql-editor .ql-color-red {
+    color: #e60000;
+  }
+  .ql-editor .ql-color-orange {
+    color: #f90;
+  }
+  .ql-editor .ql-color-yellow {
+    color: #ff0;
+  }
+  .ql-editor .ql-color-green {
+    color: #008a00;
+  }
+  .ql-editor .ql-color-blue {
+    color: #06c;
+  }
+  .ql-editor .ql-color-purple {
+    color: #93f;
+  }
+  .ql-editor .ql-font-serif {
+    font-family: Georgia, Times New Roman, serif;
+  }
+  .ql-editor .ql-font-monospace {
+    font-family: Monaco, Courier New, monospace;
+  }
+  .ql-editor .ql-size-small {
+    font-size: 0.75em;
+  }
+  .ql-editor .ql-size-large {
+    font-size: 1.5em;
+  }
+  .ql-editor .ql-size-huge {
+    font-size: 2.5em;
+  }
+  .ql-editor .ql-direction-rtl {
+    direction: rtl;
+    text-align: inherit;
+  }
+  .ql-editor .ql-align-center {
+    text-align: center;
+  }
+  .ql-editor .ql-align-justify {
+    text-align: justify;
+  }
+  .ql-editor .ql-align-right {
+    text-align: right;
+  }
+  .ql-editor.ql-blank::before {
+    color: rgba(0,0,0,0.6);
+    content: attr(data-placeholder);
+    font-style: normal;
+    left: 15px;
+    pointer-events: none;
+    position: absolute;
+    right: 15px;
+    color: #c7c7c7;
+  }
+  .ql-snow.ql-toolbar:after,
+  .ql-snow .ql-toolbar:after {
+    clear: both;
+    content: '';
+    display: table;
+  }
+  .ql-snow.ql-toolbar button,
+  .ql-snow .ql-toolbar button {
+    background: none;
+    border: none;
+    cursor: pointer;
+    display: inline-block;
+    float: left;
+    height: 24px;
+    padding: 3px 5px;
+    width: 28px;
+  }
+  .ql-snow.ql-toolbar button svg,
+  .ql-snow .ql-toolbar button svg {
+    float: left;
+    height: 100%;
+  }
+  .ql-snow.ql-toolbar button:active:hover,
+  .ql-snow .ql-toolbar button:active:hover {
+    outline: none;
+  }
+  .ql-snow.ql-toolbar input.ql-image[type=file],
+  .ql-snow .ql-toolbar input.ql-image[type=file] {
+    display: none;
+  }
+  .ql-snow.ql-toolbar button:hover,
+  .ql-snow .ql-toolbar button:hover,
+  .ql-snow.ql-toolbar button:focus,
+  .ql-snow .ql-toolbar button:focus,
+  .ql-snow.ql-toolbar button.ql-active,
+  .ql-snow .ql-toolbar button.ql-active,
+  .ql-snow.ql-toolbar .ql-picker-label:hover,
+  .ql-snow .ql-toolbar .ql-picker-label:hover,
+  .ql-snow.ql-toolbar .ql-picker-label.ql-active,
+  .ql-snow .ql-toolbar .ql-picker-label.ql-active,
+  .ql-snow.ql-toolbar .ql-picker-item:hover,
+  .ql-snow .ql-toolbar .ql-picker-item:hover,
+  .ql-snow.ql-toolbar .ql-picker-item.ql-selected,
+  .ql-snow .ql-toolbar .ql-picker-item.ql-selected {
+    color: #06c;
+  }
+  .ql-snow.ql-toolbar button:hover .ql-fill,
+  .ql-snow .ql-toolbar button:hover .ql-fill,
+  .ql-snow.ql-toolbar button:focus .ql-fill,
+  .ql-snow .ql-toolbar button:focus .ql-fill,
+  .ql-snow.ql-toolbar button.ql-active .ql-fill,
+  .ql-snow .ql-toolbar button.ql-active .ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+  .ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+  .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
+    fill: #06c;
+  }
+  .ql-snow.ql-toolbar button:hover .ql-stroke,
+  .ql-snow .ql-toolbar button:hover .ql-stroke,
+  .ql-snow.ql-toolbar button:focus .ql-stroke,
+  .ql-snow .ql-toolbar button:focus .ql-stroke,
+  .ql-snow.ql-toolbar button.ql-active .ql-stroke,
+  .ql-snow .ql-toolbar button.ql-active .ql-stroke,
+  .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
+  .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,
+  .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+  .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+  .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
+  .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,
+  .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+  .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+  .ql-snow.ql-toolbar button:hover .ql-stroke-miter,
+  .ql-snow .ql-toolbar button:hover .ql-stroke-miter,
+  .ql-snow.ql-toolbar button:focus .ql-stroke-miter,
+  .ql-snow .ql-toolbar button:focus .ql-stroke-miter,
+  .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
+  .ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,
+  .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+  .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+  .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+  .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+  .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+  .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+  .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
+  .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
+    stroke: #06c;
+  }
+  @media (pointer: coarse) {
+    .ql-snow.ql-toolbar button:hover:not(.ql-active),
+    .ql-snow .ql-toolbar button:hover:not(.ql-active) {
+      color: #444;
+    }
+    .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,
+    .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,
+    .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
+    .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
+      fill: #444;
+    }
+    .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+    .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+    .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
+    .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
+      stroke: #444;
+    }
+  }
+  .ql-snow {
+    box-sizing: border-box;
+  }
+  .ql-snow * {
+    box-sizing: border-box;
+  }
+  .ql-snow .ql-hidden {
+    display: none;
+  }
+  .ql-snow .ql-out-bottom,
+  .ql-snow .ql-out-top {
+    visibility: hidden;
+  }
+  .ql-snow .ql-tooltip {
+    position: absolute;
+    transform: translateY(10px);
+  }
+  .ql-snow .ql-tooltip a {
+    cursor: pointer;
+    text-decoration: none;
+  }
+  .ql-snow .ql-tooltip.ql-flip {
+    transform: translateY(-10px);
+  }
+  .ql-snow .ql-formats {
+    display: inline-block;
+    vertical-align: middle;
+  }
+  .ql-snow .ql-formats:after {
+    clear: both;
+    content: '';
+    display: table;
+  }
+  .ql-snow .ql-stroke {
+    fill: none;
+    stroke: #444;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+    stroke-width: 2;
+  }
+  .ql-snow .ql-stroke-miter {
+    fill: none;
+    stroke: #444;
+    stroke-miterlimit: 10;
+    stroke-width: 2;
+  }
+  .ql-snow .ql-fill,
+  .ql-snow .ql-stroke.ql-fill {
+    fill: #444;
+  }
+  .ql-snow .ql-empty {
+    fill: none;
+  }
+  .ql-snow .ql-even {
+    fill-rule: evenodd;
+  }
+  .ql-snow .ql-thin,
+  .ql-snow .ql-stroke.ql-thin {
+    stroke-width: 1;
+  }
+  .ql-snow .ql-transparent {
+    opacity: 0.4;
+  }
+  .ql-snow .ql-direction svg:last-child {
+    display: none;
+  }
+  .ql-snow .ql-direction.ql-active svg:last-child {
+    display: inline;
+  }
+  .ql-snow .ql-direction.ql-active svg:first-child {
+    display: none;
+  }
+  .ql-snow .ql-editor h1 {
+    font-size: 2em;
+  }
+  .ql-snow .ql-editor h2 {
+    font-size: 1.5em;
+  }
+  .ql-snow .ql-editor h3 {
+    font-size: 1.17em;
+  }
+  .ql-snow .ql-editor h4 {
+    font-size: 1em;
+  }
+  .ql-snow .ql-editor h5 {
+    font-size: 0.83em;
+  }
+  .ql-snow .ql-editor h6 {
+    font-size: 0.67em;
+  }
+  .ql-snow .ql-editor a {
+    text-decoration: underline;
+  }
+  .ql-snow .ql-editor blockquote {
+    border-left: 4px solid #ccc;
+    margin-bottom: 5px;
+    margin-top: 5px;
+    padding-left: 16px;
+  }
+  .ql-snow .ql-editor code,
+  .ql-snow .ql-editor pre {
+    background-color: #f0f0f0;
+    border-radius: 3px;
+  }
+  .ql-snow .ql-editor pre {
+    white-space: pre-wrap;
+    margin-bottom: 5px;
+    margin-top: 5px;
+    padding: 5px 10px;
+  }
+  .ql-snow .ql-editor code {
+    font-size: 85%;
+    padding: 2px 4px;
+  }
+  .ql-snow .ql-editor pre.ql-syntax {
+    background-color: #23241f;
+    color: #f8f8f2;
+    overflow: visible;
+  }
+  .ql-snow .ql-editor img {
+    max-width: 100%;
+  }
+  .ql-snow .ql-picker {
+    color: #444;
+    display: inline-block;
+    float: left;
+    font-size: 14px;
+    font-weight: 500;
+    height: 24px;
+    position: relative;
+    vertical-align: middle;
+  }
+  .ql-snow .ql-picker-label {
+    cursor: pointer;
+    display: inline-block;
+    height: 100%;
+    padding-left: 8px;
+    padding-right: 2px;
+    position: relative;
+    width: 100%;
+  }
+  .ql-snow .ql-picker-label::before {
+    display: inline-block;
+    line-height: 22px;
+  }
+  .ql-snow .ql-picker-options {
+    background-color: #fff;
+    display: none;
+    min-width: 100%;
+    padding: 4px 8px;
+    position: absolute;
+    white-space: nowrap;
+  }
+  .ql-snow .ql-picker-options .ql-picker-item {
+    cursor: pointer;
+    display: block;
+    padding-bottom: 5px;
+    padding-top: 5px;
+  }
+  .ql-snow .ql-picker.ql-expanded .ql-picker-label {
+    color: #ccc;
+    z-index: 2;
+  }
+  .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
+    fill: #ccc;
+  }
+  .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
+    stroke: #ccc;
+  }
+  .ql-snow .ql-picker.ql-expanded .ql-picker-options {
+    display: block;
+    margin-top: -1px;
+    top: 100%;
+    z-index: 1;
+  }
+  .ql-snow .ql-color-picker,
+  .ql-snow .ql-icon-picker {
+    width: 28px;
+  }
+  .ql-snow .ql-color-picker .ql-picker-label,
+  .ql-snow .ql-icon-picker .ql-picker-label {
+    padding: 2px 4px;
+  }
+  .ql-snow .ql-color-picker .ql-picker-label svg,
+  .ql-snow .ql-icon-picker .ql-picker-label svg {
+    right: 4px;
+  }
+  .ql-snow .ql-icon-picker .ql-picker-options {
+    padding: 4px 0px;
+  }
+  .ql-snow .ql-icon-picker .ql-picker-item {
+    height: 24px;
+    width: 24px;
+    padding: 2px 4px;
+  }
+  .ql-snow .ql-color-picker .ql-picker-options {
+    padding: 3px 5px;
+    width: 152px;
+  }
+  .ql-snow .ql-color-picker .ql-picker-item {
+    border: 1px solid transparent;
+    float: left;
+    height: 16px;
+    margin: 2px;
+    padding: 0px;
+    width: 16px;
+  }
+  .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
+    position: absolute;
+    margin-top: -9px;
+    right: 0;
+    top: 50%;
+    width: 18px;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
+  .ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
+  .ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
+    content: attr(data-label);
+  }
+  .ql-snow .ql-picker.ql-header {
+    width: 98px;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item::before {
+    content: 'Normal';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+    content: 'Heading 1';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+    content: 'Heading 2';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+    content: 'Heading 3';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+    content: 'Heading 4';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+    content: 'Heading 5';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+    content: 'Heading 6';
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+    font-size: 2em;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+    font-size: 1.5em;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+    font-size: 1.17em;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+    font-size: 1em;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+    font-size: 0.83em;
+  }
+  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+    font-size: 0.67em;
+  }
+  .ql-snow .ql-picker.ql-font {
+    width: 108px;
+  }
+  .ql-snow .ql-picker.ql-font .ql-picker-label::before,
+  .ql-snow .ql-picker.ql-font .ql-picker-item::before {
+    content: 'Sans Serif';
+  }
+  .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
+  .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+    content: 'Serif';
+  }
+  .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
+  .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+    content: 'Monospace';
+  }
+  .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+    font-family: Georgia, Times New Roman, serif;
+  }
+  .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+    font-family: Monaco, Courier New, monospace;
+  }
+  .ql-snow .ql-picker.ql-size {
+    width: 98px;
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-label::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-item::before {
+    content: 'Normal';
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+    content: 'Small';
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+    content: 'Large';
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+    content: 'Huge';
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+    font-size: 10px;
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+    font-size: 18px;
+  }
+  .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+    font-size: 32px;
+  }
+  .ql-snow .ql-color-picker.ql-background .ql-picker-item {
+    background-color: #fff;
+  }
+  .ql-snow .ql-color-picker.ql-color .ql-picker-item {
+    background-color: #000;
+  }
+  .ql-toolbar.ql-snow {
+    border: 1px solid #ccc;
+    box-sizing: border-box;
+    font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+    padding: 8px;
+  }
+  .ql-toolbar.ql-snow .ql-formats {
+    margin-right: 15px;
+  }
+  .ql-toolbar.ql-snow .ql-picker-label {
+    border: 1px solid transparent;
+  }
+  .ql-toolbar.ql-snow .ql-picker-options {
+    border: 1px solid transparent;
+    box-shadow: rgba(0,0,0,0.2) 0 2px 8px;
+  }
+  .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label {
+    border-color: #ccc;
+  }
+  .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
+    border-color: #ccc;
+  }
+  .ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,
+  .ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover {
+    border-color: #000;
+  }
+  .ql-toolbar.ql-snow + .ql-container.ql-snow {
+    border-top: 0px;
+  }
+  .ql-snow .ql-tooltip {
+    background-color: #fff;
+    border: 1px solid #ccc;
+    box-shadow: 0px 0px 5px #ddd;
+    color: #444;
+    padding: 5px 12px;
+    white-space: nowrap;
+  }
+  .ql-snow .ql-tooltip::before {
+    content: "Visit URL:";
+    line-height: 26px;
+    margin-right: 8px;
+  }
+  .ql-snow .ql-tooltip input[type=text] {
+    display: none;
+    border: 1px solid #ccc;
+    font-size: 13px;
+    height: 26px;
+    margin: 0px;
+    padding: 3px 5px;
+    width: 170px;
+  }
+  .ql-snow .ql-tooltip a.ql-preview {
+    display: inline-block;
+    max-width: 200px;
+    overflow-x: hidden;
+    text-overflow: ellipsis;
+    vertical-align: top;
+  }
+  .ql-snow .ql-tooltip a.ql-action::after {
+    border-right: 1px solid #ccc;
+    content: 'Edit';
+    margin-left: 16px;
+    padding-right: 8px;
+  }
+  .ql-snow .ql-tooltip a.ql-remove::before {
+    content: 'Remove';
+    margin-left: 8px;
+  }
+  .ql-snow .ql-tooltip a {
+    line-height: 26px;
+  }
+  .ql-snow .ql-tooltip.ql-editing a.ql-preview,
+  .ql-snow .ql-tooltip.ql-editing a.ql-remove {
+    display: none;
+  }
+  .ql-snow .ql-tooltip.ql-editing input[type=text] {
+    display: inline-block;
+  }
+  .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+    border-right: 0px;
+    content: 'Save';
+    padding-right: 0px;
+  }
+  .ql-snow .ql-tooltip[data-mode=link]::before {
+    content: "Enter link:";
+  }
+  .ql-snow .ql-tooltip[data-mode=formula]::before {
+    content: "Enter formula:";
+  }
+  .ql-snow .ql-tooltip[data-mode=video]::before {
+    content: "Enter video:";
+  }
+  .ql-snow a {
+    color: #06c;
+  }
+  .ql-container.ql-snow {
+    border: 1px solid #ccc;
+  }
+
+  .ql-editor a{
+      color: #02c8ae;
+  }

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 10 - 0
packages/app-cdfg/public/editor/static/lib/vconsole.js


BIN
packages/app-cdfg/public/editor/static/music/01.mp3


BIN
packages/app-cdfg/public/editor/static/music/02.mp3


BIN
packages/app-cdfg/public/editor/static/music/03.mp3


BIN
packages/app-cdfg/public/editor/static/music/04.mp3


BIN
packages/app-cdfg/public/editor/static/music/05.mp3


BIN
packages/app-cdfg/public/editor/static/music/06.mp3


BIN
packages/app-cdfg/public/editor/static/music/07.mp3


BIN
packages/app-cdfg/public/editor/static/music/08.mp3


+ 27 - 0
packages/app-cdfg/public/epg.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+  <link rel="icon" href="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/favicon.ico">
+  <link rel="stylesheet" href="//at.alicdn.com/t/font_3429605_ey05txmx5ll.css">
+  <link rel="stylesheet" href="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/quill/quill.snow.css" />
+  <link rel="stylesheet" href="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/animate/animate.min.css" />
+  <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/mobile-detect.js"></script>
+  <title></title>
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+      Please enable it to continue.</strong>
+  </noscript>
+  <div id="app"></div>
+  <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>/kankan-sdk-deps.js?v=<%= VUE_APP_VERSION %>"></script>
+  <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>/kankan-sdk.js?v=<%= VUE_APP_VERSION %>"></script>
+  <!-- built files will be auto injected -->
+</body>
+
+</html>

+ 22 - 0
packages/app-cdfg/public/show.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="">
+    <head>
+        <meta charset="utf-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+        <link rel="icon" href="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/favicon.ico" />
+        <link rel="stylesheet" href="//at.alicdn.com/t/font_2596172_gh0n5kmw27.css" />
+        <!-- <link rel="stylesheet" href="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/animate/animate.min.css" /> -->
+        <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/mobile-detect.js"></script>
+        <title></title>
+    </head>
+
+    <body>
+        <noscript>
+            <strong>We're sorry but doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+        </noscript>
+        <div id="app"></div>
+        <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>/kankan-sdk-deps.js?v=<%= VUE_APP_VERSION %>"></script>
+        <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>/kankan-sdk.js?v=<%= VUE_APP_VERSION %>"></script>     <!-- built files will be auto injected -->
+    </body>
+</html>

+ 58 - 0
packages/app-cdfg/scripts/create-apis.js

@@ -0,0 +1,58 @@
+const fs = require('fs')
+const os = require('os')
+const path = require('path')
+const axios = require('axios')
+const host = 'http://120.25.146.52:3090/api/open/plugin/export-full?type=json&pid=18&status=all&token=0badd469d66bec93d28d3bb9ac053ddd9eacb3ebe53a446576fb0fa0ce2ca63c'
+const code = []
+axios.get(host).then(response => {
+    response.data.forEach(item => {
+        if (item.desc == 'scene-edit') {
+            code.push(`import { http } from '@/utils/request'`)
+            item.list.forEach(item => {
+                if (item.path.indexOf('/api/scene/') != -1) {
+                    generate(item, 'edit')
+                }
+            })
+            if (code.length) {
+                fs.writeFileSync(path.resolve(__dirname, `../src/apis/${item.desc}.js`), code.join(os.EOL))
+            }
+        }
+        code.length = 0
+    })
+})
+
+function generate(scheme, split) {
+    scheme.method = scheme.method.toLowerCase()
+    if (scheme.method == 'get' || scheme.method == 'post') {
+        code.push(`/**`)
+        code.push(`* ${scheme.title}`)
+        code.push(`* @param {object} data 传入的对象参数`)
+        if (scheme.req_body_other) {
+            let req_body = JSON.parse(scheme.req_body_other)
+            if (req_body.properties) {
+                for (let key in req_body.properties) {
+                    code.push(`* @param {${req_body.properties[key].type}} data.${key} ${req_body.properties[key].description || ''}`)
+                    if (req_body.properties[key].properties) {
+                        let properties = req_body.properties[key].properties
+                        for (let subKey in properties) {
+                            code.push(`* @param {${properties[subKey].type}} data.${key}.${subKey} ${properties[subKey].description}`)
+                        }
+                    }
+                }
+            }
+        } else if (scheme.req_body_form) {
+            scheme.req_body_form.forEach(item => {
+                code.push(`* @param {${item.type}} data.${item.name} ${item.desc || ''}`)
+            })
+        }
+        code.push(`* @returns {Promise}`)
+        code.push(`**/`)
+        code.push(`export const ${scheme.path.split(split)[1].substring(1).replace(/\//g, '_')} = data => {`)
+        if (scheme.req_body_type == 'form') {
+            code.push(`    return http.postFile('${scheme.path}', data)`)
+        } else {
+            code.push(`    return http.${scheme.method}('${scheme.path}', data)`)
+        }
+        code.push(`}`)
+    }
+}

+ 88 - 0
packages/app-cdfg/scripts/oss-upload-aws.js

@@ -0,0 +1,88 @@
+/*
+ * @Author: Rindy
+ * @Date: 2020-03-13 10:56:21
+ * @LastEditors: Rindy
+ * @LastEditTime: 2022-05-26 10:20:55
+ * @Description: 静态文件上传
+ */
+
+const { S3 } = require('@aws-sdk/client-s3')
+const fs = require('fs')
+const path = require('path')
+const mime = require('mime')
+const package = require('../package.json')
+process.env.AWS_ACCESS_KEY_ID = 'AKIAWCV5QFZ34YYVET2Q'
+process.env.AWS_SECRET_ACCESS_KEY = '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'
+
+const client = new S3({ region: 'eu-west-2' })
+const toPath = dist => path.resolve(dist)
+
+async function upload(filepath, key, cache, retry = 0) {
+    fs.readFile(filepath, (err, fileData) => {
+        if (err) {
+            console.log('上传失败:' + filepath)
+            throw err
+        }
+
+        client.putObject(
+            {
+                Bucket: '4dkankan',
+                Key: key,
+                Body: fileData,
+                ContentType: mime.getType(filepath),
+            },
+            (err, data) => {
+                if (err) {
+                    if (++retry < 3) {
+                        return upload(filepath, key, cache, retry)
+                    }
+
+                    console.log('上传失败:' + filepath)
+                    throw err
+                }
+
+                console.log('上传成功:' + key)
+            }
+        )
+    })
+}
+
+function list_files(dir, callback) {
+    debugger
+    fs.readdir(dir, (err, files) => {
+        if (err) {
+            throw err
+        }
+        files.forEach(async file => {
+            let filepath = path.join(dir, file)
+            const state = fs.statSync(filepath)
+            if (state.isDirectory()) {
+                list_files(filepath, callback)
+            } else {
+                if (filepath.endsWith('.map') == false) {
+                    callback(filepath)
+                }
+            }
+        })
+    })
+}
+
+function uploads() {
+    ;[
+        {
+            dir: '../../../dist/editor/editor',
+            key: 'v4/cdfg/editor',
+        },
+    ].forEach(item => {
+        list_files(toPath(path.join(__dirname, item.dir)), filepath => {
+            let temppath = filepath.replace(/\\/g, '/')
+            let keyspath = item.key + temppath.split(item.dir.replace(/\.\.\//g, ''))[1]
+            if (item.sdk) {
+                keyspath = item.key + '/' + package.version + temppath.split(item.dir)[1]
+            }
+            upload(filepath, keyspath, 'max-age=3153600') // 缓存30天
+        })
+    })
+}
+
+uploads()

+ 63 - 0
packages/app-cdfg/scripts/update-i18n.js

@@ -0,0 +1,63 @@
+const fs = require('fs')
+const path = require('path')
+const axios = require('axios')
+const locales = ['zh', 'en', 'kr', 'fr', 'fr', 'ja']
+
+const fetchLocale = async locale => {
+    return axios.get(`https://4dkk.4dage.com/v4/www/locales/${locale}.json?${Date.now()}`)
+}
+
+const merge = (source, resource, target) => {
+    for (let key in source) {
+        if (typeof source[key] === 'string') {
+            target[key] = resource[key] == void 0 ? source[key] : resource[key]
+        } else {
+            target[key] = {}
+            resource[key] = resource[key] || {}
+            merge(source[key], resource[key], target[key])
+        }
+    }
+}
+
+const combine = (source, resource, target) => {
+    merge(source, resource, target)
+}
+
+function exec() {
+    locales.forEach(locale => {
+        fetchLocale(locale)
+            .then(response => {
+                try {
+                    fs.readFile(path.join(__dirname, '..', 'src', 'locales', locale + '.json'), (err, data) => {
+                        if (err) {
+                            return
+                        }
+
+                        var target = {}
+                        var source = JSON.parse(data.toString())
+                        var resource = response.data
+
+                        combine(source, resource, target)
+
+                        fs.writeFile(path.join(__dirname, '..', 'src', 'locales', locale + '.json'), JSON.stringify(target, null, 4), () => {})
+                        if (locale == 'zh') {
+                            fs.writeFile(path.join(__dirname, '..', 'src', 'views', 'locales', locale + '.json'), JSON.stringify(target, null, 4), () => {})
+                        }
+                    })
+                } catch (error) {
+                    console.log(error)
+                }
+            })
+            .catch(err => {
+                if (locale == 'zh') {
+                    fs.readFile(path.join(__dirname, '..', 'src', 'locales', locale + '.json'), (err, data) => {
+                        if (err) {
+                            return
+                        }
+                        fs.writeFile(path.join(__dirname, '..', 'src', 'views', 'locales', locale + '.json'), data.toString(), () => {})
+                    })
+                }
+            })
+    })
+}
+exec()

+ 9 - 0
packages/app-cdfg/src/apis/index.js

@@ -0,0 +1,9 @@
+// import { useApp } from '@/app'
+// sdk
+// let sdk,
+//     apis = null
+// export async function getApis() {
+//     sdk = await useApp()
+//     apis = await sdk.remote_editor
+//     return apis
+// }

+ 103 - 0
packages/app-cdfg/src/apis/scene-edit.js

@@ -0,0 +1,103 @@
+import { http } from '@/utils/request'
+/**
+ * 场景发布
+ * @param {object} data 传入的对象参数
+ * @param {string} data.num 场景码
+ * @returns {Promise}
+ **/
+export const publicScene = data => {
+    return http.post('/api/scene/edit/publicScene', data)
+}
+/**
+ * 场景编辑保存
+ * @param {object} data 传入的对象参数
+ * @param {string} data.num 场景码
+ * @param {string} data.floorLogo 地面logo名称
+ * @param {number} data.floorLogoSize 地面logo大小
+ * @param {string} data.music  背景音乐名称
+ * @param {string} data.scenePassword 加密浏览密码
+ * @param {string} data.title 场景标题
+ * @param {string} data.description 场景描述
+ * @param {object} data.controls
+ * @param {number} data.controls.showMap 是否展示小地图(0-否,1-是)
+ * @param {number} data.controls.showLock 是否需要密码访问(0-否,1-是)
+ * @param {number} data.controls.showTitle 是否展示标题(0-否,1-是)
+ * @param {number} data.controls.showPanorama 是否展示漫游按钮(0-否,1-是)
+ * @param {number} data.controls.showDollhouse 是否展示3D按钮(0-否,1-是)
+ * @param {number} data.controls.showFloorplan 是否展示2D按钮(0-否,1-是)
+ * @returns {Promise}
+ **/
+export const saveScene = data => {
+    return http.post('/api/scene/edit/saveScene', data)
+}
+/**
+ * 文件上传
+ * @param {object} data 传入的对象参数
+ * @param {text} data.base64 图片base64
+ * @param {text} data.num 场景码
+ * @param {text} data.type 0添加,1替换,默认为1
+ * @param {file} data.files 文件数组
+ * @param {text} data.fileName 文件名称
+ * @param {text} data.bizType 业务类型
+ * @returns {Promise}
+ **/
+export const upload_files = data => {
+    return http.postFile('/api/scene/edit/upload/files', data)
+}
+/**
+ * 文件上传后保存
+ * @param {object} data 传入的对象参数
+ * @param {string} data.num 场景码
+ * @param {string} data.type 文件业务类型
+ * @param {string} data.fileInfo 文件信息,json格式字符串
+ * @returns {Promise}
+ **/
+export const saveUpload = data => {
+    return http.post('/api/scene/edit/saveUpload', data)
+}
+/**
+ * 获取场景详情-编辑页面
+ * @param {object} data 传入的对象参数
+ * @param {string} data.num 场景码
+ * @param {number} data.reqType 请求来源(1-编辑页面 2-查看页面)
+ * @returns {Promise}
+ **/
+export const getInfo = data => {
+    return http.get('/api/scene/edit/getInfo', data)
+}
+
+/**
+ * 添加热点/添加或修改热点
+ * @param {object} data 传入的对象参数
+ * @param {string} data.num 场景码
+ * @param {array} data.hotDataList 热点数据集合
+ * @param {array} data.icons icons集合
+ * @returns {Promise}
+ **/
+export const tag_save = data => {
+    return http.post('/service/scene/edit/tag/save', data)
+}
+
+export const get_goods_list = data => {
+    return http.post('/back/product/list', data)
+}
+
+export const get_hk_product_source = data => {
+    return http.get('/back/product/productSourceHK', data)
+}
+
+export const get_hk_goods_list = data => {
+    return http.post('/back/product/HKList', data)
+}
+
+export const get_category_list = data => {
+    return http.post('/back/category/allList', data)
+}
+
+export const get_scene_list = data => {
+    return http.post('/back/scene/allList', data)
+}
+
+export const get_qrCode = data => {
+    return http.get('/service/scene/edit/down/qrCode', data)
+}

+ 33 - 0
packages/app-cdfg/src/app.js

@@ -0,0 +1,33 @@
+let _app
+let _num
+let deferred = KanKan.Deferred()
+
+console.log('build-time: 2023.10.19 16:26');
+
+export function createApp(opitons = {}) {
+    if (_app) {
+        return
+    }
+    opitons.region = process.env.VUE_APP_REGION_URL
+    opitons.resource = process.env.VUE_APP_RESOURCE_URL
+    _num = opitons.num
+    _app = new KanKan(opitons)
+    deferred.resolve(_app)
+    window.__sdk = _app
+    return _app
+}
+
+export function useApp() {
+    if (_app) {
+        return Promise.resolve(_app)
+    }
+    return deferred
+}
+
+export function getApp() {
+    return _app
+}
+
+export function getNum() {
+    return _num
+}

BIN
packages/app-cdfg/src/assets/images/cad/style-1.jpg


BIN
packages/app-cdfg/src/assets/images/cad/style-2.jpg


BIN
packages/app-cdfg/src/assets/images/cad/style-3.jpg


BIN
packages/app-cdfg/src/assets/images/cad/style-4.jpg


BIN
packages/app-cdfg/src/assets/images/floorlogo/0.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/1.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/2.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/en/0.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/en/1.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/en/2.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/enter-style-default.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/enter-style-down.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/enter-style-up.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/icon-corner-24.png


BIN
packages/app-cdfg/src/assets/images/floorlogo/icon-corner.png


BIN
packages/app-cdfg/src/assets/images/loading.jpg


BIN
packages/app-cdfg/src/assets/images/roam/roam_checked.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_checked_256.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_invisible.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_invisible_256.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_uncheck.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_uncheck_256.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_visible.png


BIN
packages/app-cdfg/src/assets/images/roam/roam_visible_256.png


BIN
packages/app-cdfg/src/assets/images/tag/style-tag.png


+ 373 - 0
packages/app-cdfg/src/assets/theme.editor.scss

@@ -0,0 +1,373 @@
+// 资源图片目录地址(必要)
+$img-base-path: '~@kankan/components/src/assets/img/';
+
+@import '~@kankan/components/src/assets/scss/theme-editor.scss';
+
+:root {
+    --editor-warn-color: #fa3f48;
+    --editor-main-color: #00c8af;
+    --editor-font-color: #999;
+    --editor-toolbox-left: 0;
+    --editor-toolbox-width: 240px;
+    --editor-toolbox-padding: 0 10px;
+    --editor-men-color: rgba(255,255,255,0.6);
+    --editor-menu-active: rgba(0, 200, 175, 0.16);
+    --colors-primary-base: var(--editor-main-color);
+    --colors-primary-click: #005046;
+}
+.theme-color {
+    color: var(--editor-main-color);
+}
+::-webkit-scrollbar {
+    width: 4px;
+    height: 1px;
+}
+
+::-webkit-scrollbar-thumb {
+    border-radius: 4px;
+    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
+    background: #ccc;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #999;
+}
+
+::-webkit-scrollbar-track {
+    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
+    border-radius: 4px;
+    background: #000000;
+}
+input[type="password"]::-ms-reveal{
+    display:none
+}
+ul,
+li {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+}
+html {
+    line-height: 1.15;
+    -webkit-text-size-adjust: 100%;
+}
+html,
+body {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    font-size: 14px;
+    overflow: hidden;
+    user-select: none;
+    background-color: #292929;
+}
+
+#app {
+    width: 100%;
+    height: 100%;
+}
+
+.disable * {
+    opacity: 0.85;
+    pointer-events: none;
+}
+
+.enable {
+    opacity: 1 !important;
+    pointer-events: all !important;
+    * {
+        opacity: 1 !important;
+        pointer-events: all !important;
+    }
+}
+
+.hidden {
+    visibility: hidden !important;
+    pointer-events: none !important;
+    z-index: -1;
+}
+
+.scene {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    z-index: 1;
+}
+.slide-right-enter-active,
+.slide-right-leave-active {
+    will-change: transform;
+    transition: all 0.2s ease-in-out;
+}
+.slide-right-enter-from {
+    opacity: 0;
+    transform: translate3d(100%, 0, 0);
+}
+.slide-right-enter {
+    opacity: 1;
+    transform: translate3d(-100%, 0, 0);
+}
+.slide-right-leave-active {
+    opacity: 0;
+    transform: translate3d(100%, 0, 0);
+}
+// .slide-right-enter-active,
+// .slide-right-leave-active,
+// .slide-left-enter-active,
+// .slide-left-leave-active {
+//     will-change: transform;
+//     transition: all 0.35s ease-in-out;
+//     position: absolute;
+// }
+
+.fade-enter,
+.fade-leave-to {
+    opacity: 0;
+}
+.fade-enter-to,
+.fade-leave {
+    opacity: 1;
+}
+.fade-enter-active,
+.fade-leave-active {
+    transition: all .3s;
+}
+
+.slide-right-enter {
+    opacity: 1;
+    transform: translate3d(-100%, 0, 0);
+}
+.slide-right-leave-active {
+    opacity: 1;
+    transform: translate3d(100%, 0, 0);
+}
+.slide-left-enter {
+    opacity: 1;
+    transform: translate3d(-100%, 0, 0);
+}
+.slide-left-leave-active {
+    opacity: 1;
+    transform: translate3d(100%, 0, 0);
+}
+.ui-editor-layout.show{
+  .ui-editor-toolbar{
+    left: calc(var(--editor-menu-width));
+    right:calc(var(--editor-toolbox-width));
+  }
+  .tour-list{
+    left: calc(var(--editor-menu-width));
+    right:calc(var(--editor-toolbox-width));
+  }
+}
+.ui-editor-layout.full {
+    [xui_min_map] {
+        right: 20px !important;
+    }
+    .ui-editor-toolbar{
+        right:  0;
+        left:0;
+    }
+    .tour-list{
+        right:  0;
+        left:0;
+    }
+    .information {
+        left: 20px;
+    }
+    .bottom-controls {
+        left: 0;
+        right: 0;
+    }
+    .header-wrapper .opts{
+        display: none;
+    }
+}
+.ui-editor-layout.full-left {
+    .ui-editor-toolbar{
+        left: 0;
+    }
+    .tour-list{
+        left: 0;
+    }
+    .information {
+        left: 20px;
+    }
+    .bottom-controls {
+        left: 0;
+    }
+    .layer{
+        left: 0;
+    }
+}
+
+.ui-editor-layout.full-right {
+    [xui_min_map] {
+        right: 20px !important;
+    }
+    .ui-editor-toolbar{
+        left: calc(var(--editor-menu-width));
+        right:0;
+    }
+    .tour-list{
+        left: calc(var(--editor-menu-width));
+        right:0;
+    }
+    .bottom-controls {
+      right: 0;
+  }
+}
+
+
+.ui-editor-toolbox {
+  .inner-box{
+    height: calc(100% - 20px);
+    >div{
+      height: 100%;
+    }
+  }
+    .edit-list {
+      // min-height: 100%;
+      // overflow: hidden;
+      // height: calc(100% - 170px);
+      height: 100%;
+        li {
+            margin-top: 20px;
+            color: var(--editor-font-color);
+            border-bottom: solid 1px rgba(255, 255, 255, 0.16);
+            &:last-child {
+                border-bottom: none;
+            }
+            > div {
+                margin-bottom: 14px;
+            }
+            label {
+                color: #fff;
+                i {
+                    cursor: pointer;
+                }
+            }
+            .title {
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                span {
+                    font-weight: bold;
+                    font-size: 16px;
+                }
+            }
+
+            .between {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+            }
+        }
+    }
+}
+
+.ql-editor {
+    padding: 0;
+    word-break: break-all;
+    white-space: normal;
+}
+.ql-editor.ql-blank::before {
+    left: 0;
+    color: #999999;
+    font-size: 14px;
+}
+a {
+    color: #00c8af;
+}
+
+[xui_min_map] {
+  transition: right 0.3s;
+    top: 70px !important;
+    right: 260px !important;
+    &.hidden {
+        visibility: hidden !important;
+    }
+}
+.slider-box {
+    position: fixed;
+    top: 0;
+    right: 0;
+    width: var(--editor-toolbox-width);
+    height: 100%;
+    background: rgba(20, 20, 20, 1);
+    z-index: 1;
+    .slider-content {
+        color: rgba(255, 255, 255, 0.6);
+        .content-item {
+            padding: 20px 10px;
+            box-sizing: border-box;
+            border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+            box-sizing: border-box;
+            &:last-of-type {
+                border-bottom: 1px solid transparent;
+            }
+        }
+
+        .item-title {
+            font-size: 16px;
+            font-weight: bold;
+            color: #999;
+            margin-bottom: 14px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+        }
+    }
+    .slider-title {
+        height: 64px;
+        border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+        color: #999;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 20px 10px;
+        box-sizing: border-box;
+        span {
+            font-size: 18px;
+            color: #999;
+            font-weight: bold;
+        }
+        i {
+            cursor: pointer;
+        }
+    }
+}
+
+.vip-tag {
+    position: absolute;
+    width: 24px;
+    height: 24px;
+    background: url('~@/assets/images/floorlogo/icon-corner-24.png') no-repeat;
+    background-size: 100%;
+    top: 0;
+    right: 0;
+    z-index: 10;
+}
+.no-vip{
+    input{
+      pointer-events: none;
+    }
+    label{
+      pointer-events: none;
+    }
+}
+
+
+[is-mobile]{
+  [xui_min_map] {
+    transition: right 0.3s;
+      top:2.1333rem !important;
+      right: .2667rem !important;
+      width: 2.6667rem;
+      height: 2.6667rem;
+      &.hidden {
+          visibility: hidden !important;
+      }
+  }
+}

+ 108 - 0
packages/app-cdfg/src/components/Controls/BottomControl.vue

@@ -0,0 +1,108 @@
+<template>
+    <div class="bottom-controls" :class="{ 'is-edit': isEdit }" :style="{ bottom }">
+        <FloorSwitch v-show="showFloorSwitch" />
+        <LeftButtons v-show="showLeftControl" :is-edit="isEdit" />
+        <RightButtons v-show="showRightControl" :is-edit="isEdit" />
+    </div>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import FloorSwitch from './FloorSwitch'
+import LeftButtons from './LeftButtons'
+import RightButtons from './RightButtons'
+const store = useStore()
+const isEdit = computed(() => store.getters.controlsEditMode)
+const bottom = computed(() => {
+    if (isEdit.value) {
+        return '80px'
+    }
+    return store.getters.controlsBottom
+})
+const showFloorSwitch = computed(() => {
+    return store.getters.sceneUIParts.floorSwitch || store.getters.sceneUI == true
+})
+const showLeftControl = computed(() => {
+    return store.getters.sceneUI == true
+})
+const showRightControl = computed(() => {
+    return store.getters.sceneUI == true
+})
+</script>
+<style lang="scss" scoped>
+.bottom-controls {
+    position: absolute;
+    left: var(--editor-menu-width);
+    right: var(--editor-toolbox-width);
+    bottom: 20px;
+    height: 34px;
+    display: flex;
+    justify-content: space-between;
+    transition: bottom 0.3s ease;
+    &.is-edit {
+        right: 0;
+    }
+}
+:deep(.buttons) {
+    pointer-events: all;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    height: 34px;
+    border-radius: 17px;
+    background-color: rgba(0, 0, 0, 0.3);
+    &.is-edit {
+        > div {
+            cursor: default;
+            span {
+                display: flex;
+            }
+        }
+    }
+    > div {
+        position: relative;
+        margin-left: 20px;
+        margin-right: 20px;
+        cursor: pointer;
+        &.active {
+            color: var(--editor-main-color);
+        }
+        > i {
+            font-size: 18px;
+        }
+        span {
+            cursor: pointer;
+            display: none;
+            position: absolute;
+            top: -20px;
+            right: -15px;
+            width: 24px;
+            height: 24px;
+            background-color: rgba(0, 0, 0, 0.5);
+            border-radius: 50%;
+            align-items: center;
+            justify-content: center;
+            color: var(--editor-main-color);
+            transition: all 0.1s;
+
+            &:hover {
+                transform: scale(1.2);
+            }
+            &.disable {
+                i {
+                    opacity: 0.5;
+                }
+            }
+        }
+    }
+}
+[is-mobile] .bottom-controls {
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    width: 100%;
+    transition: bottom 0.3s ease;
+    pointer-events: none;
+}
+</style>

+ 283 - 0
packages/app-cdfg/src/components/Controls/FloorSwitch.vue

@@ -0,0 +1,283 @@
+<template>
+    <div class="floor-switch" :class="{ disable: flying }" v-if="floors.length > 1 && mode != 'panorama'">
+        <ul>
+            <li v-if="mode != 'floorplan'" :class="{ active: 'all' == floorId }" @click.stop="onGotoFloor('all')">
+                <b></b><span>{{ $t('repair.toolbox.allPhoto') }}</span>
+            </li>
+            <li v-for="item in floors" :key="item.id" :class="{ active: item.id == floorId }" @click.stop="onGotoFloor(item.id)">
+                <b></b><span>{{ item.name }}</span>
+            </li>
+        </ul>
+    </div>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import { useApp } from '@/app'
+const store = useStore()
+const mode = computed(() => store.getters.mode)
+const flying = computed(() => store.getters['flying'])
+const floors = computed(() => store.getters['scene/floors'])
+const floorId = computed(() => store.getters.floorId)
+const onGotoFloor = id => {
+    store.commit('setFloor', id)
+}
+useApp().then(sdk =>
+    sdk.Camera.on('mode.beforeChange', ({ toMode, floorIndex }) => {
+        store.commit('mode', toMode)
+        if (toMode != 'dollhouse') {
+            store.commit('setFloorId', floorIndex)
+        }
+    })
+)
+</script>
+
+<style lang="scss" scoped>
+.floor-switch {
+    pointer-events: all;
+    position: absolute;
+    bottom: calc(100% + 5px);
+    left: 20px;
+    z-index: 10;
+    ul,
+    li {
+        padding: 0;
+        margin: 0;
+    }
+    ul {
+        position: relative;
+        z-index: 2;
+    }
+    li {
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        position: relative;
+        height: 50px;
+        &:first-child {
+            b {
+                &::before {
+                    display: none;
+                }
+            }
+            &.active {
+                b {
+                    &::after {
+                        bottom: -10px;
+                    }
+                }
+            }
+        }
+        &:last-child {
+            b {
+                &::after {
+                    display: none;
+                }
+            }
+            &.active {
+                b {
+                    &::before {
+                        top: -10px;
+                    }
+                }
+            }
+        }
+        b {
+            position: relative;
+            width: 16px;
+            height: 16px;
+            background-color: #1c1c1c;
+            border-radius: 50%;
+            box-shadow: 0px 0px 2px 1px #404040;
+            cursor: pointer;
+            &::before {
+                content: '';
+                position: absolute;
+                top: -5px;
+                left: 50%;
+                margin-left: -3px;
+                background: #1c1c1c;
+                width: 6px;
+                height: 6px;
+            }
+            &::after {
+                content: '';
+                position: absolute;
+                bottom: -5px;
+                left: 50%;
+                margin-left: -3px;
+                background: #1c1c1c;
+                width: 6px;
+                height: 6px;
+            }
+        }
+        span {
+            margin-left: 10px;
+            font-size: 14px;
+            //color: #939393;
+            cursor: pointer;
+            color: #eeeeee;
+            text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);
+        }
+        &.active {
+            b {
+                left: -6px;
+                width: 28px;
+                height: 28px;
+                border: solid 6px #1c1c1c;
+                background-color: #404040;
+                box-shadow: 0px 0px 2px 1px #404040;
+                &::before {
+                    top: -10px;
+                }
+                &::after {
+                    bottom: -10px;
+                }
+            }
+            span {
+                margin-left: 0;
+                position: relative;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                height: 32px;
+                color: rgba(255, 255, 255, 0.88);
+                border: solid 4px #1c1c1c;
+                background-color: #404040;
+                border-radius: 32px;
+                box-shadow: 0px 0px 2px 1px #404040;
+                padding: 0px 10px;
+                text-shadow: none;
+                &::before {
+                    content: '';
+                    position: absolute;
+                    left: -10px;
+                    background: #1c1c1c;
+                    width: 7px;
+                    height: 4px;
+                    box-shadow: -2px 0px 2px 1px #404040;
+                }
+                &::after {
+                    content: '';
+                    position: absolute;
+                    left: -16px;
+                    background: #1c1c1c;
+                    width: 6px;
+                    height: 11px;
+                    border-radius: 50%;
+                }
+            }
+        }
+    }
+    &::after {
+        content: '';
+        position: absolute;
+        left: 5px;
+        top: 20px;
+        bottom: 20px;
+        width: 6px;
+        background: #1c1c1c;
+        box-shadow: 0px 0px 2px 1px #404040;
+        z-index: 1;
+    }
+}
+[is-mobile] {
+    .floor-switch {
+        bottom: 3rem;
+        left: 0.5333rem;
+        li {
+            height: 45px;
+            &:first-child {
+                &.active {
+                    b {
+                        &::after {
+                            bottom: -6px;
+                        }
+                    }
+                }
+            }
+            &:last-child {
+                &.active {
+                    b {
+                        &::before {
+                            top: -6px;
+                        }
+                    }
+                }
+            }
+            b {
+                width: 14px;
+                height: 14px;
+                &::before {
+                    top: -3px;
+                    left: 50%;
+                    margin-left: -2px;
+                    width: 4px;
+                    height: 4px;
+                }
+                &::after {
+                    bottom: -3px;
+                    left: 50%;
+                    margin-left: -2px;
+                    width: 4px;
+                    height: 4px;
+                }
+            }
+            span {
+                font-size: 0.36842rem;
+                white-space: nowrap;
+            }
+            &.active {
+                b {
+                    left: -3px;
+                    width: 20px;
+                    height: 20px;
+                    border: solid 4px #1c1c1c;
+                    &::before {
+                        top: -6px;
+                    }
+                    &::after {
+                        bottom: -6px;
+                    }
+                }
+                span {
+                    height: 30px;
+                    border: solid 3px #1c1c1c;
+                    border-radius: 30px;
+                    padding: 0px 10px;
+                    margin-left: 5px;
+                    &::before {
+                        left: -11px;
+                        width: 10px;
+                        height: 4px;
+                        box-shadow: -2px 0px 1px 1px #404040;
+                    }
+                    &::after {
+                        left: -14px;
+                        width: 3px;
+                        height: 6px;
+                        border-radius: 40%;
+                    }
+                }
+            }
+        }
+        &::after {
+            left: 5px;
+            top: 20px;
+            bottom: 20px;
+            width: 4px;
+        }
+    }
+
+    @media (orientation: landscape) {
+        .floor-switch {
+            bottom: 1rem;
+            li {
+                span {
+                    font-size: 0.25rem;
+                }
+            }
+        }
+    }
+}
+</style>

+ 555 - 0
packages/app-cdfg/src/components/Controls/LeftButtons.vue

@@ -0,0 +1,555 @@
+<template>
+    <div class="controls-left-buttons" :class="{ disabled: flying }">
+        <!-- <div class="buttons tour" :class="{ 'is-edit': isEdit }" v-if="isEdit">
+            <div @click="onModeChange('panorama')">
+                <i class="iconfont" :class="['icon-preview']"></i>
+            </div>
+            <div class="show-list">
+                <i class="iconfont" :class="['icon-pull-down']"></i>
+                <span @click="onSetControls('tour')" :class="{ disable: !controls.tour }">
+                    <i class="iconfont icon-eye-s"></i>
+                </span>
+            </div>
+        </div> -->
+        <teleport :to="refMinMap" v-if="refMinMap">
+            <span class="min-map-visible" @click.stop="onSetControls('showMap')" :class="{ hide: !controls.showMap }">
+                <i class="iconfont icon-eye-s"></i>
+            </span>
+        </teleport>
+        <div class="buttons mode" :class="{ 'is-edit': isEdit, disabled: flying || isPlay, single: single, 'flex-start': isMobile }">
+            <div :class="{ active: viewmode == 'panorama', hiding: !controls.showPanorama }" @click="onModeChange('panorama')">
+                <ui-icon :tip="$t('mode.panorama')" tipV="top" :type="viewmode == 'panorama' ? 'show_roaming_selected' : 'show_roaming_normal'"></ui-icon>
+                <p class="mobile-text">{{ $t('mode.panorama') }}</p>
+                <span @click="onSetControls('showPanorama')" :class="{ hide: !controls.showPanorama }">
+                    <i class="iconfont icon-eye-s"></i>
+                </span>
+            </div>
+            <div :class="{ active: viewmode == 'floorplan', hiding: !controls.showFloorplan }" @click="onModeChange('floorplan')">
+                <ui-icon :tip="$t('mode.floorplan')" tipV="top" :type="viewmode == 'floorplan' ? 'show_plane_selected' : 'show_plane_normal'"></ui-icon>
+                <p class="mobile-text">2D</p>
+                <span @click="onSetControls('showFloorplan')" :class="{ hide: !controls.showFloorplan }">
+                    <i class="iconfont icon-eye-s"></i>
+                </span>
+            </div>
+            <div :class="{ active: viewmode == 'dollhouse', hiding: !controls.showDollhouse }" @click="onModeChange('dollhouse')">
+                <ui-icon :tip="$t('mode.dollhouse')" tipV="top" :type="viewmode == 'dollhouse' ? 'show_3d_selected' : 'show_3d_normal'"></ui-icon>
+                <p class="mobile-text">3D</p>
+                <span @click="onSetControls('showDollhouse')" :class="{ hide: !controls.showDollhouse }">
+                    <i class="iconfont icon-eye-s"></i>
+                </span>
+            </div>
+        </div>
+        <div class="play-control" v-if="tours.length > 0 && router.name == 'tour' && !isMobile" :class="{ disabled: flying }">
+            <div class="play-btn tour-btn" @click="playTour">
+                <ui-icon :tip="isPlay ? $t('tour.toolbar.pauseTour') : $t('tour.toolbar.playTour')" tipV="top" :type="isPlay ? 'pause' : 'preview'"></ui-icon>
+            </div>
+
+            <!-- <div class="tour-btn bor" @click="openTours">
+                <ui-icon type="pull-down" :class="{ active: showTours }"></ui-icon>
+            </div>
+            <teleport :to="editorMain">
+                <div class="tour-list" :style="`height:${showTours ? '120px' : '0px'};`">
+                    <div class="part-content" ref="tourScroll">
+                        <div class="part-list" v-if="tours.length > 1">
+                            <div
+                                @click="changeFrame(0)"
+                                :class="{ disabled: isPlay && partId != index }"
+                                class="part-item"
+                                v-for="(i, index) in tours"
+                                :style="`background-image:url(${common.changeUrl(i.list[0].enter.cover)});`"
+                            >
+                                <div class="part-title">{{ i.name }}</div>
+                                <div v-if="partId == index" class="precent" :style="`width:${progressNum}%;`"></div>
+                            </div>
+                        </div>
+                        <div class="part-list frame-list" v-else>
+                            <div
+                                @click="changeFrame(index)"
+                                :class="{ disabled: isPlay && frameId != index }"
+                                class="part-item"
+                                v-for="(i, index) in tours[0].list"
+                                :style="`background-image:url(${common.changeUrl(i.enter.cover)});`"
+                            >
+                                <div class="part-title"></div>
+                                <div v-if="frameId == index" class="precent" :style="`width:${progressNum}%;`"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </teleport> -->
+        </div>
+    </div>
+</template>
+<script setup>
+import { computed, defineProps, ref, watch, onMounted, nextTick } from 'vue'
+import { useApp, getApp } from '@/app'
+import { useStore } from 'vuex'
+import { Scrollbar, Dialog } from '@kankan/components'
+import { useMusicPlayer } from '@/utils/sound'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const musicPlayer = useMusicPlayer()
+const store = useStore()
+const isMobile = computed(() => store.getters['mobile'])
+const isInit = ref(false)
+const tours = computed(() => {
+    let tours = store.getters['tour/tours']
+    // console.error(tours)
+    // if (tours.length > 0) {
+    //     if (tourScroll.value && !isInit.value) {
+    //         isInit.value = true
+    //         new Scrollbar(tourScroll.value, { onlyHorizontal: true })
+    //     }
+    // }
+    return tours
+})
+const showDescription = computed(() => store.getters['scene/showDescription'])
+const frameId = computed(() => store.getters['tour/frameId'])
+const partId = computed(() => store.getters['tour/partId'])
+const hlIndex = computed(() => store.getters['tour/hlIndex'])
+const router = computed(() => store.getters['router'])
+const progressNum = ref(0)
+const isPlay = computed(() => {
+    let status = store.getters['tour/isPlay']
+    let map = document.querySelector('.kankan-app div[xui_min_map]')
+    if (map) {
+        if (status) {
+            map.classList.add('disabled')
+        } else {
+            map.classList.remove('disabled')
+        }
+    }
+
+    return status
+})
+watch(showDescription, val => {
+    if (controls.value.showMap) {
+        if (val) {
+            getApp().MinMap.hide(true)
+        } else {
+            getApp().MinMap.show(true)
+        }
+    }
+})
+watch(isPlay, val => {
+    // debugger
+})
+
+const showTours = computed(() => store.getters['tour/showTours'])
+const props = defineProps({
+    isEdit: Boolean,
+})
+
+const viewmode = computed(() => store.getters.mode)
+const controls = computed(() => store.getters['settings/controls'])
+const flying = computed(() => store.getters['flying'])
+const single = computed(() => {
+    if (props.isEdit) {
+        return
+    }
+    let value = controls.value
+    if (value) {
+        if (value.showPanorama + value.showFloorplan + value.showDollhouse == 1) {
+            return true
+        }
+    }
+})
+const tourScroll = ref(null)
+const refMinMap = ref(null)
+const editorMain = ref(null)
+
+const onModeChange = name => {
+    if (props.isEdit || viewmode.value == name) {
+        return
+    }
+    store.commit('setMode', name)
+}
+const playTour = async () => {
+    let player = await getApp().TourManager.player
+    if (isPlay.value) {
+        store.commit('tour/setData', { isPlay: false })
+        player.pause()
+    } else {
+        // if (hlIndex.value != -1) {
+        //     player.play(hlIndex.value)
+        // } else {
+        //     player.play()
+        // }
+        player.play(partId.value)
+        store.commit('tour/setData', { isPlay: true, hlIndex: -1 })
+        store.commit('setToolbox', {
+            show: false,
+            type: null,
+        })
+    }
+}
+
+const hanlderTour = async () => {
+    let player = await getApp().TourManager.player
+    player.on('play', tours => {
+        musicPlayer.pause(true)
+    })
+    player.on('pause', tours => {
+        // console.log('pause')
+        store.commit('tour/setData', { isPlay: false })
+    })
+    player.on('end', tours => {
+        store.commit('tour/setData', { isPlay: false })
+        console.log('end')
+    })
+
+    player.on('progress', ({ partId, frameId, progress }) => {
+        progressNum.value = progress * 100
+        // store.commit('tour/setData', { partId, frameId, isPlay: true })
+        store.commit('tour/setData', { partId, frameId })
+    })
+
+    nextTick(() => {
+        editorMain.value = document.querySelector('.ui-editor-main')
+    })
+}
+
+const onSetControls = name => {
+    let msg = ''
+    if (name == 'showPanorama') {
+        // msg = '漫游视角'
+        msg = t('mode.panorama') + t('mode.view')
+    } else if (name == 'showFloorplan') {
+        msg = t('mode.floorplan') + t('mode.view')
+    } else if (name == 'showDollhouse') {
+        msg = t('mode.dollhouse') + t('mode.view')
+    } else if (name == 'showMap') {
+        msg = t('mode.miniMap')
+    }
+    if (controls.value[name]) {
+        Dialog.toast(msg + t('mode.function') + t('common.isClose'))
+    } else {
+        Dialog.toast(msg + t('mode.function') + t('common.isOpen'))
+    }
+    store.commit('settings/controls', name)
+}
+
+watch(controls, () => {
+    if (!props.isEdit) {
+        useApp().then(app => {
+            if (controls.value.showMap) {
+                app.MinMap.show(true)
+            } else {
+                app.MinMap.hide(true)
+            }
+        })
+    }
+})
+
+watch(props, () => {
+    if (props.isEdit) {
+        getApp()
+            .MinMap.show(true)
+            .then(() => {
+                refMinMap.value = '[xui_min_map]'
+            })
+    } else {
+        if (controls.value.showMap) {
+            getApp().MinMap.show(true)
+        } else {
+            getApp().MinMap.hide(true)
+        }
+        refMinMap.value = null
+    }
+})
+const openTours = () => {
+    // showTours.value = !showTours.value
+    store.commit('tour/setData', { showTours: !showTours.value })
+    let bot = '20px'
+    if (showTours.value) {
+        bot = '140px'
+    } else {
+        bot = '20px'
+    }
+    store.commit('setControlsBottom', bot)
+}
+const changeFrame = id => {
+    recorder.selectFrame(id)
+}
+const onClickHandler = async () => {
+    if (isPlay.value) {
+        let player = await getApp().TourManager.player
+        player.pause()
+        // musicPlayer.resume()
+    } else {
+        store.commit('tour/setData', {
+            hlIndex: -1,
+        })
+        store.commit('setToolbox', {
+            show: false,
+            type: null,
+        })
+    }
+}
+let recorder = null
+onMounted(() => {
+    useApp().then(async sdk => {
+        recorder = await sdk.TourManager.recorder
+        hanlderTour()
+    })
+})
+
+watch(
+    () => router.value.name,
+    (val, old) => {
+        nextTick(() => {
+            if (val == 'tour') {
+                const player = document.querySelector('.player[name="main"]')
+                if (player) {
+                    player.addEventListener('click', onClickHandler)
+                }
+            } else {
+                const player = document.querySelector('.player[name="main"]')
+                if (player) {
+                    player.removeEventListener('click', onClickHandler)
+                }
+            }
+        })
+    },
+    {
+        deep: true,
+    }
+)
+</script>
+
+<style lang="scss" scoped>
+.play-control {
+    pointer-events: all;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 34px;
+    border-radius: 17px;
+    background-color: rgba(0, 0, 0, 0.3);
+    min-width: 34px;
+    border-radius: 17px;
+    margin-left: 10px;
+    .tour-btn {
+        width: 34px;
+        height: 22px;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        > .iconfont {
+            font-size: 14px;
+        }
+        &.play-btn {
+            // border-right: 1px solid rgba(255, 255, 255, 0.2);
+            > .iconfont {
+                font-size: 20px;
+                transition: rotate 0.3s;
+            }
+        }
+        &.bor {
+            border-left: 1px solid rgba(255, 255, 255, 0.2);
+
+            > .iconfont {
+                transform: rotate(180deg);
+                &.active {
+                    transform: rotate(0deg);
+                }
+            }
+        }
+    }
+}
+.controls-left-buttons {
+    margin-left: 20px;
+    margin-bottom: 20px;
+    display: flex;
+    .hide {
+        color: #999;
+    }
+    .hiding {
+        display: none;
+    }
+    .is-edit {
+        .hiding {
+            display: block;
+        }
+    }
+}
+.buttons.mode {
+    justify-content: space-between;
+    > div {
+        .mobile-text {
+            display: none;
+        }
+    }
+}
+.buttons.single {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    width: 34px;
+    height: 34px;
+    margin: 0;
+    &.flex-start {
+        justify-content: flex-start;
+    }
+}
+.buttons.tour {
+    margin-right: 10px;
+    > div {
+        margin-left: 0px;
+        margin-right: 0px;
+        padding: 0 10px;
+        &.show-list {
+            border-left: solid 1px var(--editor-font-color);
+        }
+        .icon-pull-down {
+            font-size: 12px;
+        }
+        span {
+            right: -10px;
+        }
+    }
+}
+</style>
+<style lang="scss">
+.min-map-visible {
+    display: flex;
+    cursor: pointer;
+    position: absolute;
+    top: -14px;
+    right: -10px;
+    width: 24px;
+    height: 24px;
+    background-color: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    align-items: center;
+    justify-content: center;
+    color: var(--editor-main-color);
+    transition: all 0.1s;
+    &:hover {
+        transform: scale(1.2);
+    }
+    &.hide {
+        color: #999;
+    }
+    i {
+        font-size: 18px;
+    }
+}
+.tour-list {
+    position: absolute;
+    bottom: 0;
+    right: calc(var(--editor-toolbox-width) + var(--editor-menu-right));
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    // height: var(--editor-toolbar-height);
+    height: 120px;
+    background-color: var(--editor-menu-back);
+    pointer-events: all;
+    left: var(--editor-toolbox-left);
+    z-index: 1;
+    transition: all 0.3s ease;
+    .part-content {
+        display: flex;
+        flex-direction: row;
+        overflow: hidden;
+        width: 100%;
+        height: 100%;
+        .part-list {
+            width: 100%;
+            height: 120px;
+            display: flex;
+            align-items: center;
+            justify-content: flex-start;
+            padding: 0 10px;
+            box-sizing: border-box;
+            .part-item {
+                width: 120px;
+                height: 80px;
+                position: relative;
+                cursor: pointer;
+                margin-left: 10px;
+                background-repeat: no-repeat;
+                background-size: 100%;
+                background-position: center;
+                &:first-of-type {
+                    margin-left: 0px;
+                }
+                .precent {
+                    width: 10%;
+                    height: 24px;
+                    position: absolute;
+                    bottom: 0;
+                    left: 0;
+                    background: #00c8af;
+                    opacity: 0.4;
+                    z-index: 1;
+                    transition: all 0.1s;
+                }
+                .part-title {
+                    width: 100%;
+                    height: 24px;
+                    background: linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.5) 100%);
+                    position: absolute;
+                    bottom: 0;
+                    left: 0;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 14px;
+                    z-index: 10;
+                }
+            }
+        }
+    }
+}
+[is-mobile] {
+    .controls-left-buttons {
+        bottom: 2.96rem;
+        right: 0.28rem;
+        position: absolute;
+        margin-bottom: 0;
+        margin-left: 0;
+        .buttons.mode {
+            // flex-direction: column;
+            // height: 5.1467rem;
+            background-color: transparent;
+            flex-direction: column-reverse;
+            -webkit-flex-direction: column-reverse;
+
+            > div {
+                // margin: 0 0 0.4667rem;
+                margin: 0 0 1rem;
+                .iconfont {
+                    font-size: 0.64rem;
+                }
+                &.active {
+                    .iconfont {
+                        background: rgba(0, 0, 0, 0.2);
+                        padding: 0.2667rem;
+                        border-radius: 50%;
+                    }
+                }
+
+                .mobile-text {
+                    display: block;
+                    text-align: center;
+                    // margin-top: 0.2667rem;
+                    font-size: 0.32rem;
+                    font-weight: 500;
+                    color: #fff;
+                    position: absolute;
+                    left: 50%;
+                    transform: translateX(-50%);
+                    bottom: -90%;
+                    white-space: nowrap;
+                }
+            }
+        }
+    }
+}
+</style>

+ 202 - 0
packages/app-cdfg/src/components/Controls/RightButtons.vue

@@ -0,0 +1,202 @@
+<template>
+    <div class="controls-right-buttons" v-if="mode == 'panorama'" :class="{ 'is-edit': isEdit, disabled: flying || isPlay }">
+        <div :class="{ hiding: !controls.showVR }" @click="onHandler('vr')" v-show="router.name == 'settings' || isMobile">
+            <ui-icon type="vr" :tip="$t('mode.vr')" tipV="top"></ui-icon>
+            <p class="mobile-text">VR</p>
+            <!-- <i class="iconfont icon-vr"></i> -->
+            <span @click="onSetControls('showVR')" :class="{ hide: !controls.showVR }">
+                <i class="iconfont icon-eye-s"></i>
+            </span>
+        </div>
+        <!-- 
+        <div>
+            <i class="iconfont icon-rule"></i>
+            <span @click="onSetControls('showRule')" :class="{ disable: !controls.showRule }">
+                <i class="iconfont icon-eye-s"></i>
+            </span>
+        </div> -->
+
+        <div class="music-icon" v-if="showMusic && !isEdit" @click="onHandler('music')" v-show="router.name == 'info' || isMobile">
+            <!-- <i class="iconfont icon-music"></i> -->
+            <ui-icon type="music" :class="{ playing: showMusicPlaying }" :tip="showMusicPlaying ? '' : $t('mode.music')" tipV="top"></ui-icon>
+            <p class="mobile-text">{{ $t('mode.music') }}</p>
+        </div>
+    </div>
+</template>
+<script setup>
+import { computed, ref, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import { useMusicPlayer } from '@/utils/sound'
+import { getApp, getNum } from '@/app'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const isMobile = computed(() => store.getters['mobile'])
+const mode = computed(() => store.getters['mode'])
+const musicPlayer = useMusicPlayer()
+
+import { Dialog } from '@kankan/components'
+
+const props = defineProps({
+    isEdit: Boolean,
+})
+const store = useStore()
+const router = computed(() => store.getters.router)
+const controls = computed(() => store.getters['settings/controls'])
+const showMusic = computed(() => store.getters['info/musicURL'])
+const isPlay = computed(() => store.getters['tour/isPlay'])
+const flying = computed(() => store.getters['flying'])
+const showMusicPlaying = ref(false)
+
+const onSetControls = name => {
+    let msg = ''
+    if (name == 'showVR') {
+        msg = t('mode.vr')
+    }
+
+    if (controls.value[name]) {
+        Dialog.toast(msg + t('common.isClose'))
+    } else {
+        Dialog.toast(msg + t('common.isOpen'))
+    }
+    store.commit('settings/controls', name)
+}
+
+const onHandler = name => {
+    if (props.isEdit) {
+        return
+    }
+    if (name == 'vr') {
+        Dialog.toast(t('limit.viewInVr'))
+    } else if (name == 'music') {
+        showMusicPlaying.value ? musicPlayer.pause() : musicPlayer.play()
+    }
+}
+
+musicPlayer.on('play', () => (showMusicPlaying.value = true))
+musicPlayer.on('pause', () => (showMusicPlaying.value = false))
+</script>
+<style lang="scss" scoped>
+.controls-right-buttons {
+    pointer-events: all;
+    display: flex;
+    margin-right: 20px;
+    margin-bottom: 20px;
+    &.is-edit {
+        > div {
+            cursor: default;
+            span {
+                display: flex;
+            }
+        }
+        .hiding {
+            display: flex;
+        }
+    }
+    > div {
+        position: relative;
+        cursor: pointer;
+        margin-left: 10px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 34px;
+        height: 34px;
+        background-color: rgba(0, 0, 0, 0.3);
+        border-radius: 50%;
+        pointer-events: all;
+        &.music-icon {
+        }
+        .mobile-text {
+            display: none;
+        }
+        > i {
+            font-size: 18px;
+            display: block;
+        }
+        span {
+            cursor: pointer;
+            display: none;
+            position: absolute;
+            top: -14px;
+            right: -8px;
+            width: 24px;
+            height: 24px;
+            background-color: rgba(0, 0, 0, 0.5);
+            border-radius: 50%;
+            align-items: center;
+            justify-content: center;
+            color: var(--editor-main-color);
+            transition: all 0.1s;
+
+            &:hover {
+                transform: scale(1.2);
+            }
+            &.disable {
+                i {
+                    opacity: 0.5;
+                }
+            }
+        }
+    }
+    .hide {
+        color: #999;
+    }
+    .hiding {
+        display: none;
+    }
+}
+.playing {
+    animation: spinner 4s linear infinite;
+}
+@keyframes spinner {
+    0% {
+        transform: rotate(0);
+        transform-origin: center center;
+    }
+
+    to {
+        transform: rotate(1turn);
+        transform-origin: center center;
+    }
+}
+
+[is-mobile] {
+    .controls-right-buttons {
+        flex-direction: column;
+        bottom: 2.96rem;
+        left: 0.28rem;
+        margin-right: 0px;
+        margin-bottom: 0px;
+        position: absolute;
+        padding: 0.2667rem 0.2667rem 0;
+        > div {
+            background: transparent;
+            width: auto;
+            height: auto;
+            display: block;
+            // margin: 0 0 0.4667rem;
+            margin: 0 0 1rem;
+            &.music-icon {
+                // display: none;
+            }
+            > .iconfont {
+                font-size: 0.64rem;
+                display: inline-block;
+            }
+            .mobile-text {
+                display: block;
+                text-align: center;
+                margin-top: 0.2667rem;
+                font-size: 0.32rem;
+                font-weight: 500;
+                color: #fff;
+                position: absolute;
+                left: 50%;
+                transform: translateX(-50%);
+                bottom: -90%;
+                white-space: nowrap;
+            }
+        }
+    }
+}
+</style>

+ 126 - 0
packages/app-cdfg/src/components/Header/Edit.vue

@@ -0,0 +1,126 @@
+<template>
+    <div x-name="header-edit" class="header-wrapper">
+        <span class="title">{{ title }}<b />{{ $t(`menu.${router.name}`) }}</span>
+        <div class="buttons" v-show="show">
+            <ui-button type="normal" @click.stop="onCancel">{{ $t('common.exit') }}</ui-button>
+            <ui-button type="primary" @click.stop="onSave">{{ $t('common.save') }}</ui-button>
+        </div>
+    </div>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import { Loading, Dialog } from '@kankan/components'
+import { getApp, getNum } from '@/app'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const title = computed(() => store.getters['scene/metadata'].title)
+const module = computed(() => store.getters.editModule)
+const router = computed(() => store.getters.router)
+const enterVisible = computed(() => store.getters['tag/enterVisible'])
+const show = computed(() => {
+    // 基础设置的编辑按钮需要特性处理
+    if (store.getters.toolbar && store.getters.toolbar.show && module.value == 'settings') {
+        return false
+    }
+    return true
+})
+const onSave = () => {
+    Loading.show()
+    store
+        .dispatch(`${module.value}/save`)
+        .then(response => {
+            console.log(response)
+            Loading.hide()
+            if (response) {
+                if (response.success) {
+                    Dialog.toast.hide()
+                    Dialog.toast({ content: t('common.saveSuccess'), type: 'success' })
+                } else {
+                    Dialog.toast.hide()
+                    Dialog.toast({ content: t('common.busy'), type: 'error' })
+                }
+            } else {
+            }
+        })
+        .catch(error => {
+            console.log(error)
+            Loading.hide()
+            if (error) {
+                // console.error(error)
+                // Dialog.toast.hide()
+
+                Dialog[error.type]({
+                    content: t(error.msg),
+                    type: error.tips,
+                })
+                // if (typeof error === 'object' && error.errorMsg) {
+                //     Dialog.toast({ content: '保存失败,' + error.errorMsg, type: 'error' })
+                // } else {
+                //     Dialog.toast({ content: '保存失败,请重试!', type: 'error' })
+                // }
+            }
+        })
+}
+const onCancel = () => {
+    if (router.value.name == 'tag' && enterVisible.value) {
+        if (!getApp().TagManager.edit.checkNeedSaveTagVisi()) {
+            store.commit(`${module.value}/cancel`)
+            return
+        }
+    }
+    if (router.value.name == 'roam') {
+        if (!getApp().WalkManager.edit.checkNeedSave()) {
+            store.commit(`${module.value}/cancel`)
+            return
+        }
+    }
+
+    Dialog.confirm({
+        content: t('toast.saveTips'),
+        title: t('common.tips'),
+        okText: t('common.confirm'),
+        noText: t('common.cancel'),
+        func: state => {
+            if (state == 'ok') {
+                store.commit(`${module.value}/cancel`)
+            }
+        },
+    })
+}
+</script>
+<style lang="scss" scoped>
+.header-wrapper {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.title {
+    font-size: 16px;
+    white-space: nowrap;
+    display: flex;
+    align-items: center;
+    b {
+        display: inline-block;
+        width: 2px;
+        height: 14px;
+        margin: 0 10px;
+        background: rgba(255, 255, 255, 0.16);
+    }
+}
+.buttons {
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    button {
+        width: 105px;
+        margin-right: 10px;
+    }
+}
+</style>

+ 97 - 0
packages/app-cdfg/src/components/Header/Publish.vue

@@ -0,0 +1,97 @@
+<template>
+    <div x-name="header-publish" class="header-wrapper">
+        <span class="title">{{ title }}</span>
+        <ul class="opts">
+            <!-- <li><ui-icon tip="帮助中心" tipV="bottom" type="course1"></ui-icon></li> -->
+            <li @click="onPublish">
+                <ui-button type="primary">
+                    <i class="iconfont icon-publish"></i>
+                    <span>{{ $t('common.publish') }}</span>
+                </ui-button>
+            </li>
+        </ul>
+    </div>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import browser from '@/utils/browser'
+import { Loading, Dialog } from '@kankan/components'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const title = computed(() => store.getters['scene/metadata'].title)
+const disable = computed(() => store.getters.outsideDisable)
+const onPublish = () => {
+    Loading.show()
+    store
+        .dispatch('scene/publish')
+        .then(res => {
+            Loading.hide()
+            Dialog.confirm({
+                content: t('toast.goViewTips'),
+                title: t('common.tips'),
+                noText: t('common.notGo'),
+                okText: t('common.goNow'),
+                single: false,
+                func: state => {
+                    if (state == 'ok') {
+                        let domain = store.getters['scene/whichDept'] == 'HK' ? process.env.VUE_APP_SHOW_HK_URL : process.env.VUE_APP_SHOW_URL
+                        let link = domain + `index.html?m=${browser.getURLParam('m')}`
+                        window.open(link)
+                    }
+                }
+            })
+        })
+        .catch(error => {
+            Loading.hide()
+            if (error) {
+                console.error(error)
+                Dialog.alert(t('toast.publishFail'))
+            }
+        })
+}
+</script>
+<style lang="scss" scoped>
+.header-wrapper {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.title {
+    font-size: 16px;
+}
+.opts {
+    position: absolute;
+    right: 0;
+    top: 0;
+    height: 100%;
+    display: flex;
+    li {
+        cursor: pointer;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        padding: 0 10px;
+        margin-left: 5px;
+        color: var(--editor-font-color);
+        &:hover {
+            color: #fff;
+        }
+        i {
+            font-size: 18px;
+        }
+    }
+    button {
+        width: 80px;
+        height: 34px;
+        border-radius: 17px;
+        i {
+            font-size: 14px;
+            margin-right: 6px;
+        }
+    }
+}
+</style>

+ 14 - 0
packages/app-cdfg/src/components/Header/index.vue

@@ -0,0 +1,14 @@
+<template>
+    <ui-editor-head>
+        <HeaderEdit v-if="isEditModule" />
+        <HeaderPublish v-else />
+    </ui-editor-head>
+</template>
+<script setup>
+import HeaderEdit from './Edit'
+import HeaderPublish from './Publish'
+import { computed, ref } from 'vue'
+import { useStore } from 'vuex'
+const store = useStore()
+const isEditModule = computed(() => store.getters.editModule != void 0)
+</script>

+ 368 - 0
packages/app-cdfg/src/components/Information/Edit.vue

@@ -0,0 +1,368 @@
+<template>
+    <div class="title">
+        <div class="text" :class="{ 'is-edit': isEditTitle }">
+            <span>*</span>
+
+            <div>
+                <input type="text" ref="title$" :placeholder="$t('info.toolbox.inputTitle')" v-model="title" :maxlength="titleMax" @focus="onEditTitle" @blur="onEditTitle" />
+                <span>
+                    <label>{{ title.length }}</label> / {{ titleMax }}
+                </span>
+            </div>
+        </div>
+    </div>
+    <div class="description">
+        <div class="text" :class="{ 'is-edit': isEditDescription }">
+            <div :class="{ active: showBorder }">
+                <Editor @mousedown="showBorder = true" :html="description" :maxlength="descriptionMax" ref="editor$" @change="onDescriptionChange" @blur="onEditDescription" />
+                <div class="tool">
+                    <div class="link" @click="openEditLink">
+                        <ui-icon type="link" :tip="$t('tag.toolbox.addLink')" tipV="top" :class="{ active: isEditLink }"></ui-icon>
+                        <!-- <i class="iconfont icon-link" :class="{ active: isEditLink }"></i> -->
+                        <div class="link-content" @click.stop="" v-if="isEditLink">
+                            <div class="link-header">{{ $t('tag.toolbox.addLink') }}</div>
+                            <div class="link-msg">
+                                <div class="text-input">
+                                    <ui-input v-model.trim="text" maxlength="40" type="text" :placeholder="$t('tag.toolbox.linkTitleTips')" />
+                                </div>
+                                <div class="link-input">
+                                    <ui-input v-model.trim="link" type="text" :placeholder="$t('tag.toolbox.linkUrlTips')" />
+                                </div>
+                            </div>
+                            <div class="link-foot">
+                                <ui-button class="cancel" @click.stop="closeEditLink">{{ $t('common.cancel') }}</ui-button>
+                                <ui-button :class="{ disabled: text == '' || link == '' }" @click.stop="addLink" class="confirm" type="primary">{{ $t('common.add') }}</ui-button>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="tips">
+                        <span>{{ descriptionLength }}</span> / <label>{{ descriptionMax }}</label>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, nextTick, computed, watch, onMounted } from 'vue'
+import { useStore } from 'vuex'
+import Editor from '@/components/shared/Editor'
+import { Dialog } from '@kankan/components'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const title$ = ref(null)
+const editor$ = ref(null)
+
+const store = useStore()
+const titleMax = 30
+const descriptionMax = 200
+const descriptionLength = ref(0)
+const metadata = computed(() => store.getters['info/metadata'])
+const frist = ref(true)
+onMounted(() => {
+    frist.value = false
+})
+const title = computed({
+    get() {
+        return metadata.value.title || ''
+    },
+    set(title) {
+        title = title.trim()
+        store.commit('info/update', { title })
+    }
+})
+const description = computed({
+    get() {
+        // 编辑器这里需要特殊处理
+        let value = metadata.value.description
+        if (editor$.value && !editor$.value.init && value !== void 0) {
+            editor$.value.setHtml(value)
+            descriptionLength.value = editor$.value.getLength()
+        }
+
+        return value
+    },
+    set(description) {
+        store.commit('info/update', { description })
+    }
+})
+const text = ref('')
+const link = ref('')
+const isEditTitle = ref(false)
+const isEditDescription = ref(true)
+const isEditLink = ref(false)
+const showBorder = ref(false)
+
+const onEditTitle = () => {
+    isEditTitle.value = !isEditTitle.value
+}
+const onEditDescription = () => {
+    showBorder.value = false
+}
+const openEditLink = () => {
+    text.value = ''
+    link.value = ''
+    isEditLink.value = true
+    showBorder.value = true
+}
+const closeEditLink = () => {
+    text.value = ''
+    link.value = ''
+    isEditLink.value = false
+    showBorder.value = false
+}
+const addLink = () => {
+    if (!text.value) {
+        return Dialog.toast(t('tag.toolbox.linkTitleTips'))
+    }
+    if (!link.value) {
+        return Dialog.toast(t('tag.toolbox.linkUrlTips'))
+    }
+
+    editor$.value.insertLink(text.value, link.value)
+    setTimeout(() => {
+        store.commit('info/update', { description: editor$.value.getHtml() })
+    }, 400)
+    closeEditLink()
+}
+const onDescriptionChange = ({ length, html, paste }) => {
+    // if (init) {
+    //     description.value = html
+    //     return (descriptionLength.value = length)
+    // }
+
+    if (showBorder.value || paste) {
+        description.value = html
+    }
+    descriptionLength.value = length
+}
+store.subscribe((mutation, state) => {
+    if (editor$.value && mutation.type === 'info/cancel') {
+        editor$.value.init = false
+        editor$.value.setHtml(description.value)
+    }
+})
+</script>
+<style lang="scss" scoped>
+.title {
+    display: flex;
+    align-items: center;
+    // padding-right: 30px;
+    padding-right: 10px;
+    width: 100%;
+    height: 100%;
+    .text {
+        // width: 190px;
+        padding-left: 10px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        width: 400px;
+        display: flex;
+        align-items: center;
+        > span {
+            color: red;
+            margin-right: 2px;
+            margin-top: 4px;
+        }
+        div {
+            width: 100%;
+            // border: 1px solid transparent;
+            position: relative;
+            background: rgba(0, 0, 0, 0.2);
+            border-radius: 4px;
+            padding: 0 10px;
+            box-sizing: border-box;
+            border: 1px solid transparent;
+            span {
+                font-size: 12px;
+                position: absolute;
+                right: 10px;
+                top: 50%;
+                transform: translateY(-50%);
+            }
+            label {
+                color: var(--editor-main-color);
+            }
+        }
+        &.is-edit {
+            // width: 400px;
+            // padding-left: 9px;
+
+            > div {
+                position: relative;
+                border-radius: 4px;
+                border: 1px solid var(--editor-main-color);
+                box-sizing: border-box;
+            }
+            label {
+                color: var(--editor-main-color);
+            }
+        }
+
+        input {
+            color: rgba(255, 255, 255, 0.88);
+            // background: rgba(0, 0, 0, 0.2);
+            width: 100%;
+            height: 24px;
+            line-height: 19px;
+            // padding-left: 10px;
+            // padding-right: 60px;
+            border: none;
+        }
+    }
+
+    i {
+        transition: transform 0.2s ease-in-out;
+    }
+    &.pull-up {
+        i {
+            transform: rotate(180deg);
+        }
+    }
+}
+.description {
+    position: absolute;
+    left: 0;
+    top: calc(100% + 10px);
+    background: rgba(15, 15, 15, 0.3);
+    border-radius: 10px;
+    // padding-right: 30px;
+    padding-right: 10px;
+    .icon {
+        top: 10px !important;
+        bottom: auto !important;
+    }
+    > .text {
+        display: flex;
+        padding: 20px;
+        flex-direction: column;
+        width: 370px;
+        letter-spacing: 1px;
+        &.is-edit {
+            padding: 10px;
+            padding-right: 0px;
+            width: 400px;
+
+            > div {
+                display: flex;
+                flex-direction: column;
+                border-radius: 4px;
+                border: 1px solid transparent;
+                background: rgba(0, 0, 0, 0.2);
+                &.active {
+                    border: 1px solid var(--editor-main-color);
+                }
+            }
+
+            .shared-editor {
+                // padding: 10px;
+                margin: 10px;
+                line-height: 19px;
+                height: 150px;
+            }
+        }
+        .tool {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            height: 30px;
+            background: rgba(70, 43, 43, 0.16);
+            border-radius: 0px 0px 4px 4px;
+            .link {
+                margin-left: 10px;
+                cursor: pointer;
+                position: relative;
+                .iconfont {
+                    position: static;
+                    font-size: 16px;
+                    &.active {
+                        color: var(--editor-main-color);
+                        .tip {
+                            z-index: 100;
+                        }
+                    }
+                }
+                .link-content {
+                    position: absolute;
+                    left: -20px;
+                    top: 35px;
+                    width: 410px;
+                    height: auto;
+                    background: rgba(27, 27, 28, 0.8);
+                    // box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+                    border: 1px solid #000000;
+                    border-radius: 10px;
+                    cursor: auto;
+                    &::before {
+                        position: absolute;
+                        content: '';
+                        left: 20px;
+                        top: -10px;
+                        width: 0;
+                        height: 0;
+
+                        border-left: 6px solid transparent; // 根据三角形方向选择对应的boder-direction
+                        border-right: 6px solid transparent; // 根据三角形方向选择对应的boder-direction
+                        border-bottom: 10px solid rgba(27, 27, 28, 0.8); // 根据三角形方向选择对应的boder-direction
+                    }
+
+                    .link-header {
+                        padding: 20px;
+                        font-size: 16px;
+                        color: var(--editor-font-color);
+                        border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+                    }
+                    .link-msg {
+                        width: 100%;
+                        height: auto;
+                        padding: 40px 20px 46px;
+                        box-sizing: border-box;
+                        > div {
+                            height: 40px;
+                            width: 100%;
+                            // background: rgba(255, 255, 255, 0.1);
+                            margin-bottom: 30px;
+                            border-radius: 4px;
+                            &.link-input {
+                                margin-bottom: 0px;
+                            }
+
+                            .ui-input {
+                                font-size: 14px;
+                                height: 100%;
+                                width: 100%;
+                                box-sizing: border-box;
+                                input {
+                                    padding-right: 50px;
+                                }
+                            }
+                            .ui-input .text.suffix input {
+                                padding-right: 60px !important;
+                            }
+                        }
+                    }
+                    .link-foot {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        margin-bottom: 12px;
+                        > .ui-button {
+                            width: 105px;
+                            height: 34px;
+                            margin-right: 20px;
+                        }
+                    }
+                }
+            }
+            .tips {
+                margin-right: 10px;
+                span {
+                    color: var(--editor-main-color);
+                }
+            }
+        }
+    }
+}
+</style>

+ 187 - 0
packages/app-cdfg/src/components/Information/View.vue

@@ -0,0 +1,187 @@
+<template>
+    <div class="information">
+        <div class="title" @click="onShowDescription" :class="{ 'pull-up': showDescription, collapse: !showTitle }">
+            <div class="back-btn" @click.stop="showTitle = !showTitle">
+                <i class="iconfont icon-_back"></i>
+            </div>
+            <div class="text">
+                <div>{{ metadata.title }}</div>
+            </div>
+            <div class="icon">
+                <i class="iconfont icon-pull-down"></i>
+            </div>
+        </div>
+        <div v-if="showTitle" class="description" :class="{ show: showDescription }" @click="onShowDescription">
+            <div class="text" v-html="metadata.description"></div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+
+import { useStore } from 'vuex'
+const showTitle = ref(true)
+const store = useStore()
+const metadata = computed(() => store.getters['scene/metadata'])
+const showDescription = ref(false)
+
+const onShowDescription = () => {
+    showDescription.value = !showDescription.value
+}
+</script>
+
+<style lang="scss" scoped>
+.information {
+    position: absolute;
+    left: 0;
+    top: 20px;
+    height: 34px;
+    border-radius: 0 34px 34px 0;
+    background: rgba(0, 0, 0, 0.3);
+    pointer-events: all;
+    color: rgba(255, 255, 255, 0.88);
+    z-index: 100;
+    &.mobile {
+        position: absolute;
+        top: 0;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 100%;
+        background: none;
+        height: 0;
+        &.disabled {
+            // position: absolute;
+        }
+    }
+    :deep(.icon) {
+        cursor: pointer;
+        display: flex;
+        position: absolute;
+        right: 0;
+        top: 0;
+        bottom: 0;
+        width: 24px;
+        align-items: center;
+        justify-content: flex-start;
+        color: inherit;
+        i {
+            font-size: 14px;
+        }
+    }
+}
+.title {
+    display: flex;
+    align-items: center;
+    padding-right: 30px;
+    width: 100%;
+    height: 100%;
+    cursor: pointer;
+    transition: all 0.1s;
+    &.collapse {
+        // width: 34px;
+        padding-right: 0;
+        .back-btn {
+            .iconfont {
+                transform: rotate(180deg);
+            }
+            &::after {
+                display: none;
+            }
+        }
+        .text {
+            display: none;
+        }
+        .icon {
+            display: none;
+        }
+    }
+    .back-btn {
+        width: 34px;
+        height: 34px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+        position: relative;
+        transition: all 0.1s;
+        &::after {
+            content: '';
+            position: absolute;
+            width: 1px;
+            height: 65%;
+            background: linear-gradient(transparent, #fff, transparent);
+            top: 50%;
+            transform: translateY(-50%);
+            right: 0;
+        }
+    }
+    .text {
+        // width: 190px;
+        width: 240px;
+        padding-left: 20px;
+        transition: width 0.3s;
+        > div {
+            width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+    }
+    .icon {
+        i {
+            transition: transform 0.2s ease-in-out;
+        }
+    }
+
+    &.pull-up {
+        // .text {
+        //     width: 240px;
+        //     padding-left: 20px;
+
+        //     > div {
+        //         // width: 100%;
+        //         // overflow: hidden;
+        //         // text-overflow: ellipsis;
+        //         // white-space: nowrap;
+        //     }
+        // }
+        .icon {
+            i {
+                transform: rotate(180deg);
+            }
+        }
+    }
+}
+
+.description {
+    display: none;
+    position: absolute;
+    left: 10px;
+    top: calc(100% + 10px);
+    background: rgba(15, 15, 15, 0.3);
+    border-radius: 10px;
+    &.show {
+        display: block;
+    }
+    .text {
+        padding: 20px;
+        width: 400px;
+        letter-spacing: 1px;
+        overflow: hidden;
+        word-break: break-all;
+        white-space: normal;
+        line-height: 1.5;
+    }
+}
+[xui_min_map] {
+    right: 160px !important;
+}
+</style>
+
+<style>
+body [xui_min_map] {
+    top: 70px !important;
+    right: 60px !important;
+}
+</style>

+ 124 - 0
packages/app-cdfg/src/components/Information/editView.vue

@@ -0,0 +1,124 @@
+<template>
+    <div class="title" @click="onShowDescription" :class="{ 'pull-up': showDescription }">
+        <div class="text">
+            <div>{{ metadata.title }}</div>
+        </div>
+        <div class="icon">
+            <i class="iconfont icon-pull-down"></i>
+        </div>
+    </div>
+    <div class="description" :class="{ show: showDescription }" @click="onShowDescription">
+        <div class="text" v-html="metadata.description"></div>
+    </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+
+import { useStore } from 'vuex'
+
+const store = useStore()
+const metadata = computed(() => store.getters['scene/metadata'])
+// const showDescription = ref(false)
+const showDescription = computed(() => store.getters['scene/showDescription'])
+
+const onShowDescription = () => {
+    store.commit('scene/setScene', { showDescription: !showDescription.value })
+    // showDescription.value = !showDescription.value
+}
+</script>
+
+<style lang="scss" scoped>
+.title {
+    display: flex;
+    align-items: center;
+    padding-right: 30px;
+    width: 100%;
+    height: 100%;
+    .text {
+        // width: 190px;
+        width: 240px;
+        padding-left: 20px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        div {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+    }
+
+    i {
+        transition: transform 0.2s ease-in-out;
+    }
+    &.pull-up {
+        i {
+            transform: rotate(180deg);
+        }
+    }
+}
+
+.description {
+    display: none;
+    position: absolute;
+    left: 0;
+    top: calc(100% + 10px);
+    background: rgba(15, 15, 15, 0.3);
+    border-radius: 10px;
+    &.show {
+        display: block;
+    }
+    .text {
+        padding: 20px;
+        width: 400px;
+        letter-spacing: 1px;
+        overflow: hidden;
+        word-break: break-all;
+        white-space: normal;
+        line-height: 1.5;
+    }
+}
+[is-mobile] {
+    .title {
+        display: flex;
+
+        align-items: center;
+        padding-right: 0.4rem;
+        width: 100%;
+        height: 100%;
+        .text {
+            width: 4.2667rem;
+            padding-left: 0.2667rem;
+            margin-right: 0.2667rem;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            text-align: center;
+        }
+
+        i {
+            transition: transform 0.2s ease-in-out;
+            font-size: 0.2667rem;
+        }
+        &.pull-up {
+            i {
+                transform: rotate(180deg);
+            }
+        }
+    }
+    .description {
+        left: 50%;
+        transform: translateX(-50%);
+        .text {
+            padding: 0.2667rem;
+            width: 8rem;
+            letter-spacing: 1px;
+            overflow: hidden;
+            word-break: break-all;
+            white-space: normal;
+            line-height: 1.5;
+        }
+    }
+}
+</style>

+ 81 - 0
packages/app-cdfg/src/components/Information/index.vue

@@ -0,0 +1,81 @@
+<template>
+    <div></div>
+    <div class="information" :class="{ hidden: isHidden, 'edit-mode': isEdit, disable: isPlay }">
+        <Edit v-if="isEdit" />
+        <View v-else />
+    </div>
+</template>
+
+<script setup>
+// import { useRoute } from 'vue-router'
+import { ref, computed, watchEffect } from 'vue'
+import { useApp, getApp, getNum } from '@/app'
+import { useStore } from 'vuex'
+import View from './editView.vue'
+import Edit from './Edit.vue'
+const store = useStore()
+// const route = useRoute()
+const isEdit = ref(false)
+const isHidden = computed(() => store.getters.sceneUI == false)
+const isPlay = computed(() => store.getters['tour/isPlay'])
+
+watchEffect(() => {
+    isEdit.value = store.getters.router.name === 'info'
+})
+</script>
+
+<style lang="scss" scoped>
+.information {
+    position: absolute;
+    left: calc(var(--editor-menu-width) + 20px);
+    top: 20px;
+    height: 34px;
+    border-radius: 34px;
+    background: rgba(15, 15, 15, 0.3);
+    pointer-events: all;
+    color: rgba(255, 255, 255, 0.88);
+    :deep(.icon) {
+        cursor: pointer;
+        display: flex;
+        position: absolute;
+        right: 0;
+        top: 0;
+        bottom: 0;
+        width: 24px;
+        align-items: center;
+        justify-content: flex-start;
+        color: inherit;
+        i {
+            font-size: 14px;
+        }
+    }
+}
+[is-mobile] {
+    .information {
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        top: 0.32rem;
+        height: 1.0667rem;
+        border-radius: 0.5333rem;
+        background: rgba(15, 15, 15, 0.3);
+        pointer-events: all;
+        color: rgba(255, 255, 255, 0.88);
+        :deep(.icon) {
+            cursor: pointer;
+            display: flex;
+            position: absolute;
+            right: 0;
+            top: 0;
+            bottom: 0;
+            width: 0.64rem;
+            align-items: center;
+            justify-content: flex-start;
+            color: inherit;
+            // i {
+            //     font-size: 0.1867rem;
+            // }
+        }
+    }
+}
+</style>

+ 34 - 0
packages/app-cdfg/src/components/Menuer/index.vue

@@ -0,0 +1,34 @@
+<template>
+    <ui-editor-menu :name="name" :menu="menu" @menu-click="onMenuClick" v-show="!full && !module"></ui-editor-menu>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { useStore } from 'vuex'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const router = useRouter()
+const route = useRoute()
+const name = computed(() => route.name)
+const menu = computed(() => {
+    let menu = store.getters.menu
+    menu.map(item => {
+        item.title = t(`menu.${item.name}`)
+    })
+    return menu
+})
+const full = computed(() => {
+    if (store.getters.sceneFull) {
+        return true
+    }
+    if (store.getters.toolbar && store.getters.toolbar.show && store.getters.toolbx && store.getters.toolbx.show) {
+        return true
+    }
+})
+const module = computed(() => store.getters.editModule)
+
+const onMenuClick = name => {
+    router.push({ name })
+}
+</script>

+ 24 - 0
packages/app-cdfg/src/components/StorageToast/index.js

@@ -0,0 +1,24 @@
+import { Dialog } from '@kankan/components'
+const StorageToast = function (options) {
+    let storage = JSON.parse(localStorage.getItem('storageToast')) || null
+    if (storage && storage[options.moudle]) {
+        return null
+    } else {
+        return Dialog.toast({
+            type: 'fixed',
+            content: options.content,
+            showClose: true,
+            close: function () {
+                if (storage) {
+                    storage[options.moudle] = true
+                    localStorage.setItem('storageToast', JSON.stringify(storage))
+                } else {
+                    let data = {}
+                    data[options.moudle] = true
+                    localStorage.setItem('storageToast', JSON.stringify(data))
+                }
+            },
+        })
+    }
+}
+export default StorageToast

+ 97 - 0
packages/app-cdfg/src/components/Tags/constant.js

@@ -0,0 +1,97 @@
+export const custom = {
+    image: {
+        icon: 'pic',
+        upload: true,
+        uploadPlace: `上傳圖片`,
+        accept: `.jpg,.png`,
+        multiple: true,
+        name: '圖片',
+        maxSize: 5 * 1024 * 1024,
+        maxNum: 9,
+        othPlaceholder: '支持JPG、PNG圖片格式,單張不超過5MB,最多支持上傳9張。'
+    },
+    video: {
+        icon: 'video',
+        upload: true,
+        uploadPlace: `上傳視頻`,
+        accept: `.mp4, .mov`,
+        multiple: false,
+        name: '視頻',
+        maxSize: 20 * 1024 * 1024,
+        othPlaceholder: '支持MP4、MOV視頻格式,碼率小於2Mbps,不超過20MB'
+    },
+    audio: {
+        icon: 'music',
+        upload: true,
+        uploadPlace: `上傳音頻`,
+        accept: '.mp3, .wav',
+        multiple: false,
+        name: '音頻',
+        maxSize: 5 * 1024 * 1024,
+        othPlaceholder: '支持MP3、WAV格式,不超過5MB'
+    },
+    link: {
+        icon: 'web',
+        name: '鏈接'
+    }
+}
+
+export const customized = {
+    commodity: {
+        name: '商品',
+        hotType: 0,
+        productsError: '商品關聯不能為空'
+    },
+    coupon: {
+        name: '尋寶遊戲',
+        hotType: 1,
+        upload: true,
+        uploadPlace: `上傳圖片`,
+        accept: `.jpg,.png`,
+        multiple: true,
+        maxSize: 5 * 1024 * 1024,
+        maxNum: 9,
+        othPlaceholder: '支持JPG、PNG圖片格式,單張不超過5MB,最多支持上傳9張。',
+        couponLinkError: '專題ID不能為空',
+        inspectionField: ['couponLink']
+    },
+    applet_link: {
+        hotType: 2,
+        name: '第三方跳轉',
+        upload: true,
+        uploadPlace: `上傳圖片`,
+        accept: `.jpg,.png`,
+        multiple: false,
+        maxSize: 5 * 1024 * 1024,
+        maxNum: 1,
+        othPlaceholder: '支持JPG、PNG圖片格式,單張不超過5MB,最多支持上傳9張。',
+        liveLinkError: '專題ID不能為空',
+        liveIconError: '直播頭像不能為空',
+        subField: 'src',
+        inspectionField: ['liveIcon', 'liveLink']
+    },
+    waterfall: {
+        hotType: 3,
+        name: '瀑布流',
+        productsError: '商品關聯不能為空'
+    },
+    link_scene: {
+        hotType: 4,
+        name: '場景關聯',
+        sceneFirstViewError: '場景關聯不能為空',
+        subField: 'num',
+        inspectionField: ['sceneFirstView']
+    },
+    exhibits: {
+        hotType: 5,
+        name: '陈列品',
+        productsError: '商品關聯不能為空'
+    },
+    point_jump: {
+        name: '點位跳轉',
+        hotType: 6,
+        othPlaceholder: '請選擇跳轉的點位',
+        videoIdError: '跳轉的點位不能為空',
+        inspectionField: ['videoId']
+    }
+}

+ 400 - 0
packages/app-cdfg/src/components/Tags/goods-list.vue

@@ -0,0 +1,400 @@
+<!--  -->
+<template>
+    <teleport to="body">
+        <div class="goods-layer">
+            <div class="goods-info">
+                <div class="goods-header">
+                    <span>選擇商品</span>
+                    <ui-icon @click.stop="close" class="close-btn" type="close"></ui-icon>
+                </div>
+
+                <div class="goods-con">
+                    <div class="g-search">
+                        <ui-input type="text" width="260px" placeholder="輸入關鍵字" v-model="searchKey">
+                            <template v-slot:preIcon>
+                                <ui-icon type="search" class="icon" />
+                            </template>
+                        </ui-input>
+                        <ui-input
+                            :class="{ disabled: !canChangeOption && hotData.products.length > 0 }"
+                            v-if="isHongKong"
+                            @click.stop
+                            type="select"
+                            @update:modelValue="onSelect"
+                            :options="productSourceOptions"
+                            width="160px"
+                            v-model="productSource"
+                            placeholder="選擇店鋪"
+                        >
+                            <template v-slot:option="{ raw }">
+                                <span>{{ raw.label }}</span>
+                            </template>
+                        </ui-input>
+                    </div>
+                    <div
+                        v-show="showListPanel"
+                        class="table-layer"
+                        v-infinite-scroll="getData"
+                        infinite-scroll-disabled="disabled_x"
+                        :infinite-scroll-throttle-delay="500"
+                        :infinite-scroll-immediate-check="false"
+                        infinite-scroll-distance="30"
+                    >
+                        <table class="list">
+                            <thead>
+                                <tr>
+                                    <th width="10px">
+                                        <div class="checkbox">
+                                            <input type="checkbox" v-model="allSelect" @click="e => selectGoodAll(e.target.checked)" />
+                                            <span></span>
+                                        </div>
+                                    </th>
+                                    <th>序號</th>
+                                    <th>商品編號</th>
+                                    <th width="100px">商品圖片</th>
+                                    <th :width="isHongKong ? '300px' : '100px'">商品名稱</th>
+                                    <th v-if="!isHongKong">價格</th>
+                                    <th v-if="!isHongKong">庫存</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                <tr v-for="(item, i) in list" :key="i">
+                                    <td width="10px">
+                                        <div class="checkbox" :class="{ disabled: item.isBind }">
+                                            <input type="checkbox" :disabled="item.isBind" @change="e => selectGood(item, e.target.checked)" :checked="item.isBind || item.isCheck" />
+                                            <span></span>
+                                        </div>
+                                    </td>
+                                    <td>{{ i + 1 }}</td>
+                                    <td>{{ item.id }}</td>
+                                    <td><img style="width: 100px" :src="item.pic" alt="" /></td>
+                                    <td>{{ item.name }}</td>
+                                    <td v-if="!isHongKong">{{ item.symbol }} {{ item.price }}</td>
+                                    <td v-if="!isHongKong">{{ item.stock }}</td>
+                                </tr>
+                                <tr class="nodata" v-if="list.length === 0">
+                                    <td colspan="7">暫無數據</td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+
+                <div class="goods-footer">
+                    <ui-button @click.stop="close" type="cancel">取消</ui-button>
+                    <ui-button @click.stop="submit" type="primary">保存</ui-button>
+                </div>
+            </div>
+        </div>
+    </teleport>
+</template>
+
+<script setup>
+import { reactive, defineEmits, onBeforeMount, onMounted, ref, watchEffect, computed, watch, nextTick } from 'vue'
+import { Dialog, Loading } from '@kankan/components'
+import metasImage from './metas/metas-image'
+import common from '@/utils/common'
+
+import { useStore } from 'vuex'
+const emit = defineEmits(['close'])
+
+const store = useStore()
+const isHongKong = computed(() => store.getters['scene/whichDept'] == 'HK')
+const hotData = computed(() => store.getters['tag/hotData'])
+
+console.log(hotData.value)
+
+const productSourceOptions = computed(() => {
+    let list = store.getters['tag/HKProductSourceList']
+    list.map(i => {
+        i.value = i.id
+        i.label = i.cdfName
+    })
+    return list
+})
+
+const list = computed(() => {
+    return store.getters['tag/goodsList'] || []
+})
+
+const requestLock = computed(() => store.getters['tag/requestLock'])
+const auth = computed(() => store.getters['scene/auth'])
+
+const productSource = ref(null)
+const searchKey = ref('')
+const allSelect = ref(false)
+const canChangeOption = ref(true)
+
+const select = ref([])
+const showListPanel = ref(false)
+
+const selectGoodAll = isSelect => {
+    list.value.forEach(item => {
+        selectGood(item, isSelect)
+    })
+}
+
+const selectGood = (item, isSelect) => {
+    item.isCheck = isSelect
+    let index = select.value.findIndex(i => i.id == item.id)
+    if (isSelect) {
+        !~index && select.value.push(item)
+    } else {
+        ~index && select.value.splice(index, 1)
+    }
+}
+
+const close = () => {
+    emit('close')
+}
+const onSelect = data => {
+    productSource.value = data
+}
+
+const getData = common.debounce(
+    (reset = false) => {
+        store.dispatch('tag/getGoodsList', {
+            keyword: searchKey.value,
+            pageNum: reset ? 1 : Math.floor(list.value.length / 20) + 1,
+            reset,
+            productSourceId: productSource.value
+        })
+        if (!showListPanel.value) {
+            showListPanel.value = true
+        }
+    },
+    700,
+    false
+)
+
+const getGoodsFrom = async () => {
+    console.log(1111)
+    await store.dispatch('tag/getGoodsFrom', {})
+
+    if (hotData.value?.productSourceId) {
+        productSource.value = hotData.value?.productSourceId
+        canChangeOption.value = false
+    } else {
+        productSource.value = productSourceOptions.value[0].id
+        hotData.value.productSourceId = productSourceOptions.value[0].id
+    }
+
+    console.log(productSource.value, 'productSource.value')
+}
+
+const submit = () => {
+    store.commit('tag/pushGoodsItems', select)
+    close()
+}
+
+watch(
+    () => productSource.value,
+    (newval, old) => {
+        if (productSource.value) {
+            selectGoodAll(false)
+            getData(true)
+            hotData.value.productSourceId = productSource.value
+        }
+    }
+)
+
+watch(
+    () => searchKey.value,
+    (newval, old) => {
+        getData(true)
+    }
+)
+
+onMounted(() => {
+    if (isHongKong.value) {
+        nextTick(() => {
+            //重置下选择
+            selectGoodAll(false)
+            getGoodsFrom()
+        })
+    } else {
+        showListPanel.value = true
+        getData(true)
+        nextTick(() => {
+            //重置下选择
+            selectGoodAll(false)
+        })
+    }
+})
+</script>
+<style lang="scss">
+.goods-layer {
+    width: 100vw;
+    height: 100vh;
+    z-index: 100000;
+    top: 0;
+    position: fixed;
+    left: 0;
+    // padding: calc(var(--editor-head-height) + 20px) calc(var(--editor-toolbox-width) + 20px) 60px calc(var(--editor-menu-width) + 20px);
+    // background-color: rgba(255, 255, 255, 0.7);
+    z-index: 999;
+    .goods-info {
+        color: #fff;
+        width: 800px;
+        height: 760px;
+        margin: 5% auto;
+        user-select: none;
+        filter: var(--editor-menu-filter);
+        background-color: var(--editor-menu-back);
+        backdrop-filter: blur(4px);
+        border: solid 1px #000;
+        border-radius: 4px;
+        overflow: hidden;
+        &::after {
+            position: absolute;
+            top: 0;
+            left: 0;
+            content: '';
+            width: 100%;
+            height: 100%;
+            z-index: -1;
+            border: solid 1px rgba(255, 255, 255, 0.16);
+        }
+
+        .goods-header {
+            width: 100%;
+            padding: 22px 20px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+            .close-btn {
+                font-size: 16px;
+                cursor: pointer;
+            }
+        }
+
+        .goods-con {
+            padding: 20px;
+            height: calc(100% - 142px);
+            .g-search {
+            }
+
+            .table-layer {
+                overflow-y: auto;
+                height: calc(100% - 54px);
+                flex: 1;
+                margin-top: 20px;
+                background: rgba(176, 176, 176, 0.16);
+                border: 1px solid rgba(255, 255, 255, 0.1);
+                .list {
+                    border-collapse: collapse;
+                    width: 100%;
+                }
+
+                .list td,
+                .list th {
+                    font-size: 14px;
+                    line-height: 20px;
+                    text-align: left;
+                    padding: 15px;
+                    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+                    &:not(&:first-of-type) {
+                        // min-width: 90px;
+                    }
+                }
+
+                .list th {
+                    color: rgba(255, 255, 255, 0.6);
+                    font-weight: normal;
+                    background: rgba(27, 27, 28, 0.8);
+                }
+
+                .nodata td,
+                .nodata th {
+                    text-align: center;
+                }
+
+                .list-info {
+                    height: 28px;
+                    padding-left: 33px;
+                    line-height: 28px;
+                    position: relative;
+                    text-align: left;
+                }
+
+                .list-info > img {
+                    position: absolute;
+                    left: 0;
+                    top: 0;
+                    width: 28px;
+                    height: 28px;
+                }
+            }
+        }
+
+        .goods-footer {
+            width: 100%;
+            padding: 22px 20px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            > button {
+                width: 105px;
+                margin: 0 10px;
+            }
+        }
+    }
+
+    .checkbox {
+        position: relative;
+        width: 14px;
+        height: 14px;
+        border-radius: 2px;
+        &.disabled {
+            opacity: 0.3;
+            > input {
+                cursor: default;
+            }
+        }
+    }
+
+    .checkbox > input,
+    .checkbox > span {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        left: 0;
+        top: 0;
+        border: 1px solid rgba(176, 176, 176, 0.5);
+    }
+
+    .checkbox > input {
+        z-index: 1;
+        opacity: 0;
+        cursor: pointer;
+    }
+
+    .checkbox > span {
+        z-index: 2;
+        pointer-events: none;
+        border-radius: 2px;
+    }
+
+    .checkbox > input:checked + span {
+        background: #00c8af;
+        border: 1px solid #00c8af;
+        &::after {
+            position: absolute;
+            display: table;
+            border: 2px solid #fff;
+            border-top: 0;
+            border-left: 0;
+            -webkit-transform: rotate(45deg) scale(0.6) translate(-50%, -50%);
+            transform: rotate(45deg) scale(0.6) translate(-50%, -50%);
+            opacity: 1;
+            -webkit-transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
+            transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
+            content: ' ';
+            width: 9px;
+            height: 16px;
+            top: 20%;
+        }
+    }
+}
+</style>

+ 229 - 0
packages/app-cdfg/src/components/Tags/index.vue

@@ -0,0 +1,229 @@
+<template>
+    <teleport :to="tags$" v-if="tags$">
+        <template :key="index" v-for="(tag, index) in tags">
+            <div @mouseenter="onMouseEnter(tag, index)" @mouseleave="onMouseLeave($event, tag)" :style="{ left: tag.x + 'px', top: tag.y + 'px' }" :class="{ visible: tag.visible }">
+                <span @click.stop="goTag(tag, index)" class="point zoom" :style="{ 'background-image': 'url(' + getUrl(tag.icon) + ')' }"></span>
+                <!-- <div class="content"> -->
+                <div class="content">
+                    <div class="trans" :class="{ active: (isFixed && tag.sid == hotData.sid) || showInfo }">
+                        <template v-if="hotData && tag.sid == hotData.sid && !showMsg">
+                            <div class="arrow" :id="`arrow_${tag.sid}`"></div>
+                            <TagInfo v-if="isEdit && hotData" />
+                            <ShowTag @click.stop="" v-if="!isEdit && hotData" @open="openInfo" />
+                        </template>
+                    </div>
+                </div>
+                <TagView @click.stop="" v-if="showMsg && toggleIndex == index" @close="closeInfo" />
+            </div>
+        </template>
+    </teleport>
+    <GoodsList v-if="isShowGoodList" @close="closeGoodsList" />
+    <SceneList v-if="isShowSceneList" @close="closeSceneList" />
+</template>
+<script setup>
+import { ref, onMounted, computed, watch, watchEffect, onActivated, onDeactivated, getCurrentInstance, nextTick } from 'vue'
+import { getApp, useApp } from '@/app'
+import { useStore } from 'vuex'
+import common from '../../utils/common'
+import TagView from './tag-view.vue'
+import GoodsList from './goods-list.vue'
+import SceneList from './scene-list.vue'
+
+import TagInfo from './tag-info.vue'
+import ShowTag from './show-tag.vue'
+import { useRoute } from 'vue-router'
+import { useMusicPlayer } from '@/utils/sound'
+const musicPlayer = useMusicPlayer()
+// const route = useRoute()
+let init = true
+const hotData = computed(() => {
+    let data = store.getters['tag/hotData']
+    if (!data) {
+        // musicPlayer.play()
+        // musicPlayer.pause(true)
+        // if (!getApp().Scene.isCurrentPanoHasVideo) {
+        //     console.log('resume')
+        //     console.log(init)
+        //     musicPlayer.resume()
+        // }
+    }
+    return data
+})
+const isPlay = computed(() => store.getters['tour/isPlay'])
+const router = computed(() => store.getters['router'])
+const flying = computed(() => store.getters['flying'])
+const leaveId = computed(() => store.getters['tag/leaveId'])
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const isShowGoodList = computed(() => store.getters['tag/isShowGoodList'])
+const isShowSceneList = computed(() => store.getters['tag/isShowSceneList'])
+
+const isFixed = computed(() => store.getters['tag/isFixed'])
+const enterVisible = computed(() => store.getters['tag/enterVisible'])
+const editPosition = computed(() => store.getters['tag/editPosition'])
+const toggleIndex = computed(() => store.getters['tag/toggleIndex'])
+const isClick = computed(() => store.getters['tag/isClick'])
+const editModule = computed(() => store.getters['editModule'])
+const positionInfo = computed(() => store.getters['tag/positionInfo'])
+const store = useStore()
+const tags$ = ref(null)
+const tags = computed(() => {
+    return store.getters['tag/tags'] || []
+})
+
+watch(
+    () => router.value.name,
+    (val, old) => {
+        // console.log(val)
+        if (val !== 'tag') {
+            store.commit('tag/setFixed', false)
+        }
+    }
+)
+
+const showInfo = ref(false)
+const showMsg = ref(false)
+
+// const toggleIndex = ref(null)
+const openInfo = () => {
+    showMsg.value = true
+    store.commit('tag/setFixed', false)
+    showInfo.value = false
+}
+const closeInfo = () => {
+    showMsg.value = false
+    if (isClick.value) {
+        //只有点击定位的才恢复显示
+        store.commit('tag/show', toggleIndex.value)
+        store.commit('tag/setFixed', true)
+        // showInfo.value = true
+        showInfo.value = false
+    }
+    store.commit('tag/setClick', false)
+}
+
+const closeGoodsList = () => {
+    store.commit('tag/setShowGoodsList', false)
+}
+
+const closeSceneList = () => {
+    store.commit('tag/setShowSceneList', false)
+}
+
+const closeTag = async () => {
+    const app = getApp()
+    const player = await app.TourManager.player
+    //关闭热点面板时候,继续播放之前暂停的音频
+    if (!app.Scene.isCurrentPanoHasVideo && !player.isPlaying) {
+        if (hotData.value.type == 'audio' || hotData.value.type == 'video') {
+            // console.log('resume')
+            musicPlayer.resume()
+        }
+    }
+
+    store.commit('tag/setFixed', false)
+    store.commit('tag/closeTag')
+    showInfo.value = false
+}
+const goTag = async (item, index) => {
+    let player = await getApp().TourManager.player
+    if (isPlay.value) {
+        player.pause()
+        store.commit('tour/setData', { isPlay: false })
+    }
+    if (flying.value) {
+        return
+    }
+    if (isFixed.value && !isEdit.value && hotData.value.sid == item.sid && !positionInfo.value) {
+        closeTag()
+    } else {
+        if (!enterVisible.value && !editPosition.value) {
+            if (!isEdit.value && !positionInfo.value) {
+                store.commit('tag/show', index)
+                store.commit('tag/setFixed', true)
+                showInfo.value = true
+                store.commit('tag/setToggleIndex', index)
+                store.commit('tag/gotoTag', item)
+            }
+            store.commit('tag/setClick', true)
+        } else {
+            //热点可视操作
+            getApp().TagManager.edit.setTagVisi(item.sid)
+        }
+    }
+}
+
+onMounted(async () => {
+    const app = await useApp()
+    await app.TagManager.tag()
+    init = false
+
+    tags$.value = '[xui_tags]'
+    app.TagManager.updatePosition(tags.value)
+    if (app.config.mobile) {
+        nextTick(() => {
+            let player = document.querySelector('.player')
+            player.addEventListener('touchstart', onClickHandler)
+        })
+    } else {
+        window.addEventListener('click', onClickHandler)
+    }
+})
+const getUrl = icon => {
+    let url = icon == '' || !icon ? getApp().resource.getAppURL('images/tag_icon_default.svg') : icon
+
+    return common.changeUrl(url)
+}
+const onMouseEnter = (tag, index) => {
+    if (!getApp().config.mobile) {
+        if (flying.value || isPlay.value) {
+            return
+        }
+        if (!enterVisible.value && !editPosition.value && !isEdit.value && !positionInfo.value) {
+            // console.log('onMouseEnter')
+
+            showInfo.value = true
+            store.commit('tag/show', index)
+
+            store.commit('tag/setToggleIndex', index)
+            if (leaveId.value != tag.sid) {
+                //聚焦后 移到其他热点取消fixed
+                store.commit('tag/setFixed', false)
+            }
+        }
+    }
+}
+
+const onMouseLeave = (event, tag) => {
+    if (!getApp().config.mobile) {
+        if (flying.value) {
+            return
+        }
+        if (event.relatedTarget != null) {
+            // if (!isEdit.value) {
+            showInfo.value = false
+            // }
+            store.commit('tag/setLeaveId', tag.sid)
+            if (!enterVisible.value && !isFixed.value && !showMsg.value && !editPosition.value && !positionInfo.value) {
+                closeTag()
+            }
+        }
+    }
+}
+
+const onClickHandler = () => {
+    // if (flying.value) {
+    //     return
+    // }
+
+    if (!isEdit.value && !positionInfo.value && isFixed.value) {
+        closeTag()
+        store.commit('tag/setClick', false)
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+[xui_tags] .content .trans {
+    min-width: 525px;
+}
+</style>

+ 132 - 0
packages/app-cdfg/src/components/Tags/link-manage.vue

@@ -0,0 +1,132 @@
+<!--  -->
+<template>
+    <div class="link-manage">
+        <div class="header">
+            <div class="header-title">{{ $t('tag.toolbox.addLink') }}</div>
+        </div>
+        <div class="link-content">
+            <ui-input width="100%" @input="onInput('text')" :placeholder="$t('tag.toolbox.linkTitleTips')" type="text" v-model="text" require :error="error.text" :maxlength="40" />
+            <ui-input width="100%" type="text" v-model="link" @input="onInput('link')" :error="error.link" require :placeholder="$t('tag.toolbox.linkUrlTips')" />
+        </div>
+        <div class="btn">
+            <ui-button class="cancel" @click="emit('close')">{{ $t('common.cancel') }}</ui-button>
+            <ui-button type="primary" @click="confirm">{{ $t('common.add') }}</ui-button>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, defineEmits, defineProps, ref } from 'vue'
+import { useI18n } from '../../i18n'
+const { t } = useI18n({ useScope: 'global' })
+// 注册事件
+const emit = defineEmits(['close', 'confirm'])
+const text = ref('')
+const link = ref('')
+const error = ref({
+    text: '',
+    link: '',
+})
+const onInput = type => {
+    if (type == 'text') {
+        if (text.value == '') {
+            error.value.text = `${t('tag.toolbox.linkTitleTips')}`
+        } else {
+            error.value.text = ''
+        }
+    } else if (type == 'link') {
+        if (link.value == '') {
+            error.value.link = `${t('tag.toolbox.linkUrlTips')}`
+        } else {
+            error.value.link = ''
+        }
+    }
+}
+
+const confirm = () => {
+    if (text.value == '') {
+        error.value.text = `${t('tag.toolbox.linkTitleTips')}`
+    } else {
+        error.value.text = ''
+    }
+    if (link.value == '') {
+        error.value.link = `${t('tag.toolbox.linkUrlTips')}`
+    } else {
+        error.value.link = ''
+    }
+
+    if (text.value == '' || link.value == '') {
+        return
+    }
+    emit('confirm', { text, link })
+}
+</script>
+<style lang="scss" scoped>
+.link-manage {
+    position: absolute;
+    width: 400px;
+    // height: 310px;
+    background: rgba(27, 27, 28, 0.9);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+    border: 1px solid #000000;
+    backdrop-filter: blur(4px);
+    left: -22px;
+    top: 100%;
+    margin-top: 5px;
+    z-index: 100;
+    &::after {
+        position: absolute;
+        content: '';
+        left: 29px;
+        top: -10px;
+        width: 0;
+        height: 0;
+        border-left: 6px solid transparent;
+        border-right: 6px solid transparent;
+        border-bottom: 10px solid rgba(27, 27, 28, 0.8);
+    }
+    .link-content {
+        border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+        padding: 40px 20px;
+        .ui-input {
+            margin-bottom: 30px;
+            &:last-of-type {
+                margin-bottom: 0;
+            }
+        }
+    }
+    .header {
+        height: 60px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 20px;
+        box-sizing: border-box;
+        color: #999;
+        border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+        .header-title {
+            font-size: 16px;
+            font-weight: bold;
+        }
+    }
+    .btn {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 0 85px;
+        height: 60px;
+        .cancel {
+            color: #00c8af;
+            border: 1px solid #00c8af;
+            &:hover {
+                opacity: 0.9;
+            }
+        }
+        button {
+            &:first-of-type {
+                margin-right: 10px;
+            }
+        }
+    }
+}
+</style>

+ 100 - 0
packages/app-cdfg/src/components/Tags/metas-upload-customized.vue

@@ -0,0 +1,100 @@
+<!--  -->
+<template>
+    <div class="metas-upload-customized">
+        <metasCommodity v-if="type == 'commodity'" />
+        <metasCoupon v-if="type == 'coupon'" />
+        <metasAppletLink v-if="type == 'applet_link'" />
+        <metasWaterfall v-if="type == 'waterfall'" />
+        <metasExhibits v-if="type == 'exhibits'" />
+        <metasLinkScene v-if="type == 'link_scene'" />
+        <metasPointJump v-if="type == 'point_jump'" />
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, ref, defineProps, computed, watch } from 'vue'
+import { useStore } from 'vuex'
+import { Loading, Dialog } from '@kankan/components'
+import { customized } from './constant.js'
+import metasCommodity from './metas/metas-commodity'
+import metasCoupon from './metas/metas-coupon'
+import metasAppletLink from './metas/metas-appletLink'
+import metasWaterfall from './metas/metas-waterfall'
+import metasLinkScene from './metas/metas-linkScene'
+import metasExhibits from './metas/metas-exhibits'
+import metasPointJump from './metas/metas-pointJump'
+
+import { useI18n } from '../../i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const imageNum = ref(0)
+const props = defineProps({
+    type: {
+        type: String,
+        default: null
+    }
+})
+const hotData = computed(() => store.getters['tag/hotData'])
+const imageList = computed(() => {
+    return hotData.value.media.image || []
+})
+const videoSrc = computed(() => {
+    return hotData.value.media.video || []
+})
+const audioInfo = computed(() => {
+    return hotData.value.media.audio || []
+})
+
+const hanlderFiles = data => {
+    switch (props.type) {
+        case 'image':
+            // store.commit('tag/setImageList', data[0])
+            setImageList(data[0])
+            break
+        case 'video':
+            store.commit('tag/setVideo', data)
+            break
+        case 'audio':
+            store.commit('tag/setAudio', data)
+            break
+    }
+}
+const setImageList = data => {
+    let picLength = 0
+    let list = JSON.parse(JSON.stringify(imageList.value))
+    if (list.length > 0) {
+        picLength = list.length
+    }
+    for (let i = 0; i < data.length; i++) {
+        if (list.length < customized['image'].maxNum) {
+            list.push('')
+            var index = i + picLength
+            list[index] = { src: URL.createObjectURL(data[i]), file: data[i] }
+        } else {
+            Dialog.toast({
+                type: 'error',
+                content: `${t('limit.maxLengthFile', { length: customized['image'].maxNum })}`
+            })
+            break
+        }
+    }
+    store.commit('tag/setImageList', list)
+}
+</script>
+<style lang="scss" scoped>
+.hide {
+    display: none;
+}
+.metas-upload-customized {
+    width: 100%;
+    min-height: 65px;
+    position: relative;
+    .ui-input {
+        width: 80px;
+        height: 80px;
+        border: 1px solid rgba(255, 255, 255, 0.2);
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+    }
+}
+</style>

+ 106 - 0
packages/app-cdfg/src/components/Tags/metas-upload.vue

@@ -0,0 +1,106 @@
+<!--  -->
+<template>
+    <div class="metas-upload">
+        <ui-input
+            type="file"
+            v-if="type != 'link'"
+            v-show="hotData.media[type].length <= 0"
+            :class="{ hide: audioInfo.length > 0 && type == 'audio' }"
+            :placeholder="custom[type].uploadPlace"
+            :disable="custom[type].upload"
+            :scale="custom[type].scale"
+            :accept="custom[type].accept"
+            :multiple="custom[type].multiple"
+            :maxSize="custom[type].maxSize"
+            :maxLen="custom[type].maxNum"
+            :othPlaceholder="custom[type].othPlaceholder"
+            @update:modelValue="data => hanlderFiles(data)"
+        >
+        </ui-input>
+        <metasWeb v-else />
+        <metasImage v-if="imageList.length > 0 && type == 'image'" />
+        <metasVideo v-if="videoSrc.length > 0 && type == 'video'" />
+        <metasAudio v-if="audioInfo.length > 0 && type == 'audio'" />
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, ref, defineProps, computed, watch } from 'vue'
+import { useStore } from 'vuex'
+import { Loading, Dialog } from '@kankan/components'
+import { custom } from './constant.js'
+import metasImage from './metas/metas-image'
+import metasVideo from './metas/metas-video'
+import metasAudio from './metas/metas-audio'
+import metasWeb from './metas/metas-web'
+import { useI18n } from '../../i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const imageNum = ref(0)
+const props = defineProps({
+    type: {
+        type: String,
+        default: null,
+    },
+})
+const hotData = computed(() => store.getters['tag/hotData'])
+const imageList = computed(() => {
+    return hotData.value.media.image || []
+})
+const videoSrc = computed(() => {
+    return hotData.value.media.video || []
+})
+const audioInfo = computed(() => {
+    return hotData.value.media.audio || []
+})
+
+const hanlderFiles = data => {
+    switch (props.type) {
+        case 'image':
+            // store.commit('tag/setImageList', data[0])
+            setImageList(data[0])
+            break
+        case 'video':
+            store.commit('tag/setVideo', data)
+            break
+        case 'audio':
+            store.commit('tag/setAudio', data)
+            break
+    }
+}
+const setImageList = data => {
+    let picLength = 0
+    let list = JSON.parse(JSON.stringify(imageList.value))
+    if (list.length > 0) {
+        picLength = list.length
+    }
+    for (let i = 0; i < data.length; i++) {
+        if (list.length < custom['image'].maxNum) {
+            list.push('')
+            var index = i + picLength
+            list[index] = { src: URL.createObjectURL(data[i]), file: data[i] }
+        } else {
+            Dialog.toast({
+                type: 'error',
+                content: `${t('limit.maxLengthFile', { length: custom['image'].maxNum })}`,
+            })
+            break
+        }
+    }
+    store.commit('tag/setImageList', list)
+}
+</script>
+<style lang="scss" scoped>
+.hide {
+    display: none;
+}
+.metas-upload {
+    width: 100%;
+    height: 100%;
+    position: relative;
+    .ui-input {
+        width: 100%;
+        height: 100%;
+    }
+}
+</style>

+ 232 - 0
packages/app-cdfg/src/components/Tags/metas/metas-appletLink.vue

@@ -0,0 +1,232 @@
+<!--  -->
+<template>
+    <div class="metas-applet-link">
+        <template v-if="isEdit">
+            <div class="add">
+                <ui-input
+                    require
+                    type="file"
+                    :disable="customized[type].upload"
+                    :scale="customized[type].scale"
+                    :accept="customized[type].accept"
+                    :multiple="customized[type].multiple"
+                    :maxSize="customized[type].maxSize"
+                    :maxLen="customized[type].maxNum"
+                    @update:modelValue="data => hanlderFiles(data)"
+                >
+                    <template v-slot:replace>
+                        <ui-icon type="add" class="icon" />
+                        <div class="avatar-span">直播頭像</div>
+                    </template>
+                </ui-input>
+
+                <div class="live-icon" :style="`background-image:url(${common.changeUrl(liveIcon.src)});`"></div>
+            </div>
+
+            <ui-input require class="input" width="100%" :placeholder="'請輸入專題ID'" type="text" :modelValue="liveLink" @update:modelValue="value => (liveLink = value)" />
+        </template>
+        <div class="show-live" v-else>
+            <div>
+                <span>直播頭像:</span>
+                <div class="live-icon" :style="`background-image:url(${common.changeUrl(liveIcon.src)});`"></div>
+            </div>
+            <div>
+                <span>專題ID:</span>
+                <span>{{ liveLink }}</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+import Editor from '../../shared/Editor'
+
+const store = useStore()
+const type = ref('applet_link')
+
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+
+const showBorder = ref(false)
+const descriptionMax = 200
+const editor$ = ref(null)
+const descriptionLength = ref(0)
+
+const liveLink = computed({
+    get() {
+        if (hotData.value.hotContent && hotData.value.hotContent != void 0) {
+            if (typeof hotData.value.hotContent !== 'string') {
+                return hotData.value.hotContent.liveLink
+            }
+        }
+        return ''
+    },
+    set(value) {
+        store.commit('tag/setLiveLink', value)
+    }
+})
+
+const liveIcon = computed(() => {
+    if (hotData.value.hotContent && hotData.value.hotContent != void 0) {
+        console.log(hotData.value.hotContent)
+        if (typeof hotData.value.hotContent !== 'string') {
+            return hotData.value.hotContent.liveIcon
+        }
+    }
+
+    return {
+        src: '',
+        name: '',
+        file: ''
+    }
+})
+
+const hanlderFiles = data => {
+    let liveIcon = { src: URL.createObjectURL(data), file: data }
+    console.log(liveIcon.src)
+    store.commit('tag/setLiveIcon', liveIcon)
+}
+
+const onDescriptionChange = ({ length, html, init }) => {
+    // // alert(1)
+    // console.log(length, html, init)
+    // if (init) {
+    //     return (descriptionLength.value = length)
+    // }
+
+    liveContent.value = html
+    descriptionLength.value = length
+}
+
+const onEditDescription = () => {
+    showBorder.value = false
+}
+
+const liveContent = computed({
+    get() {
+        if (editor$.value) {
+            descriptionLength.value = editor$.value.getLength()
+            if (hotData.value.hotContent.liveContent && hotData.value.hotContent.liveContent != void 0) {
+                return hotData.value.hotContent.liveContent
+            } else {
+                editor$.value.setHtml(hotData.value.hotContent.liveContent)
+            }
+        }
+        return hotData.value.hotContent.liveContent || ''
+    },
+    set(value) {
+        console.log(value)
+        store.commit('tag/setLiveContent', { value })
+    }
+})
+</script>
+<style lang="scss" scoped>
+.metas-applet-link {
+    width: 100%;
+    padding-top: 10px;
+    .add {
+        width: 80px;
+        margin-right: 14px;
+        text-align: center;
+        color: rgba(255, 255, 255, 0.3);
+        display: inline-block;
+        position: relative;
+
+        .ui-input {
+            width: 80px;
+            height: 80px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            :deep(.use-replace) {
+                display: inline-block !important;
+            }
+            .avatar-span {
+                font-size: 12px;
+                margin-top: 6px;
+                display: block;
+            }
+        }
+
+        .live-icon {
+            position: absolute;
+            width: 100%;
+            height: 100%;
+            top: 0;
+            left: 0;
+            background-size: cover;
+            z-index: 11;
+            pointer-events: none;
+        }
+    }
+    .input {
+        margin-top: 10px;
+    }
+    .desc-box {
+        margin-top: 10px;
+        width: 100%;
+        height: 158px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        border: 1px solid rgba(255, 255, 255, 0.2);
+        margin-bottom: 10px;
+        &.border {
+            border: 1px solid var(--editor-main-color);
+        }
+        .edit-box {
+            height: 128px;
+            padding: 10px;
+            box-sizing: border-box;
+        }
+    }
+    .tag-metas {
+        width: 100%;
+        height: 225px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        overflow: hidden;
+    }
+    .desc-link {
+        position: relative;
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        border-radius: 0px 0px 4px 4px;
+        height: 30px;
+        padding: 0 10px;
+        .iconfont {
+            font-size: 16px;
+            cursor: pointer;
+        }
+        span {
+            font-size: 12px;
+        }
+    }
+
+    .show-live {
+        padding: 0 10px;
+        > div {
+            margin: 8px 0;
+            display: flex;
+            .live-icon {
+                width: 60px;
+                height: 60px;
+                background-size: cover;
+                display: inline-block;
+                border: 1px solid rgba(255, 255, 255, 0.5);
+                border-radius: 4px;
+            }
+            > span {
+                display: inline-block;
+                min-width: 70px;
+            }
+        }
+    }
+}
+</style>

+ 67 - 0
packages/app-cdfg/src/components/Tags/metas/metas-audio.vue

@@ -0,0 +1,67 @@
+<template>
+    <div class="audio-box">
+        <div class="del-btn" @click="delAudio()">
+            <ui-icon type="del"></ui-icon>
+        </div>
+        <div v-for="(i, index) in audioInfo" class="audio-msg">
+            <ui-icon type="music"></ui-icon>
+            <span>{{ i.name }}</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, ref, computed } from 'vue'
+import { useStore } from 'vuex'
+import { Cropper, Loading, Dialog } from '@kankan/components'
+import { custom } from '../constant.js'
+import common from '@/utils/common'
+const store = useStore()
+const audioNum = ref(0)
+const type = ref('audio')
+const hotData = computed(() => store.getters['tag/hotData'])
+const audioInfo = computed(() => {
+    return hotData.value.media.audio
+})
+const delAudio = () => {
+    store.commit('tag/delMetas', { type: type.value, index: audioNum.value })
+}
+</script>
+<style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.audio-box {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    background: rgba(255, 255, 255, 0.1);
+    top: 0;
+    left: 0;
+    z-index: 10;
+    .audio-msg {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        .iconfont {
+            margin-right: 6px;
+        }
+    }
+}
+</style>

+ 172 - 0
packages/app-cdfg/src/components/Tags/metas/metas-commodity.vue

@@ -0,0 +1,172 @@
+<!--  -->
+<template>
+    <div class="metas-commodity" :class="{ noEdit: !isEdit }">
+        <div class="addcon" v-if="isEdit" @click.stop="store.commit('tag/setShowGoodsList', true)">
+            <div class="add">
+                <ui-icon type="add" class="icon" />
+            </div>
+            <span>添加商品關聯</span>
+        </div>
+        <div class="style-item" v-for="(item, i) in productsList" :key="i">
+            <div :style="`background-image:url(${item.pic});`">
+                <div v-if="isEdit" class="close-btn" @click.stop="delItem(i)">
+                    <ui-icon type="close"></ui-icon>
+                </div>
+            </div>
+            <span>{{ item.name }}</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, watch, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+
+const unique = arr => {
+    for (var i = 0; i < arr.length; i++) {
+        for (var j = i + 1; j < arr.length; j++) {
+            if (arr[i].id === arr[j].id) {
+                arr.splice(j, 1)
+                j--
+            }
+        }
+    }
+    return arr
+}
+
+const store = useStore()
+const type = ref('commodity')
+const isEdit = computed(() => store.getters['tag/isEdit'])
+
+const hotData = computed(() => store.getters['tag/hotData'])
+
+const productsList = computed(() => {
+    return hotData.value.products
+})
+const delItem = index => {
+    store.commit('tag/delMetas', { index, type: type.value })
+}
+
+watch(
+    () => store.getters['tag/seleteGoodsItems'],
+    (val, old) => {
+        setProductsList(val)
+    }
+)
+
+const setProductsList = data => {
+    let list = productsList.value.concat(data)
+    if (unique(list).length > 9) {
+        return Dialog.alert({ title: '提示', content: '單個熱點關聯商品不能超過9個', okText: '確定' })
+    }
+    store.commit('tag/setProductsList', unique(list))
+}
+
+onMounted(async () => {
+    const app = await useApp()
+})
+</script>
+<style lang="scss" scoped>
+.metas-commodity {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    flex-wrap: wrap;
+    padding-top: 10px;
+    max-height: 54vh;
+    overflow-y: auto;
+    .addcon {
+        color: rgba(255, 255, 255, 0.3);
+        margin-right: 8px;
+        .add {
+            cursor: pointer;
+            width: 80px;
+            height: 80px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            text-align: center;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            text-align: center;
+            width: 100%;
+            display: inline-block;
+        }
+    }
+    .style-item {
+        width: 80px;
+        margin-right: 8px;
+        margin-bottom: 10px;
+        color: rgba(255, 255, 255, 0.3);
+        > div {
+            width: 80px;
+            height: 80px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-repeat: no-repeat;
+            background-size: cover;
+            position: relative;
+            border-radius: 4px;
+            &.default {
+            }
+            &.active {
+                border: 1px solid var(--editor-main-color);
+            }
+            .close-btn {
+                width: 16px;
+                height: 16px;
+                border-radius: 50%;
+                position: absolute;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: rgba(250, 63, 72, 1);
+                cursor: pointer;
+                opacity: 0.6;
+                right: -6px;
+                top: -6px;
+                z-index: 101;
+                display: none;
+                &:hover {
+                    opacity: 1;
+                }
+                > i {
+                    font-size: 12px;
+                    color: #fff;
+                    transform: scale(0.8);
+                }
+            }
+            &:hover {
+                .close-btn {
+                    display: flex;
+                }
+            }
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            display: inline-block;
+            width: 100%;
+            overflow: hidden;
+            display: -webkit-box; /*彈性伸縮盒子*/
+            -webkit-box-orient: vertical; /*子元素垂直排列*/
+            -webkit-line-clamp: 2; /*可以顯示的行數,超出部分用...表示*/
+            text-overflow: ellipsis; /*(多行文本的情況下,用省略號「…」隱藏溢出範圍的文本)*/
+        }
+    }
+}
+.noEdit {
+    padding: 10px;
+}
+</style>

+ 49 - 0
packages/app-cdfg/src/components/Tags/metas/metas-coupon.vue

@@ -0,0 +1,49 @@
+<!--  -->
+<template>
+    <div class="metas-coupon">
+        <ui-input v-if="isEdit" require class="input" width="100%" :placeholder="'請輸入專題ID'" type="text" v-model="couponLink" />
+        <span v-else> 專題ID:{{ couponLink }}</span>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+const store = useStore()
+const type = ref('coupon')
+
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+const whichDept = computed(() => store.getters['scene/whichDept'])
+
+const couponLink = computed({
+    get() {
+        if (hotData.value.hotContent && hotData.value.hotContent != void 0) {
+            if (typeof hotData.value.hotContent !== 'string') {
+                return hotData.value.hotContent.couponLink
+            }
+        }
+        return ''
+    },
+    set(value) {
+        store.commit('tag/setCouponLink', value)
+    }
+})
+</script>
+<style lang="scss" scoped>
+.metas-coupon {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    flex-wrap: wrap;
+    padding-top: 10px;
+
+    > span {
+        padding: 0 10px;
+    }
+}
+</style>

+ 174 - 0
packages/app-cdfg/src/components/Tags/metas/metas-exhibits.vue

@@ -0,0 +1,174 @@
+<!--  -->
+<template>
+    <div class="metas-exhibits" :class="{ noEdit: !isEdit }">
+        <div class="addcon" v-if="isEdit" @click.stop="store.commit('tag/setShowGoodsList', true)">
+            <div class="add">
+                <ui-icon type="add" class="icon" />
+            </div>
+            <span>添加商品關聯</span>
+        </div>
+        <div class="style-item" v-for="(item, i) in productsList" :key="i">
+            <div :style="`background-image:url(${item.pic});`">
+                <div v-if="isEdit" class="close-btn" @click.stop="delItem(i)">
+                    <ui-icon type="close"></ui-icon>
+                </div>
+            </div>
+            <span>{{ item.name }}</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, watch, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+
+const unique = arr => {
+    for (var i = 0; i < arr.length; i++) {
+        for (var j = i + 1; j < arr.length; j++) {
+            if (arr[i].id === arr[j].id) {
+                arr.splice(j, 1)
+                j--
+            }
+        }
+    }
+    return arr
+}
+
+const store = useStore()
+const type = ref('exhibits')
+const isEdit = computed(() => store.getters['tag/isEdit'])
+
+const hotData = computed(() => store.getters['tag/hotData'])
+const isHongKong = computed(() => store.getters['scene/whichDept'] == 'HK')
+
+const productsList = computed(() => {
+    return hotData.value.products
+})
+const delItem = index => {
+    store.commit('tag/delMetas', { index, type: type.value })
+}
+
+watch(
+    () => store.getters['tag/seleteGoodsItems'],
+    (val, old) => {
+        setProductsList(val)
+    }
+)
+
+const setProductsList = data => {
+    let LENGTH = isHongKong.value ? 30 : 9
+    let list = productsList.value.concat(data)
+    if (unique(list).length > LENGTH) {
+        return Dialog.alert({ title: '提示', content: `單個熱點關聯商品不能超過${LENGTH}個`, okText: '確定' })
+    }
+    store.commit('tag/setProductsList', unique(list))
+}
+
+onMounted(async () => {
+    const app = await useApp()
+})
+</script>
+<style lang="scss" scoped>
+.metas-exhibits {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    flex-wrap: wrap;
+    padding-top: 10px;
+    max-height: 50vh;
+    overflow-y: auto;
+    .addcon {
+        color: rgba(255, 255, 255, 0.3);
+        margin-right: 8px;
+        .add {
+            cursor: pointer;
+            width: 80px;
+            height: 80px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            text-align: center;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            text-align: center;
+            width: 100%;
+            display: inline-block;
+        }
+    }
+    .style-item {
+        width: 80px;
+        margin-right: 8px;
+        margin-bottom: 10px;
+        color: rgba(255, 255, 255, 0.3);
+        > div {
+            width: 80px;
+            height: 80px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-repeat: no-repeat;
+            background-size: cover;
+            position: relative;
+            border-radius: 4px;
+            &.default {
+            }
+            &.active {
+                border: 1px solid var(--editor-main-color);
+            }
+            .close-btn {
+                width: 16px;
+                height: 16px;
+                border-radius: 50%;
+                position: absolute;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: rgba(250, 63, 72, 1);
+                opacity: 0.6;
+                right: -6px;
+                top: -6px;
+                z-index: 101;
+                cursor: pointer;
+                display: none;
+                &:hover {
+                    opacity: 1;
+                }
+                > i {
+                    font-size: 12px;
+                    color: #fff;
+                    transform: scale(0.8);
+                }
+            }
+            &:hover {
+                .close-btn {
+                    display: flex;
+                }
+            }
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            display: inline-block;
+            width: 100%;
+            overflow: hidden;
+            display: -webkit-box; /*彈性伸縮盒子*/
+            -webkit-box-orient: vertical; /*子元素垂直排列*/
+            -webkit-line-clamp: 2; /*可以顯示的行數,超出部分用...表示*/
+            text-overflow: ellipsis; /*(多行文本的情況下,用省略號「…」隱藏溢出範圍的文本)*/
+        }
+    }
+}
+.noEdit {
+    padding: 10px;
+}
+</style>

+ 375 - 0
packages/app-cdfg/src/components/Tags/metas/metas-image.vue

@@ -0,0 +1,375 @@
+<!--  -->
+<template>
+    <!-- <div v-if="imageList.length > 0 && type == 'IMAGE'" class="pic-box"> -->
+    <div class="pic-box" :class="{ show: viewer }" :style="metasHeight ? `height:${metasHeight}px;` : ''">
+        <div>
+            <div class="ctrl-btn left-btn" v-if="imageNum != 0" @click.stop="chengeImgae('pre')">
+                <ui-icon type="left"></ui-icon>
+            </div>
+            <div class="ctrl-btn right-btn" v-if="imageNum < imageList.length - 1" @click.stop="chengeImgae('next')">
+                <ui-icon type="right"></ui-icon>
+            </div>
+        </div>
+        <div class="over-box">
+            <div v-show="!loading" class="image-list" :style="`transform:translateX(${-100 * imageNum}%);`">
+                <div :style="`transform:translateX(${100 * index}%);background-image:url(${common.changeUrl(i.src)});`" class="image-item" v-for="(i, index) in imageList"></div>
+                <!-- <div v-else :style="`transform:translateX(${100 * index}%);`" class="image-item" v-for="(i, index) in imageList">
+                    <img @error="filesError(index)" :src="common.changeUrl(i.src)" alt="" />
+                </div> -->
+            </div>
+            <ui-icon v-show="loading" class="loading-icon" type="_loading_"></ui-icon>
+
+            <div v-if="isEdit" class="del-btn" @click="delPic()">
+                <ui-icon type="del"></ui-icon>
+            </div>
+        </div>
+        <div class="continue" v-if="(!isEdit && imageList.length > 1) || isEdit">
+            <ui-input
+                v-if="imageList.length < custom[type].maxNum && isEdit"
+                type="file"
+                :placeholder="custom[type].uploadPlace"
+                :disable="custom[type].upload"
+                :scale="custom[type].scale"
+                :accept="custom[type].accept"
+                :multiple="custom[type].multiple"
+                :maxSize="custom[type].maxSize"
+                :maxLen="custom[type].maxNum"
+                :othPlaceholder="custom[type].othPlaceholder"
+                @update:modelValue="data => hanlderFiles(data)"
+            >
+                <template v-slot:replace>
+                    <span class="continue-tips">{{ $t('tag.toolbox.continueAdd') }}</span>
+                </template>
+            </ui-input>
+            <span v-if="isEdit" class="pic-num">
+                <span class="cur">{{ imageList.length }}</span>
+                <span> / {{ custom[type].maxNum }}</span>
+            </span>
+            <span v-else class="pic-num">
+                <span class="cur">{{ imageNum + 1 }}</span>
+                <span><span>&nbsp;</span>/<span>&nbsp;</span></span>
+                <span>{{ imageList.length }}</span>
+            </span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { custom } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+import { useI18n } from '../../../i18n'
+const { t } = useI18n({ useScope: 'global' })
+const isMobile = ref(false)
+const store = useStore()
+const type = ref('image')
+const props = defineProps({
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+    viewer: {
+        type: Boolean,
+        default: false,
+    },
+})
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+const imageList = computed(() => {
+    return hotData.value.media.image
+})
+const loading = ref(true)
+const imageNum = ref(0)
+const delPic = () => {
+    store.commit('tag/delMetas', { index: imageNum.value, type: type.value })
+    if (imageNum.value > 0) {
+        imageNum.value--
+    }
+}
+const chengeImgae = type => {
+    if (type == 'pre') {
+        imageNum.value--
+    } else {
+        imageNum.value++
+    }
+}
+const hanlderFiles = data => {
+    console.log(data[0])
+    // store.commit('tag/setImageList', data[0])
+    setImageList(data[0])
+    console.log(imageList.value.length, data[0].length - 1)
+    // if (imageNum.value < imageList.value.length + data[0].length - 1) {
+    //     imageNum.value = imageList.value.length + data[0].length - 1
+    // }
+    imageNum.value = imageList.value.length - 1
+}
+
+const setImageList = data => {
+    let picLength = 0
+    let list = JSON.parse(JSON.stringify(imageList.value))
+    if (list.length > 0) {
+        picLength = list.length
+    }
+    for (let i = 0; i < data.length; i++) {
+        if (list.length < custom['image'].maxNum) {
+            list.push('')
+            var index = i + picLength
+            list[index] = { src: URL.createObjectURL(data[i]), file: data[i] }
+        } else {
+            Dialog.toast({
+                type: 'error',
+                content: `${t('limit.maxLengthFile', { length: custom['image'].maxNum })}`,
+            })
+            break
+        }
+    }
+    store.commit('tag/setImageList', list)
+}
+
+onMounted(async () => {
+    const app = await useApp()
+    isMobile.value = app.config.mobile
+
+    nextTick(() => {
+        let img = new Image()
+        img.onload = () => {
+            loading.value = false
+        }
+        img.src = common.changeUrl(imageList.value[0].src)
+    })
+})
+const filesError = index => {
+    loading.value = false
+}
+</script>
+<style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.pic-box {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    top: 0;
+    left: 0;
+    z-index: 10;
+    .loading-icon {
+        color: var(--editor-main-color);
+        animation: rotate 2s infinite linear;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 30px;
+    }
+    @keyframes rotate {
+        0% {
+            transform: translate(-50%, -50%) rotate(0deg);
+        }
+        100% {
+            transform: translate(-50%, -50%) rotate(360deg);
+        }
+    }
+    .over-box {
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+    }
+    .continue {
+        width: 100%;
+        height: 32px;
+        background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 200%);
+        border-radius: 0px 0px 4px 4px;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+
+        .ui-input {
+            width: 100%;
+        }
+        .continue-tips {
+            font-size: 12px;
+        }
+        .pic-num {
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+            font-size: 12px;
+            .cur {
+                color: var(--editor-main-color);
+            }
+        }
+    }
+
+    .ctrl-btn {
+        width: 32px;
+        height: 32px;
+        background: rgba(0, 0, 0, 0.2);
+        border-radius: 50%;
+        position: absolute;
+        cursor: pointer;
+        top: 50%;
+        transform: translateY(-50%);
+        z-index: 10;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        .iconfont {
+            font-size: 14px;
+        }
+        &.left-btn {
+            left: 5px;
+        }
+        &.right-btn {
+            right: 5px;
+        }
+    }
+    .image-list {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        transition: all 0.3s linear;
+        .image-item {
+            width: 100%;
+            height: 100%;
+            // background: red;
+            position: absolute;
+            transform: translateX(0);
+            text-align: center;
+            background-repeat: no-repeat;
+            background-size: contain;
+            background-position: center;
+            img {
+                height: 100%;
+                width: auto;
+            }
+        }
+    }
+    &.show {
+        .ctrl-btn {
+            width: 40px;
+            height: 80px;
+            background: rgba(0, 0, 0, 0.6);
+            .iconfont {
+                font-size: 20px;
+            }
+            &.left-btn {
+                left: 0px;
+                border-radius: 0 40px 40px 0;
+                .icon {
+                    margin-right: 5px;
+                }
+            }
+            &.right-btn {
+                right: 0px;
+                border-radius: 40px 0 0 40px;
+                .icon {
+                    margin-left: 8px;
+                }
+            }
+        }
+        .continue {
+            width: 76px;
+            height: 36px;
+            background: rgba(0, 0, 0, 0.6);
+            border-radius: 20px;
+            position: absolute;
+            bottom: -5%;
+            left: 50%;
+            transform: translateX(-50%);
+
+            .pic-num {
+                width: 76px;
+                height: 36px;
+                display: inline-block;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                font-size: 20px;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+                span {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }
+            }
+        }
+    }
+}
+
+[is-mobile] {
+    .pic-box {
+        &.show {
+            .ctrl-btn {
+                width: 40px;
+                height: 80px;
+                background: rgba(0, 0, 0, 0.6);
+                .iconfont {
+                    font-size: 20px;
+                }
+                &.left-btn {
+                    left: 0px;
+                    border-radius: 0 40px 40px 0;
+                    .icon {
+                        margin-right: 5px;
+                    }
+                }
+                &.right-btn {
+                    right: 0px;
+                    border-radius: 40px 0 0 40px;
+                    .icon {
+                        margin-left: 8px;
+                    }
+                }
+            }
+            .continue {
+                width: 76px;
+                height: 36px;
+                background: rgba(0, 0, 0, 0.6);
+                border-radius: 20px;
+                position: absolute;
+                bottom: -6%;
+                left: 50%;
+                transform: translateX(-50%);
+
+                .pic-num {
+                    width: 76px;
+                    height: 36px;
+                    display: inline-block;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 20px;
+                    top: 50%;
+                    left: 50%;
+                    transform: translate(-50%, -50%);
+                    span {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 225 - 0
packages/app-cdfg/src/components/Tags/metas/metas-lineScene.vue

@@ -0,0 +1,225 @@
+<!--  -->
+<template>
+    <div class="metas-applet-link">
+        <div class="add">
+            <ui-input
+                type="file"
+                :disable="customized[type].upload"
+                :scale="customized[type].scale"
+                :accept="customized[type].accept"
+                :multiple="customized[type].multiple"
+                :maxSize="customized[type].maxSize"
+                :maxLen="customized[type].maxNum"
+                :othPlaceholder="''"
+                @update:modelValue="data => hanlderFiles(data)"
+            >
+                <template v-slot:replace>
+                    <ui-icon type="add" class="icon" />
+                    <div class="avatar-span">直播头像</div>
+                </template>
+            </ui-input>
+        </div>
+
+        <ui-input require class="input" width="100%" :placeholder="'请输入网页链接'" type="text" :modelValue="liveLink" @update:modelValue="value => (liveLink = value)" maxlength="30" />
+        <ui-input require class="input" width="100%" :placeholder="'请输入网页链接'" type="text" :modelValue="liveLink" @update:modelValue="value => (liveLink = value)" maxlength="30" />
+
+        <div class="desc-box" :class="{ border: showBorder }">
+            <div class="edit-box">
+                <Editor @click="showBorder = true" :placeholder="`请输入直播介绍`" :maxlength="descriptionMax" :html="content" ref="editor$" @change="onDescriptionChange" @blur="onEditDescription" />
+            </div>
+            <div class="desc-link">
+                <!-- <LinkManage :show="!!(inInsertLink && maxTextLen)" :textlen="maxTextLen" @close="inInsertLink = false" @add="insertel" /> -->
+                <span>
+                    <span class="theme-color">{{ descriptionLength }}</span>
+                    /
+                    <span>{{ descriptionMax }}</span>
+                </span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+import { useI18n } from '../../../i18n'
+import Editor from '../../shared/Editor'
+
+const { t } = useI18n({ useScope: 'global' })
+const isMobile = ref(false)
+const store = useStore()
+const type = ref('applet_link')
+const props = defineProps({
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+    viewer: {
+        type: Boolean,
+        default: false,
+    },
+})
+
+const showList = computed(() => {
+    let styles = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+    return styles
+})
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+const imageList = computed(() => {
+    return hotData.value.media.image
+})
+const loading = ref(true)
+const imageNum = ref(0)
+
+const liveLink = ref('')
+const showBorder = ref(false)
+const descriptionMax = 200
+const editor$ = ref(null)
+const descriptionLength = ref(0)
+
+const onDescriptionChange = ({ length, html, init }) => {
+    // // alert(1)
+    // console.log(length, html, init)
+    // if (init) {
+    //     return (descriptionLength.value = length)
+    // }
+
+    content.value = html
+    descriptionLength.value = length
+}
+
+const onEditDescription = () => {
+    showBorder.value = false
+}
+
+const delPic = () => {
+    store.commit('tag/delMetas', { index: imageNum.value, type: type.value })
+    if (imageNum.value > 0) {
+        imageNum.value--
+    }
+}
+const chengeImgae = type => {
+    if (type == 'pre') {
+        imageNum.value--
+    } else {
+        imageNum.value++
+    }
+}
+const hanlderFiles = data => {
+    console.log(data[0])
+    // store.commit('tag/setImageList', data[0])
+    setImageList(data[0])
+    console.log(imageList.value.length, data[0].length - 1)
+    // if (imageNum.value < imageList.value.length + data[0].length - 1) {
+    //     imageNum.value = imageList.value.length + data[0].length - 1
+    // }
+    imageNum.value = imageList.value.length - 1
+}
+
+const setImageList = data => {
+    let picLength = 0
+    let list = JSON.parse(JSON.stringify(imageList.value))
+    if (list.length > 0) {
+        picLength = list.length
+    }
+    for (let i = 0; i < data.length; i++) {
+        if (list.length < custom['image'].maxNum) {
+            list.push('')
+            var index = i + picLength
+            list[index] = { src: URL.createObjectURL(data[i]), file: data[i] }
+        } else {
+            Dialog.toast({
+                type: 'error',
+                content: `${t('limit.maxLengthFile', { length: custom['image'].maxNum })}`,
+            })
+            break
+        }
+    }
+    store.commit('tag/setImageList', list)
+}
+
+onMounted(async () => {
+    const app = await useApp()
+    isMobile.value = app.config.mobile
+})
+const filesError = index => {
+    loading.value = false
+}
+</script>
+<style lang="scss" scoped>
+.metas-applet-link {
+    width: 100%;
+    padding-top: 10px;
+    .add {
+        width: 80px;
+        margin-right: 14px;
+        text-align: center;
+        color: rgba(255, 255, 255, 0.3);
+        display: inline-block;
+
+        .ui-input {
+            width: 80px;
+            height: 80px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            :deep(.use-replace) {
+                display: inline-block !important;
+            }
+            .avatar-span {
+                font-size: 12px;
+                margin-top: 6px;
+                display: block;
+            }
+        }
+    }
+    .input {
+        margin-top: 10px;
+    }
+    .desc-box {
+        margin-top: 10px;
+        width: 100%;
+        height: 158px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        border: 1px solid rgba(255, 255, 255, 0.2);
+        margin-bottom: 10px;
+        &.border {
+            border: 1px solid var(--editor-main-color);
+        }
+        .edit-box {
+            height: 128px;
+            padding: 10px;
+            box-sizing: border-box;
+        }
+    }
+    .tag-metas {
+        width: 100%;
+        height: 225px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        overflow: hidden;
+    }
+    .desc-link {
+        position: relative;
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        border-radius: 0px 0px 4px 4px;
+        height: 30px;
+        padding: 0 10px;
+        .iconfont {
+            font-size: 16px;
+            cursor: pointer;
+        }
+        span {
+            font-size: 12px;
+        }
+    }
+}
+</style>

+ 161 - 0
packages/app-cdfg/src/components/Tags/metas/metas-linkScene.vue

@@ -0,0 +1,161 @@
+<!--  -->
+<template>
+    <div class="metas-link-scene">
+        <div class="addcon" v-if="!sceneFirstView.sceneviewimg" @click.stop="store.commit('tag/setShowSceneList', true)">
+            <div class="add">
+                <ui-icon type="add" class="icon" />
+                <div class="avatar-span">選擇場景</div>
+            </div>
+        </div>
+
+        <div v-else class="viewcon">
+            <div class="viewscene" :style="`background-image:url(${common.changeUrl(sceneFirstView.sceneviewimg)});`"></div>
+            <div v-if="isEdit" class="close-btn" @click.stop="delItem">
+                <ui-icon type="close"></ui-icon>
+            </div>
+            <div v-if="isEdit" class="replace-mask">
+                <ui-button class="btn" @click.stop="store.commit('tag/setShowSceneList', true)" type="primary">替換場景</ui-button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, watch, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+import Editor from '../../shared/Editor'
+
+const store = useStore()
+const type = ref('link_scene')
+
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+
+const sceneFirstView = computed(() => {
+    if (hotData.value.hotContent && hotData.value.hotContent != void 0) {
+        if (typeof hotData.value.hotContent !== 'string') {
+            return hotData.value.hotContent.sceneFirstView
+        }
+    }
+
+    return {
+        num: '',
+        sceneview: '',
+        sceneviewimg: '',
+        sceneviewimgdata: '',
+    }
+})
+
+watch(
+    () => store.getters['tag/seleteSceneItem'],
+    (val, old) => {
+        setSceneList(val)
+    }
+)
+
+const setSceneList = data => {
+    console.log(data)
+    store.commit('tag/setSceneFirstView', data)
+}
+
+const delItem = () => {
+    store.commit('tag/setSceneFirstView', {
+        num: '',
+        sceneview: '',
+        sceneviewimg: '',
+        sceneviewimgdata: '',
+    })
+}
+
+onMounted(async () => {
+    const app = await useApp()
+})
+</script>
+<style lang="scss" scoped>
+.metas-link-scene {
+    width: 100%;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    background: rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+    .addcon {
+        width: 100%;
+        height: 225px;
+        position: relative;
+        cursor: pointer;
+        .add {
+            text-align: center;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            color: rgba(153, 153, 153, 1);
+            .avatar-span {
+                font-size: 12px;
+                margin-top: 6px;
+                display: block;
+            }
+        }
+    }
+    .viewcon {
+        width: 100%;
+        height: 225px;
+        .viewscene {
+            width: 100%;
+            height: 100%;
+            background-size: cover;
+        }
+        .close-btn {
+            width: 16px;
+            height: 16px;
+            border-radius: 50%;
+            position: absolute;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: rgba(250, 63, 72, 1);
+            opacity: 0.6;
+            right: -6px;
+            top: -6px;
+            cursor: pointer;
+            z-index: 101;
+            display: none;
+            &:hover {
+                opacity: 1;
+            }
+            > i {
+                font-size: 12px;
+                color: #fff;
+                transform: scale(0.8);
+            }
+        }
+
+        .replace-mask {
+            position: absolute;
+            top: 0;
+            left: 0;
+            background: rgba(0, 0, 0, 0.5);
+            z-index: 99;
+            width: 100%;
+            height: 100%;
+            display: none;
+            justify-content: center;
+            align-items: center;
+            .btn {
+                width: 40%;
+            }
+        }
+        &:hover {
+            .close-btn {
+                display: flex;
+            }
+            .replace-mask {
+                display: flex;
+            }
+        }
+    }
+}
+</style>

+ 182 - 0
packages/app-cdfg/src/components/Tags/metas/metas-pointJump.vue

@@ -0,0 +1,182 @@
+<!--  -->
+<template>
+    <div class="metas-point_jump">
+        <div v-if="isEdit" class="metas-select">
+            <input
+                ref="vmssRef"
+                spellcheck="false"
+                class="ui-text icon"
+                type="text"
+                autocomplete="off"
+                placeholder="請選擇點位ID"
+                readonly
+                @blur="blurHandler"
+                @focus="showHandler"
+                @click="clickShowHandler"
+                v-model="videoId"
+            />
+            <ul :class="`${showOption ? 'show' : ''}`">
+                <li :class="{ active: videoId == item.id }" @click="handleItem(item)" v-for="(item, i) in ballVideos" :key="i">
+                    {{ item.id }}
+                </li>
+            </ul>
+            <i class="iconfont ui-kankan-icon icon small tip-h-center tip-v-bottom icon-pull-down"></i>
+        </div>
+        <!-- <ui-input type="select" :options="ballVideos" :placeholder="'請選擇點位ID'" v-model="videoId">
+                                                <template v-slot:option="{ raw }">
+                                                    <div>{{ raw.id }}</div>
+                                                </template>
+                                            </ui-input> -->
+        <span v-else> 點位ID:{{ videoId }}</span>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+const store = useStore()
+const type = ref('point_jump')
+
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const hotData = computed(() => store.getters['tag/hotData'])
+const whichDept = computed(() => store.getters['scene/whichDept'])
+const ballVideos = computed(() => store.getters['scene/ballVideos'])
+const vmssRef = ref(null)
+const showOption = ref(false)
+
+let clickCount = 0
+
+const clickShowHandler = () => {
+    clickCount++
+    if (showOption.value && !(clickCount % 2)) {
+        showOption.value = false
+        vmssRef.value.blur()
+    } else {
+        showHandler()
+    }
+}
+
+const showHandler = () => {
+    clearTimeout(timeout)
+    showOption.value = true
+    vmssRef.value.focus()
+}
+
+let timeout
+const blurHandler = () =>
+    (timeout = setTimeout(() => {
+        showOption.value = false
+        clickCount = 0
+    }, 16))
+
+const handleItem = item => {
+    store.commit('tag/setVideoId', item.id)
+}
+
+const videoId = computed(() => {
+    if (hotData.value.hotContent && hotData.value.hotContent != void 0) {
+        if (typeof hotData.value.hotContent !== 'string') {
+            return hotData.value.hotContent.videoId
+        }
+    }
+    return ''
+})
+</script>
+<style lang="scss" scoped>
+.metas-point_jump {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    flex-wrap: wrap;
+    padding-top: 10px;
+
+    .metas-select {
+        position: relative;
+        width: 100%;
+        display: inline-flex;
+        align-items: center;
+        --base-border-color: rgba(255, 255, 255, 0.2);
+        --colors-content-color: #fff;
+        height: 100%;
+        background: var(--colors-normal-back);
+        height: 100%;
+        padding: 8px 10px;
+        border-radius: 4px;
+        border: 1px solid var(--base-border-color);
+        transition: border 0.3s ease;
+        cursor: pointer;
+
+        > input {
+            background: none;
+            padding: 0;
+            width: 100%;
+            height: 100%;
+            border-radius: 0;
+            border: unset;
+            outline: none;
+            color: #fff;
+            cursor: pointer;
+        }
+
+        > ul {
+            position: absolute;
+            top: 112%;
+            left: -1%;
+            width: 102%;
+            z-index: 999;
+            --colors-content-color: #fff;
+            list-style: none;
+            max-height: 288px;
+            background: rgba(26, 26, 26, 0.8);
+            box-shadow: 0px 0px 10px 0px rgb(0 0 0 / 30%), inset 0 0 1px rgb(255 255 255 / 90%);
+            backdrop-filter: blur(4px);
+            border-radius: 4px;
+            overflow-y: auto;
+            color: var(--colors-content-color);
+            transform: scale(1, 0);
+            transition: ease transform 0.3s;
+            opacity: 0;
+            // pointer-events: none;
+
+            > li {
+                padding: 10px 10px;
+                font-size: 14px;
+
+                &.un-data {
+                    padding: 20px;
+                    color: rgba(255, 255, 255, 0.3);
+                }
+
+                &.active {
+                    background: var(--colors-normal-back);
+                    color: var(--colors-primary-base);
+                }
+
+                &:not(.active):hover {
+                    cursor: pointer;
+                    background-color: var(--colors-primary-base);
+                }
+            }
+
+            &.show {
+                transform: scale(1, 1);
+                opacity: 1;
+                // pointer-events: auto;
+            }
+        }
+
+        .iconfont {
+            position: absolute;
+            right: 10px;
+        }
+    }
+
+    > span {
+        padding: 0 10px;
+    }
+}
+</style>

+ 127 - 0
packages/app-cdfg/src/components/Tags/metas/metas-video.vue

@@ -0,0 +1,127 @@
+<!--  -->
+<template>
+    <!-- <div class="video-box" :style="metasHeight ? `height:${metasHeight}px;` : ''"> -->
+    <div class="video-box">
+        <div v-if="isEdit" class="del-btn" @click="delVideo()">
+            <ui-icon type="del"></ui-icon>
+        </div>
+        <ui-icon v-show="loading" class="loading-icon" type="_loading_"></ui-icon>
+        <video
+            @error="filesError"
+            v-show="!loading"
+            id="video"
+            v-for="(i, index) in videoSrc"
+            class="video-item"
+            x5-video-player-type="h5-page"
+            controlslist="nodownload"
+            disablepictureinpicture=""
+            webkit-playsinline=""
+            x-webkit-airplay=""
+            playsinline=""
+            :controls="controls"
+            autoplay
+            :src="common.changeUrl(i.src)"
+        ></video>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import { custom } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import common from '@/utils/common'
+import { Dialog } from '@kankan/components'
+const videoNum = ref(0)
+const store = useStore()
+const type = ref('video')
+const hotData = computed(() => store.getters['tag/hotData'])
+const props = defineProps({
+    controls: {
+        type: Boolean,
+        default: true,
+    },
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+})
+const loading = ref(true)
+
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const videoSrc = computed(() => {
+    return hotData.value.media.video
+})
+const delVideo = () => {
+    store.commit('tag/delMetas', { type: type.value, index: videoNum.value })
+}
+
+const initVideo = file => {
+    store.commit('tag/setVideo', file)
+}
+const filesError = file => {
+    loading.value = false
+    // Dialog.toast({
+    //     content: '视频文件加载失败',
+    //     type: 'warn',
+    // })
+}
+
+onMounted(() => {
+    nextTick(() => {
+        let myVideo = document.getElementById('video')
+        myVideo.oncanplay = function () {
+            loading.value = false
+        }
+    })
+})
+</script>
+<style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.video-box {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    top: 0;
+    left: 0;
+    z-index: 10;
+    .loading-icon {
+        color: var(--editor-main-color);
+        animation: rotate 2s infinite linear;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 30px;
+    }
+    @keyframes rotate {
+        0% {
+            transform: translate(-50%, -50%) rotate(0deg);
+        }
+        100% {
+            transform: translate(-50%, -50%) rotate(360deg);
+        }
+    }
+    .video-item {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+    }
+}
+</style>

+ 174 - 0
packages/app-cdfg/src/components/Tags/metas/metas-waterfall.vue

@@ -0,0 +1,174 @@
+<!--  -->
+<template>
+    <div class="metas-waterfall" :class="{ noEdit: !isEdit }">
+        <div class="addcon" v-if="isEdit" @click.stop="store.commit('tag/setShowGoodsList', true)">
+            <div class="add">
+                <ui-icon type="add" class="icon" />
+            </div>
+            <span>添加商品關聯</span>
+        </div>
+        <div class="style-item" v-for="(item, i) in productsList" :key="i">
+            <div :style="`background-image:url(${item.pic});`">
+                <div v-if="isEdit" class="close-btn" @click.stop="delItem(i)">
+                    <ui-icon type="close"></ui-icon>
+                </div>
+            </div>
+            <span>{{ item.name }}</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, watch, onMounted, nextTick, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import common from '@/utils/common'
+import { customized } from '../constant.js'
+import { getApp, useApp } from '@/app'
+import { Dialog } from '@kankan/components'
+
+const unique = arr => {
+    for (var i = 0; i < arr.length; i++) {
+        for (var j = i + 1; j < arr.length; j++) {
+            if (arr[i].id === arr[j].id) {
+                arr.splice(j, 1)
+                j--
+            }
+        }
+    }
+    return arr
+}
+
+const store = useStore()
+const type = ref('waterfall')
+const isEdit = computed(() => store.getters['tag/isEdit'])
+
+const hotData = computed(() => store.getters['tag/hotData'])
+const isHongKong = computed(() => store.getters['scene/whichDept'] == 'HK')
+
+const productsList = computed(() => {
+    return hotData.value.products
+})
+const delItem = index => {
+    store.commit('tag/delMetas', { index, type: type.value })
+}
+
+watch(
+    () => store.getters['tag/seleteGoodsItems'],
+    (val, old) => {
+        setProductsList(val)
+    }
+)
+
+const setProductsList = data => {
+    let LENGTH = isHongKong.value ? 30 : 10
+    let list = productsList.value.concat(data)
+    if (unique(list).length > LENGTH) {
+        return Dialog.alert({ title: '提示', content: `單個熱點關聯商品不能超過${LENGTH}個`, okText: '確定' })
+    }
+    store.commit('tag/setProductsList', unique(list))
+}
+
+onMounted(async () => {
+    const app = await useApp()
+})
+</script>
+<style lang="scss" scoped>
+.metas-waterfall {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    flex-wrap: wrap;
+    padding-top: 10px;
+    max-height: 50vh;
+    overflow-y: auto;
+    .addcon {
+        color: rgba(255, 255, 255, 0.3);
+        margin-right: 8px;
+        .add {
+            cursor: pointer;
+            width: 80px;
+            height: 80px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            text-align: center;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            text-align: center;
+            width: 100%;
+            display: inline-block;
+        }
+    }
+    .style-item {
+        width: 80px;
+        margin-right: 8px;
+        margin-bottom: 10px;
+        color: rgba(255, 255, 255, 0.3);
+        > div {
+            width: 80px;
+            height: 80px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-repeat: no-repeat;
+            background-size: cover;
+            position: relative;
+            border-radius: 4px;
+            &.default {
+            }
+            &.active {
+                border: 1px solid var(--editor-main-color);
+            }
+            .close-btn {
+                width: 16px;
+                height: 16px;
+                border-radius: 50%;
+                position: absolute;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: rgba(250, 63, 72, 1);
+                opacity: 0.6;
+                right: -6px;
+                top: -6px;
+                z-index: 101;
+                cursor: pointer;
+                display: none;
+                &:hover {
+                    opacity: 1;
+                }
+                > i {
+                    font-size: 12px;
+                    color: #fff;
+                    transform: scale(0.8);
+                }
+            }
+            &:hover {
+                .close-btn {
+                    display: flex;
+                }
+            }
+        }
+        > span {
+            font-size: 12px;
+            margin-top: 6px;
+            display: inline-block;
+            width: 100%;
+            overflow: hidden;
+            display: -webkit-box; /*彈性伸縮盒子*/
+            -webkit-box-orient: vertical; /*子元素垂直排列*/
+            -webkit-line-clamp: 2; /*可以顯示的行數,超出部分用...表示*/
+            text-overflow: ellipsis; /*(多行文本的情況下,用省略號「…」隱藏溢出範圍的文本)*/
+        }
+    }
+}
+.noEdit {
+    padding: 10px;
+}
+</style>

+ 161 - 0
packages/app-cdfg/src/components/Tags/metas/metas-web.vue

@@ -0,0 +1,161 @@
+<!--  -->
+<template>
+    <div class="web-box" :style="metasHeight ? `height:${metasHeight}px;` : ''">
+        <div class="show-tips" v-if="link.length == 0">
+            <span>{{ $t('tag.toolbox.webShow') }}</span>
+        </div>
+        <div class="iframe-box" v-if="link.length > 0">
+            <div v-if="isEdit" class="del-btn" @click="delWeb()">
+                <ui-icon type="del"></ui-icon>
+            </div>
+            <iframe v-for="(i, index) in link" :src="i.src" frameborder="0"></iframe>
+        </div>
+        <div v-if="isEdit" class="input-web" :class="{ disabled: link.length > 0 }">
+            <input type="text" v-model="inputValue" placeholder="https://" autocomplete="off" />
+            <div class="ok-web" v-if="link.length <= 0" @click="confirmWeb">
+                <ui-icon type="checkbox1"></ui-icon>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, ref, computed, defineProps } from 'vue'
+import { useStore } from 'vuex'
+import { Cropper, Loading, Dialog } from '@kankan/components'
+import { custom } from '../constant.js'
+const store = useStore()
+const linkNum = ref(0)
+const hotData = computed(() => store.getters['tag/hotData'])
+const link = computed(() => {
+    return hotData.value.media.link
+})
+const props = defineProps({
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+})
+const inputLink = computed(() => store.getters['tag/inputLink'])
+const isEdit = computed(() => store.getters['tag/isEdit'])
+const inputValue = computed({
+    get() {
+        if (inputLink.value && inputLink.value != void 0) {
+            return inputLink.value
+        }
+        return inputLink.value || ''
+    },
+    set(value) {
+        store.commit('tag/setLink', value)
+    },
+})
+const type = ref('link')
+const showIframe = ref(false)
+
+const delWeb = () => {
+    store.commit('tag/delMetas', { type: type.value, index: linkNum.value })
+    showIframe.value = false
+    inputValue.value = null
+}
+const confirmWeb = () => {
+    if (inputValue.value.indexOf('http' || 'https') == -1) {
+        inputValue.value = 'https://' + inputValue.value
+    }
+    console.log(inputValue.value)
+    store.commit('tag/setWeb', inputValue.value)
+    showIframe.value = false
+    showIframe.value = true
+}
+</script>
+<style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.web-box {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    background: rgba(255, 255, 255, 0.1);
+    top: 0;
+    left: 0;
+    z-index: 10;
+    .show-tips {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        span {
+            color: rgba(255, 255, 255, 0.3);
+            font-size: 16px;
+            font-weight: bold;
+        }
+    }
+    .iframe-box {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        iframe {
+            width: 100%;
+            height: 100%;
+        }
+    }
+    .input-web {
+        height: 32px;
+        background: linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, #000000 200%) !important;
+        border-radius: 0px 0px 4px 4px;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 10px 0 0;
+        &.disabled {
+            background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 0%, #000000 200%) !important;
+            opacity: 0.8 !important;
+        }
+        input {
+            background: none;
+            border: none;
+            padding: 0 0 0 10px;
+            width: 94%;
+            box-sizing: border-box;
+            &:focus {
+                border: none;
+            }
+            &::placeholder {
+                color: rgba(255, 255, 255, 0.6);
+            }
+        }
+        .ok-web {
+            width: 16px;
+            height: 16px;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.6);
+            color: rgba(0, 0, 0, 0.6);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+        }
+    }
+}
+</style>

+ 391 - 0
packages/app-cdfg/src/components/Tags/scene-list.vue

@@ -0,0 +1,391 @@
+<!--  -->
+<template>
+    <teleport to="body">
+        <div class="scene-layer">
+            <div class="scene-info">
+                <div class="scene-header">
+                    <span>{{ isSelect ? '设置画面' : '选择场景' }}</span>
+                    <ui-icon @click.stop="emit('close')" class="close-btn" type="close"></ui-icon>
+                </div>
+
+                <div class="scene-con" v-if="!isSelect">
+                    <div class="g-search">
+                        <!-- <ui-input @click.stop type="select" @update:modelValue="onSelect" :options="sceneOptions" width="160px" v-model="scene" placeholder="商品分类" >
+                        <template v-slot:option="{ raw }">
+                                <span>{{ raw.label }}</span>
+                        </template>
+                        </ui-input> -->
+                        <ui-input type="text" width="260px" placeholder="输入场景名" v-model="sceneName">
+                            <template v-slot:preIcon>
+                                <ui-icon type="search" class="icon" />
+                            </template>
+                        </ui-input>
+
+                        <ui-input type="text" width="260px" placeholder="输入S/N码" v-model="snCode">
+                            <template v-slot:preIcon>
+                                <ui-icon type="search" class="icon" />
+                            </template>
+                        </ui-input>
+                    </div>
+                    <div
+                        class="table-layer"
+                        v-infinite-scroll="getData"
+                        infinite-scroll-disabled="disabled_x"
+                        :infinite-scroll-throttle-delay="500"
+                        :infinite-scroll-immediate-check="false"
+                        infinite-scroll-distance="30"
+                    >
+                        <table class="list">
+                            <thead>
+                                <tr>
+                                    <th width="10px"></th>
+                                    <th>序号</th>
+                                    <th>场景名称</th>
+                                    <th>场景码</th>
+                                    <th>拍摄时间</th>
+                                    <th>SN码</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                <tr v-for="(item, i) in list" :key="i">
+                                    <td width="10px">
+                                        <div class="checkbox">
+                                            <input type="radio" @change="e => selectScene(item, e.target.checked)" :checked="item == select" />
+                                            <span></span>
+                                        </div>
+                                    </td>
+                                    <td>{{ i + 1 }}</td>
+                                    <td>{{ item.sceneName }}</td>
+                                    <td>{{ item.num }}</td>
+                                    <td>{{ item.createTime }}</td>
+                                    <td>{{ item.snCode }}</td>
+                                </tr>
+                                <tr class="nodata" v-if="list.length === 0">
+                                    <td colspan="6">暂无数据</td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+
+                <div class="view-con" v-else>
+                    <!-- ${select.num} -->
+                    <iframe ref="viewscene$" :src="`show.html?m=${select.num}`" frameborder="0"></iframe>
+                </div>
+
+                <div class="scene-footer">
+                    <ui-button @click.stop="close" type="cancel">取消</ui-button>
+                    <ui-button :class="{ disabled: loading || isSceneError }" @click.stop="submit" v-if="isSelect" type="primary">确认</ui-button>
+                    <ui-button @click.stop="save" v-else type="primary">保存</ui-button>
+                </div>
+            </div>
+        </div>
+    </teleport>
+</template>
+
+<script setup>
+import { reactive, defineEmits, onBeforeMount, onMounted, ref, watchEffect, computed, watch, nextTick } from 'vue'
+import { Dialog, Loading } from '@kankan/components'
+import metasImage from './metas/metas-image'
+import common from '@/utils/common'
+
+import { useStore } from 'vuex'
+const emit = defineEmits(['close'])
+
+const store = useStore()
+const viewscene$ = ref(null)
+const isSelect = ref(false)
+
+const loading = ref(false)
+
+const isSceneError = ref(false)
+
+// const sceneOptions = computed(() => {
+//     let list = store.getters['tag/categoryList']
+//     list.map(i => {
+//         i.value = i.id
+//         i.label = i.categoryName
+//     })
+//     return list
+// })
+
+const list = computed(() => {
+    return store.getters['tag/sceneList'] || []
+})
+
+const scene = ref(null)
+const sceneName = ref('')
+const snCode = ref('')
+
+const allSelect = ref(false)
+const select = ref(null)
+
+const selectScene = (item, isSelect) => {
+    select.value = item
+}
+
+const close = () => {
+    if (isSelect.value) {
+        isSelect.value = false
+    } else {
+        emit('close')
+    }
+}
+const onSelect = data => {
+    scene.value = data
+}
+
+const getData = common.debounce(
+    (reset = false) => {
+        store.dispatch('tag/getSceneList', {
+            sceneName: sceneName.value,
+            snCode: snCode.value,
+            pageNum: reset ? 1 : Math.floor(list.value.length / 20) + 1,
+            pageSize: 20,
+            reset
+        })
+    },
+    700,
+    false
+)
+
+const save = () => {
+    if (!select.value) {
+        Dialog.toast({
+            content: `请选择场景`,
+            type: 'warn'
+        })
+        return
+    }
+    Loading.show()
+    isSelect.value = true
+    loading.value = true
+}
+
+const submit = async () => {
+    Loading.show()
+    let thumbs = await viewscene$.value.contentWindow.__sdk.Camera.screenshot()
+    store.commit('tag/pushSceneItem', {
+        num: select.value.num,
+        sceneview: viewscene$.value.contentWindow.__sdk.Camera.getPoseUrlParams(),
+        sceneviewimg: URL.createObjectURL(thumbs[1].data),
+        sceneviewimgdata: thumbs[1].data
+    })
+    Loading.hide()
+    emit('close')
+}
+
+watch(
+    () => sceneName.value,
+    (newval, old) => {
+        getData(true)
+    }
+)
+
+watch(
+    () => snCode.value,
+    (newval, old) => {
+        getData(true)
+    }
+)
+
+onMounted(() => {
+    getData(true)
+    nextTick(() => {
+        window.addEventListener('message', res => {
+            if (Object.prototype.toString.call(res.data) == '[object Object]') {
+                if (res.data.source === 'viewSceneReady') {
+                    loading.value = false
+                    isSceneError.value = false
+                    Loading.hide()
+                }
+
+                if (res.data.source === 'viewSceneError') {
+                    loading.value = false
+                    Loading.hide()
+                    isSceneError.value = true
+                }
+            }
+        })
+    })
+})
+</script>
+<style lang="scss">
+.scene-layer {
+    width: 100vw;
+    height: 100vh;
+    z-index: 100000;
+    top: 0;
+    position: fixed;
+    left: 0;
+    // padding: calc(var(--editor-head-height) + 20px) calc(var(--editor-toolbox-width) + 20px) 60px calc(var(--editor-menu-width) + 20px);
+    // background-color: rgba(255, 255, 255, 0.7);
+    z-index: 999;
+    .scene-info {
+        color: #fff;
+        width: 800px;
+        height: 760px;
+        margin: 5% auto;
+        user-select: none;
+        filter: var(--editor-menu-filter);
+        background-color: var(--editor-menu-back);
+        backdrop-filter: blur(4px);
+        border: solid 1px #000;
+        border-radius: 4px;
+        overflow: hidden;
+        &::after {
+            position: absolute;
+            top: 0;
+            left: 0;
+            content: '';
+            width: 100%;
+            height: 100%;
+            z-index: -1;
+            border: solid 1px rgba(255, 255, 255, 0.16);
+        }
+
+        .scene-header {
+            width: 100%;
+            padding: 22px 20px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+            .close-btn {
+                font-size: 16px;
+                cursor: pointer;
+            }
+        }
+
+        .scene-con {
+            padding: 20px;
+            height: calc(100% - 142px);
+            .g-search {
+            }
+
+            .table-layer {
+                overflow-y: auto;
+                height: calc(100% - 54px);
+                flex: 1;
+                margin-top: 20px;
+                background: rgba(176, 176, 176, 0.16);
+                border: 1px solid rgba(255, 255, 255, 0.1);
+                .list {
+                    border-collapse: collapse;
+                    width: 100%;
+                }
+
+                .list td,
+                .list th {
+                    font-size: 14px;
+                    line-height: 20px;
+                    text-align: left;
+                    padding: 15px;
+                    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+                    &:not(&:first-of-type) {
+                        // min-width: 90px;
+                    }
+                }
+
+                .list th {
+                    color: rgba(255, 255, 255, 0.6);
+                    font-weight: normal;
+                    background: rgba(27, 27, 28, 0.8);
+                }
+
+                .nodata td,
+                .nodata th {
+                    text-align: center;
+                }
+
+                .list-info {
+                    height: 28px;
+                    padding-left: 33px;
+                    line-height: 28px;
+                    position: relative;
+                    text-align: left;
+                }
+
+                .list-info > img {
+                    position: absolute;
+                    left: 0;
+                    top: 0;
+                    width: 28px;
+                    height: 28px;
+                }
+            }
+        }
+
+        .view-con {
+            padding: 20px;
+            height: calc(100% - 142px);
+            > iframe {
+                width: 100%;
+                height: 100%;
+            }
+        }
+
+        .scene-footer {
+            width: 100%;
+            padding: 22px 20px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            border-top: 1px solid rgba(255, 255, 255, 0.16);
+            > button {
+                width: 105px;
+                margin: 0 10px;
+            }
+        }
+    }
+
+    .checkbox {
+        position: relative;
+        width: 14px;
+        height: 14px;
+        &.disabled {
+            opacity: 0.3;
+            > input {
+                cursor: default;
+            }
+        }
+    }
+
+    .checkbox > input,
+    .checkbox > span {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        left: 0;
+        top: 0;
+        border: 1px solid rgba(176, 176, 176, 0.5);
+    }
+
+    .checkbox > input {
+        z-index: 1;
+        opacity: 0;
+        cursor: pointer;
+    }
+
+    .checkbox > span {
+        z-index: 2;
+        pointer-events: none;
+        border-radius: 50%;
+    }
+
+    .checkbox > input:checked + span {
+        border: 1px solid #00c8af;
+        &::after {
+            position: absolute;
+            width: 60%;
+            height: 60%;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            content: ' ';
+            background: #00c8af;
+            border-radius: 50%;
+        }
+    }
+}
+</style>

+ 239 - 0
packages/app-cdfg/src/components/Tags/show-tag.vue

@@ -0,0 +1,239 @@
+<!--  -->
+<template>
+    <div class="show-tag" :id="`tagBox_${hotData.sid}`">
+        <div class="tag-title">
+            <h2>
+                {{ hotData.title }}
+                <!-- <ui-audio v-if="hotData.type == 'audio' && audioInfo.length > 0" class="audio" ref="audio" :src="common.changeUrl(audioInfo[0].src)"></ui-audio> -->
+            </h2>
+        </div>
+        <div class="desc" v-if="hotData.content != ''">
+            <div class="text" v-html="hotData.content"></div>
+        </div>
+        <div class="tag-metas" :class="{ mask: hotData.type == 'link', nocursor: hotData.type == 'video' }">
+            <!-- <metasImage v-if="hotData.type == 'image'" />
+            <metasVideo v-if="hotData.type == 'video'" />
+            <metasWeb v-if="hotData.type == 'link'" /> -->
+
+            <metasCommodity v-if="hotData.type == 'commodity'" />
+            <metasCoupon v-if="hotData.type == 'coupon'" />
+            <metasAppletLink v-if="hotData.type == 'applet_link'" />
+            <metasWaterfall v-if="hotData.type == 'waterfall'" />
+            <metasLinkScene v-if="hotData.type == 'link_scene'" />
+            <metasExhibits v-if="hotData.type == 'exhibits'" />
+            <metasPointJump v-if="hotData.type == 'point_jump'" />
+        </div>
+        <div class="edit-btn" v-if="routerName && routerName == 'tag' && !editModule">
+            <span @click="edit()"><ui-icon type="edit"></ui-icon> 修改</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, ref, watchEffect, computed, watch, defineEmits } from 'vue'
+import metasImage from './metas/metas-image'
+import metasVideo from './metas/metas-video'
+import metasAudio from './metas/metas-audio'
+import metasWeb from './metas/metas-web'
+
+import metasCommodity from './metas/metas-commodity'
+import metasCoupon from './metas/metas-coupon'
+import metasAppletLink from './metas/metas-appletLink'
+import metasWaterfall from './metas/metas-waterfall'
+import metasExhibits from './metas/metas-exhibits'
+import metasLinkScene from './metas/metas-linkScene'
+import metasPointJump from './metas/metas-pointJump'
+
+import common from '@/utils/common'
+import { useStore } from 'vuex'
+// import { useRouter } from 'vue-router'
+import { useMusicPlayer } from '@/utils/sound'
+const musicPlayer = useMusicPlayer()
+// const router = useRouter()
+const editModule = computed(() => store.getters['editModule'])
+const store = useStore()
+const emit = defineEmits(['open'])
+const hotData = computed(() => {
+    let data = store.getters['tag/hotData']
+    if (data.type == 'audio' || data.type == 'video') {
+        musicPlayer.pause(true)
+        // console.log('pause(true)')
+    }
+    return data
+})
+
+const audioInfo = computed(() => {
+    return hotData.value.media.audio
+})
+const router = computed(() => store.getters['router'])
+const routerName = computed(() => {
+    let name = router.value.name || null
+    return name
+})
+const audio = ref(null)
+watchEffect(() => {
+    if (audio.value) {
+        audio.value.play()
+    }
+})
+const open = () => {
+    if (hotData.value.type != 'video') {
+        emit('open')
+    }
+}
+const edit = () => {
+    store.commit('tag/edit')
+    store.commit('tag/gotoTag', hotData.value)
+}
+onMounted(() => {})
+</script>
+<style lang="scss" scoped>
+.show-tag {
+    pointer-events: auto;
+    background: rgba(27, 27, 28, 0.8);
+    border-radius: 4px;
+    // border: 1px solid #000000;
+    // backdrop-filter: blur(4px);
+    min-width: 400px;
+    // min-height: 100px;
+    padding: 30px 20px;
+    .edit-btn {
+        margin-top: 20px;
+        text-align: right;
+        span {
+            font-size: 14px;
+            color: rgba(255, 255, 255, 0.6);
+            cursor: pointer;
+            &:hover {
+                color: #fff;
+            }
+        }
+    }
+    .tag-metas {
+        width: 100%;
+        min-height: 225px;
+        max-height: 500px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        overflow-x: auto;
+        position: relative;
+        margin-top: 20px;
+        &.nocursor {
+            cursor: auto;
+        }
+        &.mask {
+            &::after {
+                content: '';
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                z-index: 100;
+            }
+        }
+    }
+    .tag-title {
+        word-break: break-all;
+        h2 {
+            font-size: 20px;
+            // margin-bottom: 10px;
+            line-height: 20px;
+            color: #ffffff;
+            position: relative;
+            .ui-audio {
+                float: right;
+                &.audio {
+                    display: inline-block;
+                    cursor: pointer;
+                }
+            }
+        }
+    }
+    .desc {
+        margin-top: 10px;
+        .text {
+            font-size: 14px;
+            color: #999999;
+            line-height: 20px;
+            text-align: justify;
+            word-break: break-all;
+        }
+    }
+}
+[is-mobile] {
+    .show-tag {
+        pointer-events: auto;
+        background: rgba(27, 27, 28, 0.8);
+        border-radius: 0.0533rem;
+        // border: 1px solid #000000;
+        // backdrop-filter: blur(0.0533rem);
+        min-width: 7.4667rem;
+        // min-height: 4rem;
+        padding: 0.4rem 0.2667rem;
+
+        .edit-btn {
+            margin-top: 0.2667rem;
+            text-align: right;
+            span {
+                font-size: 0.1867rem;
+                color: rgba(255, 255, 255, 0.6);
+                cursor: pointer;
+                &:hover {
+                    color: #fff;
+                }
+            }
+        }
+        .tag-metas {
+            width: 100%;
+            height: 4.2667rem;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 0.0533rem;
+            overflow: hidden;
+            position: relative;
+            cursor: -webkit-zoom-in;
+            margin-top: 0.4rem;
+            &.mask {
+                &::after {
+                    content: '';
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    width: 100%;
+                    height: 100%;
+                    z-index: 100;
+                }
+            }
+        }
+        .tag-title {
+            h2 {
+                font-size: 0.5333rem;
+                color: #ffffff;
+                position: relative;
+                .ui-audio {
+                    float: right;
+                    &.audio {
+                        display: inline-block;
+                        cursor: pointer;
+                    }
+                }
+            }
+        }
+        .desc {
+            margin-bottom: 0.2933rem;
+
+            .text {
+                font-size: 0.3733rem;
+                color: #999999;
+                line-height: 0.2533rem;
+                text-align: justify;
+                line-height: 0.5333rem;
+
+                p {
+                    line-height: 0.5333rem;
+                }
+            }
+        }
+    }
+}
+</style>

+ 344 - 0
packages/app-cdfg/src/components/Tags/style-icon.vue

@@ -0,0 +1,344 @@
+<!--  -->
+<template>
+    <div class="style-icon">
+        <div class="style-list">
+            <div class="upload-btn" v-if="icons && icons.length < 999">
+                <ui-button class="add">
+                    <ui-input v-vip:vip="{ isVip: auth.isVip, isExpired: auth.isExpired }" class="input" preview accept=".jpg, .jpeg, .png" @update:modelValue="iconUpload" type="file">
+                        <template v-slot:replace>
+                            <ui-icon type="add" class="icon" />
+                        </template>
+                    </ui-input>
+                </ui-button>
+                <div class="vip-tag"></div>
+            </div>
+            <div class="style-item default" @click="changeLogo('')" :style="`background-image:url(${defaultLogo});`" :class="{ active: icon == '' }"></div>
+            <template v-for="(i, index) in showList">
+                <!-- <div class="style-item" @click="changeLogo(i.id, index)" v-if="index < 3" :style="`background:${i.name};`" :class="{ active: icon == i.id }"></div> -->
+                <div class="style-item" @click="changeLogo(i.name, i.sid)" v-if="index < 4" :style="`background-image:url(${common.changeUrl(i.name)});`" :class="{ active: iconId == i.sid }">
+                    <div class="close-btn" @click.stop="delIcon(i)">
+                        <ui-icon type="close"></ui-icon>
+                    </div>
+                </div>
+            </template>
+            <div class="more" v-if="icons && icons.length > 4">
+                <ui-button @click.stop="showMore = !showMore">
+                    <ui-icon :class="{ active: showMore }" type="pull-down"></ui-icon>
+                </ui-button>
+                <div class="tagLogo-box" :class="{ active: showMore }">
+                    <template v-for="(i, index) in hideList">
+                        <div @click="insertLogo(i)" class="tagLogo-item" :style="`background-image:url(${common.changeUrl(i.name)});`">
+                            <div class="close-btn">
+                                <ui-icon type="close" @click.stop="delIcon(i)"></ui-icon>
+                            </div>
+                        </div>
+                    </template>
+                </div>
+            </div>
+        </div>
+        <!-- <span class="more">更多></span> -->
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, onBeforeMount, onMounted, onUnmounted, ref, computed } from 'vue'
+import { Cropper, Loading, Dialog } from '@kankan/components'
+import common from '@/utils/common'
+import { useStore } from 'vuex'
+import { getApp } from '@/app'
+import { useI18n } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
+const store = useStore()
+const showMore = ref(false)
+const defaultLogo = ref('')
+defaultLogo.value = getApp().resource.getAppURL('images/tag_icon_default.svg')
+const chooseItem = ref(null)
+const hotData = computed(() => store.getters['tag/hotData'])
+const icons = computed(() => {
+    console.log(store.getters['tag/icons'])
+    return store.getters['tag/icons'] || []
+})
+const auth = computed(() => store.getters['scene/auth'])
+const iconId = computed(() => store.getters['tag/iconId'])
+const icon = computed(() => {
+    return hotData.value.icon
+})
+
+const showList = computed(() => {
+    for (let i = 0; i < icons.value.length; i++) {
+        if (icons.value[i].sid == iconId.value) {
+            chooseItem.value = icons.value[i]
+        }
+    }
+    const styles = icons.value.slice(0, icons.value.length > 4 ? (icons.value.length < 999 ? 3 : 4) : 4)
+    if (chooseItem.value && !styles.includes(chooseItem.value)) {
+        let index = icons.value.length < 999 ? 2 : 3
+        styles[index] = chooseItem.value
+    }
+    return styles
+})
+const hideList = computed(() => {
+    let res = icons.value.filter(style => {
+        return !showList.value.includes(style)
+    })
+    return res
+})
+
+// let fristIndex = null
+const iconUpload = item => {
+    let sid = common.getRandomSid()
+    let src = URL.createObjectURL(item.file)
+    let pramas = {
+        img: src,
+        fixedNumber: [1, 1],
+        title: t('common.clip'),
+        noText: t('common.cancel'),
+        okText: t('common.confirm'),
+        longSize: t('common.longSize'),
+        squareSize: t('common.squareSize'),
+        cliping: t('settings.toolbox.cliping'),
+        clipEmpty: t('toast.clipEmpty'),
+    }
+    let suffix = item.file.type.split('/')[1]
+    let fileName = sid + '.' + suffix
+    Cropper.open(pramas).then(res => {
+        if (res) {
+            let data = {
+                sid: sid,
+                name: res[1],
+                fileName: fileName,
+            }
+            store.commit('tag/pushIcon', { ...data })
+            store.commit('tag/changeIcon', { ...data })
+        }
+    })
+}
+const changeLogo = (name, sid) => {
+    store.commit('tag/changeIcon', { name: name, sid: sid })
+    showMore.value = false
+}
+const insertLogo = item => {
+    chooseItem.value = item
+    store.commit('tag/changeIcon', item)
+    showMore.value = false
+}
+const delIcon = item => {
+    chooseItem.value = null
+    store.commit('tag/deleteIcon', item)
+    if (item.sid == iconId.value) {
+        store.commit('tag/changeIcon', { name: '', sid: '' })
+    }
+}
+const closeMore = () => {
+    showMore.value = false
+}
+onMounted(() => {
+    window.addEventListener('click', closeMore, false)
+})
+onUnmounted(() => {
+    window.removeEventListener('click', closeMore, false)
+})
+</script>
+<style lang="scss" scoped>
+.style-icon {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin: 0 0 24px;
+    // .more {
+    //     color: var(--editor-main-color);
+    //     cursor: pointer;
+    // }
+    .style-list {
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        > div {
+            margin-right: 20px;
+            &:last-of-type {
+                margin-right: 0;
+            }
+        }
+        .upload-btn {
+            position: relative;
+            cursor: pointer;
+            .ui-input {
+                position: relative;
+                z-index: 11;
+            }
+        }
+
+        .add {
+            width: 40px;
+            height: 40px;
+            border-radius: 0;
+            cursor: pointer;
+            // margin-right: 20px;
+            border: 1px solid #fde1b0;
+            color: #fde1b0;
+            position: relative;
+            border-radius: 4px;
+            transition: none;
+        }
+        .style-item {
+            width: 40px;
+            height: 40px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            // background: #f2f2f2;
+            background-repeat: no-repeat;
+            background-size: cover;
+            position: relative;
+            border-radius: 4px;
+            border: 1px solid transparent;
+            &.default {
+            }
+            &.active {
+                border: 1px solid var(--editor-main-color);
+            }
+            .close-btn {
+                width: 16px;
+                height: 16px;
+                border-radius: 50%;
+                position: absolute;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: rgba(250, 63, 72, 1);
+                opacity: 0.6;
+                right: -6px;
+                top: -6px;
+                z-index: 101;
+                display: none;
+                &:hover {
+                    opacity: 1;
+                }
+                > i {
+                    font-size: 12px;
+                    color: #fff;
+                    transform: scale(0.8);
+                }
+            }
+            &:hover {
+                .close-btn {
+                    display: flex;
+                }
+            }
+        }
+        .more {
+            width: 40px;
+            height: 40px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            // border-radius: 4px;
+            // border: 1px solid rgba(255, 255, 255, 0.2);
+            box-sizing: border-box;
+            position: relative;
+            z-index: 100;
+            button {
+                width: 100%;
+                height: 100%;
+                position: relative;
+                .iconfont {
+                    transition: all 0.3s;
+                    position: absolute;
+                    margin-top: -7px;
+                    margin-left: -7px;
+
+                    &.active {
+                        transform: rotate(180deg);
+                    }
+                }
+            }
+            .tagLogo-box {
+                position: absolute;
+                width: 380px;
+                // height: 200px;
+                background: rgba(27, 27, 28, 0.9);
+                box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+                border: 1px solid #000000;
+                backdrop-filter: blur(4px);
+                z-index: 10;
+                right: -20px;
+                top: 100%;
+                margin-top: 12px;
+                display: flex;
+                flex-wrap: wrap;
+                align-items: center;
+                justify-content: flex-start;
+                padding: 0 18px 20px 18px;
+                transform: scale(0);
+                transform-origin: 90% top;
+                transition: all 0.3s;
+                &.active {
+                    transform: scale(1);
+                    transform-origin: 90% top;
+                }
+                &::after {
+                    position: absolute;
+                    content: '';
+                    right: 32px;
+                    top: -10px;
+                    width: 0;
+                    height: 0;
+                    border-left: 6px solid transparent;
+                    border-right: 6px solid transparent;
+                    border-bottom: 10px solid rgba(27, 27, 28, 0.8);
+                }
+                .tagLogo-item {
+                    width: 40px;
+                    height: 40px;
+                    border-radius: 4px;
+                    background: #fff;
+                    margin-right: 20px;
+                    margin-top: 20px;
+                    position: relative;
+                    border: 1px solid transparent;
+                    background-size: cover;
+
+                    &.active {
+                        border: 1px solid var(--editor-main-color);
+                    }
+                    &:nth-of-type(6n) {
+                        margin-right: 0;
+                    }
+                    .close-btn {
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 50%;
+                        position: absolute;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        background: rgba(250, 63, 72, 1);
+                        opacity: 0.6;
+                        right: -6px;
+                        top: -6px;
+                        z-index: 101;
+                        display: none;
+                        &:hover {
+                            opacity: 1;
+                        }
+                        > i {
+                            font-size: 12px;
+                            color: #fff;
+                            transform: scale(0.8);
+                        }
+                    }
+                    &:hover {
+                        .close-btn {
+                            display: flex;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 259 - 0
packages/app-cdfg/src/components/Tags/tag-info.vue

@@ -0,0 +1,259 @@
+<!--  -->
+<template>
+    <div class="hot-editor" :id="`tagBox_${hotData.sid}`">
+        <div class="header">
+            <div class="header-title">{{ $t('tag.toolbox.hotspot') }}</div>
+            <!-- <ui-icon class="close" type="close"></ui-icon> -->
+        </div>
+        <div class="hot-content">
+            <StyleIcon />
+            <ui-input
+                require
+                class="input"
+                width="100%"
+                :placeholder="$t('tag.toolbox.hotspotTitleTips')"
+                type="text"
+                :modelValue="title"
+                @update:modelValue="value => (title = value)"
+                maxlength="30"
+            />
+
+            <div class="desc-box" v-if="info.showDesc" :class="{ border: showBorder }">
+                <div class="edit-box">
+                    <Editor
+                        @click="showBorder = true"
+                        :placeholder="$t('tag.toolbox.hotspotDescTips')"
+                        :maxlength="descriptionMax"
+                        :html="content"
+                        ref="editor$"
+                        @change="onDescriptionChange"
+                        @blur="onEditDescription"
+                    />
+                </div>
+                <div class="desc-link">
+                    <!-- <LinkManage :show="!!(inInsertLink && maxTextLen)" :textlen="maxTextLen" @close="inInsertLink = false" @add="insertel" /> -->
+                    <LinkManage v-if="showLink" @close="closeLink" @confirm="insertText" />
+                    <ui-icon type="link" class="icon" @click="showLink = true" />
+
+                    <span>
+                        <span class="theme-color">{{ descriptionLength }}</span>
+                        /
+                        <span>{{ descriptionMax }}</span>
+                    </span>
+                </div>
+            </div>
+
+            <div class="tag-metas" v-if="info.showDesc">
+                <MetasUpload :type="info.type" />
+            </div>
+
+            <MetasUploadCustomized v-else :type="info.type" />
+
+            <div class="submit-ctrl">
+                <div class="radio-group">
+                    <!-- <ui-input
+                        v-for="(item, type) in custom"
+                        :key="type"
+                        class="radio"
+                        type="radio"
+                        :modelValue="info.type === type"
+                        @update:modelValue="changeType(type)"
+                        :tip="item.name"
+                        :icon="item.icon"
+                    /> -->
+                    <ui-input v-for="(item, type) in customized" :key="type" class="radio" type="radio" :modelValue="info.type === type" @update:modelValue="changeType(type)" :tip="item.name">
+                        <span>{{ item.name }}</span>
+                    </ui-input>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { reactive, toRefs, ref, onBeforeMount, onMounted, defineProps, computed, nextTick } from 'vue'
+import MetasUpload from './metas-upload.vue'
+import MetasUploadCustomized from './metas-upload-customized.vue'
+import StyleIcon from './style-icon.vue'
+import LinkManage from './link-manage.vue'
+import { custom, customized } from './constant.js'
+import Editor from '../shared/Editor'
+import { useStore } from 'vuex'
+const store = useStore()
+
+const editor$ = ref(null)
+const descriptionMax = 200
+const descriptionLength = ref(0)
+const showLink = ref(false)
+const hotData = computed(() => store.getters['tag/hotData'])
+const whichDept = computed(() => store.getters['scene/whichDept'])
+
+if (whichDept.value == 'HK') {
+    delete customized['applet_link']
+} else {
+    delete customized['exhibits']
+    delete customized['point_jump']
+}
+
+const info = computed(() => {
+    // let showDesc = hotData.value.type == 'image' || hotData.value.type == 'audio' || hotData.value.type == 'video' || hotData.value.type == 'link'
+    let data = {
+        type: hotData.value.type || 'commodity'
+    }
+    return data
+})
+const title = computed({
+    get() {
+        if (hotData.value.title && hotData.value.title != void 0) {
+            return hotData.value.title
+        }
+        return hotData.value.title || ''
+    },
+    set(value) {
+        store.commit('tag/setTitle', value)
+    }
+})
+const content = computed({
+    get() {
+        if (editor$.value) {
+            descriptionLength.value = editor$.value.getLength()
+            if (hotData.value.content && hotData.value.content != void 0) {
+                return hotData.value.content
+            } else {
+                editor$.value.setHtml(hotData.value.content)
+            }
+        }
+        return hotData.value.content || ''
+    },
+    set(value) {
+        // console.log(value)
+        store.commit('tag/setContent', { value })
+    }
+})
+
+const closeLink = () => {
+    showLink.value = false
+}
+const insertText = data => {
+    editor$.value.insertLink(data.text.value, data.link.value)
+    nextTick(() => {
+        store.commit('tag/update', { content: editor$.value.getHtml() })
+    })
+    closeLink()
+}
+const onDescriptionChange = ({ length, html, init }) => {
+    // // alert(1)
+    // console.log(length, html, init)
+    // if (init) {
+    //     return (descriptionLength.value = length)
+    // }
+
+    content.value = html
+    descriptionLength.value = length
+}
+const showBorder = ref(false)
+const onEditDescription = () => {
+    showBorder.value = false
+}
+
+const changeType = type => {
+    console.log(type)
+    info.value.type = type
+    store.commit('tag/setMetaType', type)
+}
+const maxContentLen = ref(10)
+</script>
+<style lang="scss" scoped>
+.hot-editor {
+    background: rgba(27, 27, 28, 0.8);
+    border-radius: 4px;
+    pointer-events: auto;
+    // backdrop-filter: blur(4px);
+    // min-width: 400px;
+    .hot-content {
+        width: 100%;
+        padding: 30px 20px;
+        .submit-ctrl {
+            margin-top: 20px;
+            display: flex;
+
+            .radio-group {
+                flex: 1;
+                display: inline-flex;
+                white-space: nowrap;
+                > .radio {
+                    margin-right: 18px;
+                    > span {
+                        margin-left: 8px;
+                    }
+                }
+            }
+
+            .submit {
+                flex: none;
+                cursor: pointer;
+            }
+        }
+        .desc-box {
+            width: 100%;
+            height: 158px;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            margin-bottom: 10px;
+            &.border {
+                border: 1px solid var(--editor-main-color);
+            }
+            .edit-box {
+                height: 128px;
+                padding: 10px;
+                box-sizing: border-box;
+            }
+        }
+        .tag-metas {
+            width: 100%;
+            height: 225px;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px;
+            overflow: hidden;
+        }
+        .desc-link {
+            position: relative;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 0px 0px 4px 4px;
+            height: 30px;
+            padding: 0 10px;
+            .iconfont {
+                font-size: 16px;
+                cursor: pointer;
+            }
+            span {
+                font-size: 12px;
+            }
+        }
+        .input {
+            margin-bottom: 10px;
+        }
+    }
+    .header {
+        height: 60px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 20px;
+        box-sizing: border-box;
+        color: #999;
+        border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+        .header-title {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        .close {
+            cursor: pointer;
+        }
+    }
+}
+</style>

+ 0 - 0
packages/app-cdfg/src/components/Tags/tag-view.vue


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.