rindy 2 years ago
parent
commit
854b2aa63c
44 changed files with 3040 additions and 10 deletions
  1. 1 0
      package.json
  2. BIN
      src/assets/img/icons/toast-error.png
  3. BIN
      src/assets/img/icons/toast-success.png
  4. BIN
      src/assets/img/icons/toast-warn.png
  5. 64 0
      src/assets/scss/_base-vars.scss
  6. 293 0
      src/assets/scss/_base.scss
  7. 18 0
      src/assets/scss/_components.scss
  8. 22 0
      src/assets/scss/components/_audio.scss
  9. 168 0
      src/assets/scss/components/_bubble.scss
  10. 79 0
      src/assets/scss/components/_button.scss
  11. 4 0
      src/assets/scss/components/_cropper.scss
  12. 98 0
      src/assets/scss/components/_dialog.scss
  13. 3 0
      src/assets/scss/components/_floating.scss
  14. 30 0
      src/assets/scss/components/_gate.scss
  15. 94 0
      src/assets/scss/components/_group.scss
  16. 73 0
      src/assets/scss/components/_guide.scss
  17. 75 0
      src/assets/scss/components/_icon.scss
  18. 673 0
      src/assets/scss/components/_input.scss
  19. 56 0
      src/assets/scss/components/_loading.scss
  20. 27 0
      src/assets/scss/components/_men-item.scss
  21. 41 0
      src/assets/scss/components/_message.scss
  22. 28 0
      src/assets/scss/components/_size-animation.scss
  23. 27 0
      src/assets/scss/components/_slide.scss
  24. 93 0
      src/assets/scss/components/_toast.scss
  25. 173 0
      src/assets/scss/components/_tree.scss
  26. 5 0
      src/assets/scss/theme.scss
  27. 53 0
      src/components/dialog/Alert.vue
  28. 63 0
      src/components/dialog/Confirm.vue
  29. 17 0
      src/components/dialog/Dialog-content.vue
  30. 27 0
      src/components/dialog/Dialog.vue
  31. 70 0
      src/components/dialog/Toast.vue
  32. 67 0
      src/components/dialog/Window.vue
  33. 113 0
      src/components/header/CopyLink.vue
  34. 8 1
      src/components/header/index.vue
  35. 35 0
      src/components/loading/Loading.vue
  36. 36 0
      src/components/loading/index.js
  37. 5 8
      src/pages/Viewer.vue
  38. 3 1
      src/pages/viewer.js
  39. 39 0
      src/utils/componentHelper.js
  40. 135 0
      src/utils/dom.js
  41. 173 0
      src/utils/index.js
  42. 15 0
      src/utils/vm.js
  43. 5 0
      src/utils/zindex.js
  44. 31 0
      yarn.lock

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
   },
   "dependencies": {
     "axios": "^0.21.1",
+    "clipboard": "^2.0.11",
     "core-js": "^3.6.5",
     "vant": "^3.6.4",
     "vue": "^3.2.26",

BIN
src/assets/img/icons/toast-error.png


BIN
src/assets/img/icons/toast-success.png


BIN
src/assets/img/icons/toast-warn.png


+ 64 - 0
src/assets/scss/_base-vars.scss

@@ -0,0 +1,64 @@
+
+:root {
+  --colors-primary-fill: 255, 255, 255;
+  --colors-primary-base-fill: 0, 200, 175;
+  --colors-primary-base: rgb(var(--colors-primary-base-fill));
+  --colors-primary-hover: #4DD8C7;
+  // --colors-primary-hover: #008B7A;
+  --colors-primary-active: #008B7A;
+  --colors-primary-click: #005046;
+  --colors-warn: #FA3F48;
+  // --colors-color: #999;
+  --colors-color: rgba(255, 255, 255, 0.7);
+  --colors-border-color: rgba(var(--colors-primary-fill), 0.16);
+  --colors-content-color: rgb(--colors-primary-fill);
+  
+  
+  --colors-normal-back: rgba(var(--colors-primary-fill), 0.1);
+  --colors-normal-base: rgba(var(--colors-primary-fill), 0.4);
+  --colors-normal-hover: rgba(var(--colors-primary-fill), 1);
+  --colors-normal-click: var(--colors-primary-click);
+
+  --colors-normal-fill-back: var(--colors-normal-back);
+  --colors-normal-fill-base: var(--colors-normal-base);
+  --colors-normal-fill-hover: var(--colors-normal-hover);
+  --colors-normal-fill-click: var(--colors-primary-click);
+
+  --colors-error-fill: 250, 63, 72;
+  
+  --small-size: 12px;
+  --medium-size: 14px;
+  --big-size: 16px;
+
+
+  // 正常
+  --color-main-normal: var(--colors-primary-base);
+  // 悬停
+  --color-main-hover: var(--colors-primary-hover);
+  // 点击
+  --color-main-focus: var(--colors-primary-click);
+
+
+  --editor-head-filter: blur(0px);
+  --editor-head-height: 50px;
+  
+  --editor-head-back: rgba(20, 20, 20, 0.86);
+
+  --editor-menu-filter: var(--editor-head-filter);
+  --editor-menu-width: 80px;
+  --editor-menu-left: 0px;
+  --editor-menu-right: 0px;
+  --editer-menu-fill: 27, 27, 28;
+  --editor-menu-back: rgba(var(--editer-menu-fill), 0.8);
+  --editor-menu-active-back: rgba(var(--colors-primary-fill), 0.06);
+  --editor-menu-color: #999;
+  --editor-menu-active: rgba(255,255,255,0.06);;
+
+
+  --editor-toolbox-top: var(--editor-head-height);
+  --editor-toolbox-left: calc(var(--editor-menu-left) + var(--editor-menu-width));
+  --editor-toolbox-width: 340px;
+  --editor-toolbox-back: var(--editor-menu-back);
+  --editor-toolbox-padding: 20px;
+  --editor-toolbar-height: 60px;
+}

+ 293 - 0
src/assets/scss/_base.scss

@@ -0,0 +1,293 @@
+/*!
+ * ress.css • v4.0.0
+ * MIT License
+ * github.com/filipelinhares/ress
+ */
+
+/* # =================================================================
+   # Global selectors
+   # ================================================================= */
+
+html {
+    box-sizing: border-box;
+    -webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS */
+    word-break: normal;
+    -moz-tab-size: 4;
+    tab-size: 4;
+}
+
+*,
+::before,
+::after {
+    background-repeat: no-repeat; /* Set `background-repeat: no-repeat` to all elements and pseudo elements */
+    box-sizing: inherit;
+    appearance: none;
+    -webkit-tap-highlight-color: rgba(255,255,255,0);
+    text-rendering: optimizeLegibility!important;
+    -webkit-font-smoothing: antialiased!important;
+}
+
+::before,
+::after {
+    text-decoration: inherit; /* Inherit text-decoration and vertical align to ::before and ::after pseudo elements */
+    vertical-align: inherit;
+}
+
+* {
+    padding: 0; /* Reset `padding` and `margin` of all elements */
+    margin: 0;
+}
+
+/* # =================================================================
+     # General elements
+     # ================================================================= */
+
+hr {
+    overflow: visible; /* Show the overflow in Edge and IE */
+    height: 0; /* Add the correct box sizing in Firefox */
+    color: inherit; /* Correct border color in Firefox. */
+}
+
+details,
+main {
+    display: block; /* Render the `main` element consistently in IE. */
+}
+
+summary {
+    display: list-item; /* Add the correct display in all browsers */
+}
+
+small {
+    font-size: 80%; /* Set font-size to 80% in `small` elements */
+}
+
+[hidden] {
+    display: none; /* Add the correct display in IE */
+}
+
+abbr[title] {
+    border-bottom: none; /* Remove the bottom border in Chrome 57 */
+    /* Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari */
+    text-decoration: underline;
+    text-decoration: underline dotted;
+}
+
+a {
+    background-color: transparent; /* Remove the gray background on active links in IE 10 */
+}
+
+a:active,
+a:hover {
+    outline-width: 0; /* Remove the outline when hovering in all browsers */
+}
+
+code,
+kbd,
+pre,
+samp {
+    font-family: monospace, monospace; /* Specify the font family of code elements */
+}
+
+pre {
+    font-size: 1em; /* Correct the odd `em` font sizing in all browsers */
+}
+
+b,
+strong {
+    font-weight: bolder; /* Add the correct font weight in Chrome, Edge, and Safari */
+}
+
+/* https://gist.github.com/unruthless/413930 */
+sub,
+sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+}
+
+sub {
+    bottom: -0.25em;
+}
+
+sup {
+    top: -0.5em;
+}
+
+table {
+    border-color: inherit; /* Correct border color in all Chrome, Edge, and Safari. */
+    text-indent: 0; /* Remove text indentation in Chrome, Edge, and Safari */
+}
+
+/* # =================================================================
+     # Forms
+     # ================================================================= */
+
+input {
+    border-radius: 0;
+}
+
+/* Replace pointer cursor in disabled elements */
+[disabled] {
+    cursor: default;
+    user-select: none;
+}
+
+[type='number']::-webkit-inner-spin-button,
+[type='number']::-webkit-outer-spin-button {
+    height: auto; /* Correct the cursor style of increment and decrement buttons in Chrome */
+}
+
+[type='search'] {
+    -webkit-appearance: textfield; /* Correct the odd appearance in Chrome and Safari */
+    outline-offset: -2px; /* Correct the outline style in Safari */
+}
+
+[type='search']::-webkit-search-decoration {
+    -webkit-appearance: none; /* Remove the inner padding in Chrome and Safari on macOS */
+}
+
+textarea {
+    overflow: auto; /* Internet Explorer 11+ */
+    resize: vertical; /* Specify textarea resizability */
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+    font: inherit; /* Specify font inheritance of form elements */
+}
+
+optgroup {
+    font-weight: bold; /* Restore the font weight unset by the previous rule */
+}
+
+button {
+    overflow: visible; /* Address `overflow` set to `hidden` in IE 8/9/10/11 */
+}
+
+button,
+select {
+    text-transform: none; /* Firefox 40+, Internet Explorer 11- */
+}
+
+/* Apply cursor pointer to button elements */
+button,
+[type='button'],
+[type='reset'],
+[type='submit'],
+[role='button'] {
+    cursor: pointer;
+    color: inherit;
+}
+
+/* Remove inner padding and border in Firefox 4+ */
+button::-moz-focus-inner,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+    border-style: none;
+    padding: 0;
+}
+
+/* Replace focus style removed in the border reset above */
+button:-moz-focusring,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+    outline: 1px dotted #ccc;
+}
+
+button,
+  html [type='button'], /* Prevent a WebKit bug where (2) destroys native `audio` and `video`controls in Android 4 */
+  [type='reset'],
+  [type='submit'] {
+    -webkit-appearance: button; /* Correct the inability to style clickable types in iOS */
+}
+
+/* Remove the default button styling in all browsers */
+button,
+input,
+select,
+textarea {
+    background-color: transparent;
+    border-style: none;
+}
+
+a:focus,
+button:focus,
+input:focus,
+select:focus,
+textarea:focus {
+    outline-width: 0;
+}
+
+/* Style select like a standard input */
+select {
+    -moz-appearance: none; /* Firefox 36+ */
+    -webkit-appearance: none; /* Chrome 41+ */
+}
+
+select::-ms-expand {
+    display: none; /* Internet Explorer 11+ */
+}
+
+select::-ms-value {
+    color: currentColor; /* Internet Explorer 11+ */
+}
+
+legend {
+    border: 0; /* Correct `color` not being inherited in IE 8/9/10/11 */
+    color: inherit; /* Correct the color inheritance from `fieldset` elements in IE */
+    display: table; /* Correct the text wrapping in Edge and IE */
+    max-width: 100%; /* Correct the text wrapping in Edge and IE */
+    white-space: normal; /* Correct the text wrapping in Edge and IE */
+    max-width: 100%; /* Correct the text wrapping in Edge 18- and IE */
+}
+
+::-webkit-file-upload-button {
+    /* Correct the inability to style clickable types in iOS and Safari */
+    -webkit-appearance: button;
+    color: inherit;
+    font: inherit; /* Change font properties to `inherit` in Chrome and Safari */
+}
+
+/* # =================================================================
+     # Specify media element style
+     # ================================================================= */
+
+img {
+    border-style: none; /* Remove border when inside `a` element in IE 8/9/10 */
+}
+
+/* Add the correct vertical alignment in Chrome, Firefox, and Opera */
+progress {
+    vertical-align: baseline;
+}
+
+/* # =================================================================
+     # Accessibility
+     # ================================================================= */
+
+/* Specify the progress cursor of updating elements */
+[aria-busy='true'] {
+    cursor: progress;
+}
+
+/* Specify the pointer cursor of trigger elements */
+[aria-controls] {
+    cursor: pointer;
+}
+
+/* Specify the unstyled cursor of disabled, not-editable, or otherwise inoperable elements */
+[aria-disabled='true'] {
+    cursor: default;
+}
+
+.disabled,
+:disabled{
+    opacity: 0.3 !important;
+    pointer-events: none !important;
+}

+ 18 - 0
src/assets/scss/_components.scss

@@ -0,0 +1,18 @@
+@import "components/loading";
+@import "components/dialog";
+@import "components/toast";
+@import "components/tree";
+@import "components/input";
+@import "components/button";
+@import "components/group";
+@import "components/floating";
+@import "components/icon";
+@import "components/size-animation";
+@import "components/men-item";
+@import "components/gate";
+@import "components/slide";
+@import "components/audio";
+@import "components/message";
+@import "components/cropper";
+@import "components/bubble";
+@import "components/guide";

+ 22 - 0
src/assets/scss/components/_audio.scss

@@ -0,0 +1,22 @@
+.audio {
+  display: inline-block;
+  cursor: pointer;
+  
+  > span {
+    --height: 18px;
+    width: 3px;
+    height: calc(var(--height) * var(--percent));
+    background: var(--colors-primary-base);
+    display: inline-block;
+    transition: height .2s linear;
+
+
+    &:not(:last-child) {
+      margin-right: 2px;
+    }
+  }
+
+  audio {
+    display: none;
+  }
+}

+ 168 - 0
src/assets/scss/components/_bubble.scss

@@ -0,0 +1,168 @@
+.bubble {
+  --arrow-width: 45px;
+  --arrow-height: 32px;
+  --back-color: rgba(27, 27, 28, 0.8);
+  --border-color-fill: 0, 0, 0;
+  --border-color: rgb(var(--border-color-fill));
+  --radius: 8px;
+  --padding: 20px 20px;
+  --bottom-left: 40px;
+  position: absolute;
+  z-index: 9;
+  transition: transform .3s ease, opacity .3s ease;
+
+
+
+  >.bubble-layer {
+    // backdrop-filter: blur(4px);
+    position: relative;
+    padding: var(--padding);
+    min-width: calc(3 * var(--arrow-width));
+    min-height: calc(3 * var(--arrow-height));
+    background: var(--back-color);
+    box-shadow: 0px 0px 10px 0px rgba(var(--border-color-fill), 0.3);
+    // border: 1px solid var(--border-color);
+    border-radius: var(--radius);
+
+    > .bubble-arr {
+      position: absolute;
+      display: block;
+      pointer-events: none;
+      margin-left: 1px;
+      z-index: 99;
+      width: 0;
+      height: 0;
+      border-style: solid;
+      border-color: transparent;
+      border-width: calc(var(--arrow-width) / 2);
+    }
+  }
+
+  &.left,
+  &.right {
+    &::after {
+      content: '';
+      position: absolute;
+      z-index: 1;
+      width: calc(var(--arrow-width) / 1.4);
+      height: calc(var(--arrow-width) / 1.3);
+      top: 50%;
+      transform: translateY(calc(-50% + 0.5px));
+    }
+
+    top: 50%;
+    transform: translateY(calc(-50% + 0.5px));
+
+
+    >.bubble-layer > .bubble-arr {
+      top: 50%;
+      transform: translateY(calc(-50% + 0.5px));
+      border-width: calc(var(--arrow-width) / 3);
+
+    }
+    
+    
+    &.fade-enter-active,
+    &.fade-leave-active {
+      transform: translateY(calc(-50% + 0.5px)) scale(1);
+      opacity: 1;
+    }
+    &.fade-enter-from,
+    &.fade-leave-to {
+      transform: translateY(calc(-50% + 0.5px)) scale(0);
+      opacity: 0;
+    }
+  }
+
+  &.left {
+    transform-origin: center right;
+    margin-right: var(--arrow-width);
+    right: 50%;
+    
+    &::after {
+      left: 100%;
+    }
+
+    >.bubble-layer > .bubble-arr {
+      left: calc(100% - 1px);
+      right: calc(-1 * var(--arrow-width) * 1.1);
+      border-left-color: var(--back-color);
+      border-left-width: calc(var(--arrow-width) / 1.3);
+    }
+  }
+
+  &.right {
+    transform-origin: center left;
+    margin-left: var(--arrow-width);
+    left: 50%;
+
+    &::after {
+      right: 100%;
+    }
+
+    >.bubble-layer > .bubble-arr {
+      right: 100%;
+      left: calc(-1 * var(--arrow-width) * 1.1);
+      border-right-color: var(--back-color);
+      border-right-width: calc(var(--arrow-width) / 1.3);
+    }
+  }
+
+
+  &.bottom,
+  &.top {
+
+    &.fade-enter-active,
+    &.fade-leave-active {
+      transform: scale(1);
+      opacity: 1;
+    }
+    &.fade-enter-from,
+    &.fade-leave-to {
+      transform: scale(0);
+      opacity: 0;
+    }
+  }
+
+  &.bottom {
+    top: 100%;
+    left: calc(50% - var(--bottom-left));
+    padding-top: var(--arrow-width);
+    transform-origin: var(--bottom-left) top;
+
+
+    >.bubble-layer > .bubble-arr {
+      border-width: calc(var(--arrow-width) / 3);
+      border-bottom-width: calc(var(--arrow-width) / 1.3);
+      border-bottom-color: var(--back-color);
+
+
+      left: 0;
+      top: calc(-1 * var(--arrow-width));
+      bottom: 100%;
+      transform: translateX(calc(-50% + 0.5px));
+      margin-left: var(--bottom-left);
+      // clip-path: polygon(0 100%, 50% 0, 100% 100%);
+    }
+  }
+
+  &.top {
+    bottom: 100%;
+    left: calc(50% - var(--bottom-left));
+    padding-bottom: var(--arrow-width);
+    transform-origin: var(--bottom-left) bottom;
+
+
+    >.bubble-layer > .bubble-arr {
+      border-width: calc(var(--arrow-width) / 3);
+      border-top-width: calc(var(--arrow-width) / 1.3);
+      border-top-color: var(--back-color);
+
+      left: 0;
+      top: 100%;
+      transform: translateX(calc(-50% + 0.5px));
+      margin-left: var(--bottom-left);
+      // clip-path: polygon(0 100%, 50% 0, 100% 100%);
+    }
+  }
+}

+ 79 - 0
src/assets/scss/components/_button.scss

@@ -0,0 +1,79 @@
+@use "sass:map";
+
+.ui-button {
+    width: 100%;
+    height: 34px;
+    border: none;
+    outline: none;
+    border-radius: 4px;
+    font-size: 14px;
+    background: none !important;
+
+    transition: all .3s ease;
+
+    .ui-button-icon {
+        margin-right: 0.6em;
+    }
+}
+
+.ui-button.customize {
+    background: none;
+    color: rgba(var(--color), 0.8);
+    border: 1px solid rgba(var(--color), 0.6);
+}
+
+.ui-button.normal{
+    color: var(--colors-color);
+    border: 1px solid var(--colors-normal-base);
+    &:hover {
+        color: var(--colors-normal-hover);
+        border: 1px solid var(--colors-normal-hover);
+    }
+    &:active {
+        color: var(--colors-normal-click);
+        border: 1px solid var(--colors-normal-click);
+    }
+}
+
+.ui-button.submit {
+    color: var( --color-main-hover);
+    border: 1px solid var( --color-main-normal);
+    background-color: var( --color-main-normal);
+    &:hover {
+        border-color: var( --color-main-hover);
+        background-color: var( --color-main-hover);
+    }
+    &:active {
+        border-color: var( --color-main-focus);
+        background-color: var( --color-main-focus);
+    }
+}
+
+.ui-button.cancel {
+    color: var( --color-main-normal);
+    border: 1px solid var( --color-main-normal);
+    &:hover {
+        border-color: var( --color-main-hover);
+    }
+    &:active {
+        border-color: var( --color-main-focus);
+    }
+}
+
+.ui-button.primary {
+    background-color: var(--colors-primary-base) !important;
+    color: var(--colors-normal-fill-hover);
+    border: none;
+    opacity: 1;
+
+    // &:active,
+    &:hover {
+        // opacity: 0.8;
+        // background: var(--colors-primary-hover) !important;
+        background: #4DD8C7 !important;
+    }
+    &:active {
+        background-color: var(--colors-primary-active) !important;
+        color: rgba(255,255,255,0.6);
+    }
+}

+ 4 - 0
src/assets/scss/components/_cropper.scss

@@ -0,0 +1,4 @@
+.cropper-layer {
+  // height: 500px;
+  // width: 500px;
+}

+ 98 - 0
src/assets/scss/components/_dialog.scss

@@ -0,0 +1,98 @@
+.ui-dialog {
+    position: fixed;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    color: #fff;
+    background-color: rgba($color: #000000, $alpha: 0.3);
+    backdrop-filter: blur(1px);
+}
+.ui-dialog__box {
+    position: relative;
+    display: inline-block;
+    min-width: 400px;
+    min-height: 100px;
+    background-color: rgba($color: #1a1a1a, $alpha: 0.8);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.7);
+    border-radius: 4px;
+    border: 1px solid #000000;
+    // backdrop-filter: blur(400px);
+    backdrop-filter: blur(4px);
+    &::after {
+        content: '';
+        position: absolute;
+        left: 1px;
+        right: 1px;
+        bottom: 1px;
+        top: 1px;
+        border: 1px solid rgba($color: #fff, $alpha: 0.1);
+        border-radius: 4px;
+        z-index: 0;
+        pointer-events: none;
+    }
+    header {
+        color: #999999;
+        padding: 0 20px;
+        height: 60px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        border-bottom: solid 1px rgba($color: #ffffff, $alpha: 0.16);
+        font-weight: bold;
+        i {
+            cursor: pointer;
+        }
+    }
+    section {
+        padding: 40px 20px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        .message {
+            text-align: center;
+            line-height: 1.7;
+        }
+    }
+    footer {
+        padding: 20px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-top: solid 1px rgba($color: #ffffff, $alpha: 0.16);
+        button {
+            width: 105px;
+            margin-left: 10px;
+            margin-right: 10px;
+        }
+    }
+}
+[is-mobile]{
+  .ui-dialog__box{
+    max-width: 90%;
+    min-width: 80%;
+    // width: 8.9333rem;
+    // min-width: 5.3333rem;
+    // min-height: 1.3333rem;
+    // section{
+    //   padding: 1.0667rem .5333rem .8rem;
+    // }
+    // footer{
+    //   padding:  0 0 .8rem;
+    //   border-top: none;
+    // }
+    // header{
+    //   display: none;
+    // }
+  } 
+  section {
+ 
+    .message {
+        text-align: left;
+    }
+}
+} 

+ 3 - 0
src/assets/scss/components/_floating.scss

@@ -0,0 +1,3 @@
+.ui-floating {
+  position: absolute;
+}

+ 30 - 0
src/assets/scss/components/_gate.scss

@@ -0,0 +1,30 @@
+.ui-gate-layer {
+  --len: 1;
+  --current: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden !important;
+
+  .ui-gate-slides {
+    --content-width: calc(var(--len) * 100%);
+    --item-width: calc(100% / var(--len));
+
+    width: var(--content-width);
+    height: 100%;
+    transition: transform .3s ease;
+    transform: translateX(calc(-1 * var(--current) * var(--item-width)));
+
+    .ui-gate-content {
+      width: var(--item-width);
+      height: 100%;
+      float: left;
+      opacity: 0;
+      transition: opacity .3s ease;
+
+      &.active {
+        opacity: 1;
+      }
+    }
+  }
+
+}

+ 94 - 0
src/assets/scss/components/_group.scss

@@ -0,0 +1,94 @@
+$padding: 14px;
+
+.ui-group {
+  &:not(:last-child) {
+    margin-bottom: 20px;
+  }
+
+
+  &.control {
+    > .group-title {
+
+      .group-icon {
+        transition: transform .1s ease;
+        cursor: pointer;
+
+        &.show {
+          transform: rotateZ(180deg);
+        }
+      }
+    }
+    > div.group-title .group-icon.show {
+      transform: translateY(-50%) rotateZ(180deg);
+    }
+
+    .group-content {
+      overflow: hidden;
+      &.ready {
+        transition: max-height .1s ease;
+      }
+    }
+  }
+
+  > .group-title {
+    font-size: var( --big-size);
+    margin-bottom: $padding;
+    color: var(--colors-color);
+
+    .group-icon {
+      display: inline-flex;
+      align-items: center;
+    }
+  }
+
+  > div.group-title {
+    position: relative;
+
+    .group-icon {
+      position: absolute;
+      width: 14px;
+      right: 0;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  > h3.group-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .border-bottom {
+    padding-bottom: $padding;
+    border-bottom: 1px solid var(--colors-border-color);
+  }
+  .border-top {
+    padding-top: $padding;
+    border-top: 1px solid var(--colors-border-color);
+  }
+
+  > .group-content {
+    font-size: var( --medium-size);
+
+    &.border-bottom {
+      margin-bottom: 0;
+    }
+    &.border-top {
+      margin-top: 0;
+    }
+  }
+
+}
+
+.group-option {
+  &:not(:last-child) {
+    margin-bottom: $padding;
+  }
+
+  > .group-option-label {
+    display: block;
+    margin-bottom: 10px;
+    color: var(--colors-content-color);
+  }
+}

+ 73 - 0
src/assets/scss/components/_guide.scss

@@ -0,0 +1,73 @@
+.guide {
+  position: relative;
+
+  &:not(.floating-mode) {
+    &.top {
+      transform: translateY(-100%);
+    }
+  
+    .bubble {
+      --arrow-width: 14px;
+      --arrow-height: 10px;
+      --padding: 10px;
+      --bottom-left: 20px;
+  
+      .bubble-layer {
+        min-height: auto;
+        min-width: auto;
+        padding-right: 30px;
+  
+        .guide-close {
+          position: absolute;
+          right: 10px;
+          top: 10px;
+          // margin-top: 2px;
+          font-size: 12px;
+        }
+      }
+    }
+  
+    .guide-bubble {
+      .default-msg {
+        white-space: nowrap;
+      }
+    }
+  }
+}
+
+.guide-floating {
+  color: #fff;
+  font-size: 14px;
+
+  &.top {
+    transform: translateY(-100%);
+  }
+
+  .bubble {
+    --arrow-width: 14px;
+    --arrow-height: 10px;
+    --padding: 10px;
+    --bottom-left: 20px;
+    position: static;
+
+    .bubble-layer {
+      min-height: auto;
+      min-width: auto;
+      padding-right: 30px;
+
+      .guide-close {
+        position: absolute;
+        right: 10px;
+        top: 10px;
+        // margin-top: 2px;
+        font-size: 12px;
+      }
+    }
+  }
+
+  .guide-bubble {
+    .default-msg {
+      white-space: nowrap;
+    }
+  }
+}

+ 75 - 0
src/assets/scss/components/_icon.scss

@@ -0,0 +1,75 @@
+.ui-kankan-icon.iconfont {
+  color: currentColor;
+  font-size: 1em;
+
+  &.small {
+    font-size: 12px;
+  }
+
+  &.medium {
+    font-size: 16px;
+  }
+
+  &.big {
+    font-size: 20px;
+  }
+}
+
+.icon {
+  position: relative;
+  
+
+  .tip {
+    color: rgba(255,255,255,1);
+    position: absolute;
+    transform-origin: top center;
+    background: #000000;
+    border-radius: 4px;
+    opacity: 0;
+    padding: 10px;
+    margin: 10px;
+    font-size: 12px;
+    transition: opacity .3s ease;
+    pointer-events: none;
+    white-space: nowrap;
+  }
+
+  &:hover {
+    z-index: 999;
+    .tip {
+      opacity: 0.8;
+    }
+  }
+}
+
+  
+.tip-h-right .tip{
+  right: 0;
+  margin-right: 0;
+}
+
+.tip-h-left .tip {
+  left: 0;
+  margin-left: 0;
+}
+
+.tip-h-center .tip {
+  left: 50%;
+  transform: translateX(-50%);
+  margin-left: 0;
+  margin-right: 0;
+  z-index: 10;
+}
+
+.tip-v-top .tip {
+  bottom: 100%;
+}
+
+.tip-v-center .tip {
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.tip-v-bottom .tip {
+  top: 100%;
+}

+ 673 - 0
src/assets/scss/components/_input.scss

@@ -0,0 +1,673 @@
+.ui-input {
+    display: inline-flex;
+    align-items: center;
+    --base-border-color: rgba(255, 255, 255, 0.2);
+    --colors-content-color: #fff;
+    height: 100%;
+    &.error {
+        position: relative;
+        --colors-primary-base: #fa3f48;
+        --base-border-color: #fa3f48;
+        .error-msg {
+            top: 100%;
+            position: absolute;
+            color: var(--colors-primary-base);
+            margin-top: 5px;
+        }
+    }
+
+    &.require {
+        position: relative;
+
+        &::before {
+            content: '*';
+            position: absolute;
+            top: 50%;
+            transform: translateY(-50%);
+            right: 100%;
+            margin-right: 2px;
+            color: #fa3f48;
+            line-height: 1.5em;
+        }
+    }
+
+    .input {
+        position: relative;
+        align-items: center;
+        display: inline-flex;
+
+        .input-div,
+        textarea,
+        input {
+            width: 100%;
+            height: 100%;
+            outline: none;
+            border: none;
+            font-size: 14px;
+            color: var(--colors-content-color);
+            padding-left: 4px;
+            resize: none;
+
+            & + .replace {
+                position: absolute;
+                z-index: 1;
+            }
+
+            &.replace-input {
+                opacity: 0;
+                cursor: pointer;
+            }
+        }
+
+        .pre-icon {
+            position: absolute;
+            z-index: 1;
+        }
+    }
+
+    .label {
+        cursor: pointer;
+        margin-left: 7px;
+    }
+
+    .radio,
+    .checkbox {
+        width: 16px;
+        height: 16px;
+
+        input {
+            & + .replace {
+                color: var(--colors-color);
+                border: 1px solid currentColor;
+                background-color: var(--colors-normal-back);
+                left: 0;
+                top: 0;
+                right: 0;
+                bottom: 0;
+                pointer-events: none;
+                transition: all 0.1s linear;
+            }
+
+            &:focus + .replace {
+                border-color: var(--colors-primary-base);
+            }
+
+            &:checked + .replace {
+                color: var(--colors-primary-base);
+            }
+        }
+    }
+
+    .checkbox input {
+        & + .replace {
+            border-radius: 4px;
+            .icon {
+                position: absolute;
+                left: 50%;
+                top: 50%;
+                transform: translate(-50%, -50%) scale(0);
+                transition: all 0.1s linear;
+            }
+        }
+
+        &:checked + .replace {
+            .icon {
+                transform: translate(-50%, -50%) scale(1);
+            }
+        }
+    }
+
+    .radio input {
+        & + .replace {
+            border-radius: 50%;
+            &::after {
+                content: '';
+                border-radius: 50%;
+                position: absolute;
+                left: 50%;
+                top: 50%;
+                transform: translate(-50%, -50%) scale(0);
+                transition: all 0.1s linear;
+                width: 60%;
+                height: 60%;
+                background-color: currentColor;
+            }
+        }
+
+        &:checked + .replace::after {
+            transform: translate(-50%, -50%) scale(1);
+        }
+    }
+
+    .text {
+        width: 100%;
+        height: 100%;
+        border-radius: 4px;
+
+        input {
+            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;
+
+            &:focus {
+                border-color: var(--colors-primary-base);
+            }
+            &.warn {
+                border-color: var(--colors-warn);
+                &:focus {
+                    border-color: var(--colors-warn);
+                }
+            }
+
+            &::placeholder {
+                color: var(--colors-color);
+            }
+        }
+
+        &.pre-suffix {
+            input {
+                padding-left: 30px;
+            }
+            .pre-icon {
+                left: 10px;
+                top: 50%;
+                transform: translateY(-50%);
+            }
+        }
+
+        &.right input {
+            text-align: right;
+        }
+
+        &.suffix {
+            input {
+                padding-right: 60px;
+            }
+            .retouch {
+                position: absolute;
+                right: 10px;
+                top: 50%;
+                transform: translateY(-50%);
+                z-index: 10;
+                cursor: pointer;
+            }
+
+            .len {
+                font-size: var(--small-size);
+                color: rgba(var(--colors-primary-fill), 1);
+
+                span {
+                    color: var(--colors-primary-base);
+                }
+            }
+        }
+
+        &.ready {
+            .retouch,
+            input {
+                transition: all 0.1s linear;
+            }
+        }
+    }
+
+    .textarea {
+        width: 100%;
+        height: 100%;
+        min-height: 50px;
+
+        > .replace {
+            border-radius: 4px;
+            left: 0;
+            top: 0;
+            right: 0;
+            bottom: 0;
+            pointer-events: none;
+            background: var(--colors-normal-back);
+            border: 1px solid var(--base-border-color);
+            transition: border 0.3s ease;
+        }
+
+        .input-div {
+            overflow-y: auto;
+
+            a {
+                color: var(--color-main-normal);
+            }
+        }
+        .input-div,
+        textarea {
+            height: 100%;
+            width: 100%;
+            padding: 10px;
+
+            &:focus + .replace {
+                border-color: var(--colors-primary-base);
+            }
+
+            &::placeholder {
+                color: var(--colors-color);
+            }
+        }
+
+        &.right .input-div,
+        &.right textarea {
+            text-align: right;
+        }
+
+        &.suffix {
+            --bar-height: 30px;
+
+            .input-div,
+            textarea {
+                margin-bottom: var(--bar-height);
+                height: calc(100% - var(--bar-height));
+            }
+
+            > .retouch {
+                position: absolute;
+                right: 0;
+                left: 0;
+                bottom: 0;
+                background-color: rgba(var(--colors-primary-fill), 0.1);
+                height: var(--bar-height);
+                display: flex;
+                padding: 0 10px;
+                align-items: center;
+                justify-content: space-between;
+            }
+
+            .len {
+                justify-self: end;
+                font-size: var(--small-size);
+                color: rgba(var(--colors-primary-fill), 1);
+
+                span {
+                    color: var(--colors-primary-base);
+                }
+            }
+        }
+    }
+
+    .number {
+        input {
+            -moz-appearance: textfield;
+        }
+        input::-webkit-inner-spin-button,
+        input::-webkit-outer-spin-button {
+            -webkit-appearance: none;
+            margin: 0;
+        }
+
+        .ctrls {
+            position: absolute;
+            inset: 2px 0;
+            width: 8px;
+            .icon {
+                position: absolute;
+                right: 0;
+
+                &.up {
+                    bottom: 0;
+                }
+                &.down {
+                    top: 0;
+                }
+            }
+        }
+
+        &.ctrl.suffix input {
+            padding-right: 20px;
+            z-index: 1;
+        }
+    }
+
+    .select {
+        input {
+            cursor: pointer;
+            &.ui-text {
+                padding-right: 0;
+            }
+            &.icon {
+                padding-right: 30px;
+            }
+        }
+
+        &.focus {
+            input {
+                border-color: var(--colors-primary-base);
+            }
+            .retouch {
+                transform: translateY(-50%) rotateZ(180deg);
+            }
+        }
+    }
+
+    .range {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        --height: 6px;
+        --slideSize: calc(var(--height) + 8px);
+
+        .range-content {
+            flex: 1;
+            background-color: var(--colors-normal-back);
+            position: relative;
+            cursor: pointer;
+        }
+
+        .range-content::before,
+        .range-content {
+            height: var(--height);
+            border-radius: calc(var(--height) / 2);
+        }
+
+        .range-content::before,
+        .range-content .range-slide {
+            content: '';
+            position: absolute;
+        }
+
+        .range-content::before {
+            pointer-events: none;
+            left: 0;
+            top: 0;
+            width: var(--percentage);
+            background-color: var(--colors-primary-base);
+        }
+
+        .range-locus {
+            width: calc(100% - var(--slideSize));
+            height: var(--height);
+            position: relative;
+
+            .range-slide {
+                cursor: pointer;
+                height: var(--slideSize);
+                width: var(--slideSize);
+                top: 50%;
+                left: var(--percentage);
+                transform: translateY(-50%);
+                background-color: var(--colors-content-color);
+                border-radius: 50%;
+                .tips{
+                  position: absolute;
+                  background: rgba(0, 0, 0, 0.6);
+                  padding: 4px 6px;
+                  border-radius: 4px;
+                  bottom: 120%;
+                  left: 50%;
+                  -webkit-transform: translateX(-50%);
+                  transform: translateX(-50%);
+                  display: none;
+                  font-size: 12px;
+                }
+                &:hover{
+                  .tips{
+                   display: block;
+                  }
+                }
+            }
+        }
+
+        .range-text {
+            margin-left: 10px;
+            width: 65px;
+        }
+
+        .animation {
+            &.range-content::before,
+            .range-slide {
+                transition: all 0.1s linear;
+            }
+        }
+    }
+
+    .switch {
+        --height: 24px;
+        width: 50px;
+        height: var(--height);
+
+        .replace {
+            background-color: rgba(255, 255, 255, 0.3);
+            left: 0;
+            top: 0;
+            right: 0;
+            bottom: 0;
+            border-radius: calc(var(--height) / 2);
+            pointer-events: none;
+            position: relative;
+            transition: background-color 0.3s ease;
+
+            &::after {
+                content: '';
+                --padding: 3px;
+                --size: calc(var(--height) - var(--padding) * 2);
+                position: absolute;
+                width: var(--size);
+                height: var(--size);
+                top: var(--padding);
+                background: var(--colors-content-color);
+                border-radius: 50%;
+                left: var(--padding);
+                transition: left 0.3s ease;
+            }
+        }
+
+        input:checked + .replace {
+            background-color: var(--colors-primary-base);
+
+            &::after {
+                left: calc(100% - var(--size) - var(--padding));
+            }
+        }
+    }
+
+    .file {
+        width: 100%;
+        height: 100%;
+
+        input {
+            cursor: pointer;
+            opacity: 0;
+        }
+        .use-replace {
+            position: absolute;
+        }
+        .use-replace,
+        .replace {
+            left: 0;
+            right: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            pointer-events: none;
+        }
+
+        &:not(.valuable) {
+            .replace {
+                top: 0;
+                bottom: 0;
+                background: rgba(var(--colors-primary-fill), 0.1);
+                border-radius: 4px;
+                border: 1px solid rgba(var(--colors-primary-fill), 0.2);
+                // position: relative;
+
+                .placeholder {
+                    text-align: center;
+                    max-width: 80%;
+
+                    p:not(:last-child) {
+                        margin-bottom: 10px;
+                    }
+
+                    .bottom {
+                        font-size: 12px;
+                        color: rgba(255, 255, 255, 0.3);
+                        width: 90%;
+                        position: absolute;
+                        bottom: 10px;
+                        left: 50%;
+                        transform: translateX(-50%);
+                        text-align: left;
+                    }
+                }
+            }
+
+            input {
+                width: 100%;
+                height: 100%;
+
+                &:focus + .replace {
+                    border-color: var(--colors-primary-base);
+                }
+            }
+        }
+        &.valuable {
+            background: rgba(var(--colors-primary-fill), 0.1);
+            border-radius: 4px;
+            border: 1px solid rgba(var(--colors-primary-fill), 0.2);
+
+            input,
+            .replace {
+                position: absolute;
+                bottom: 0;
+                background: linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.5) 100%);
+                height: 32px;
+                line-height: 32px;
+
+                .tj {
+                    position: absolute;
+                    right: 10px;
+                    top: 0;
+                    bottom: 0;
+                    display: flex;
+                    align-items: center;
+                    font-size: 10px;
+
+                    > span {
+                        color: var(--colors-primary-base);
+                        margin-right: 4px;
+                    }
+                }
+            }
+
+            .icons {
+                position: absolute;
+                right: 10px;
+                top: 0;
+
+                span {
+                    width: 24px;
+                    height: 24px;
+                    border-radius: 50%;
+                    background: rgba(0, 0, 0, 0.3);
+                    font-size: 12px;
+                    color: rgba(255, 255, 255, 0.6);
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    margin-top: 10px;
+                }
+            }
+        }
+    }
+
+    .search {
+        .retouch {
+            transform: translateY(-50%) !important;
+
+            .clear {
+                // background-color: rgba(255,255,255,.3);
+                font-size: 16px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                color: rgba(255, 255, 255, 0.6);
+                border-radius: 50%;
+                cursor: pointer;
+            }
+        }
+    }
+
+    .color {
+        &.default {
+            input {
+                opacity: 1;
+                border: inherit;
+                outline: inherit;
+            }
+        }
+        .replace {
+            pointer-events: none;
+        }
+    }
+}
+
+.select-float {
+    transition: transform 0.3s ease, opacity 0.3s ease;
+    transform-origin: center top;
+
+    &:not(.show) {
+        transform: scale(1, 0);
+        opacity: 0;
+        pointer-events: none;
+    }
+    &.show {
+        transform: scale(1, 1);
+        opacity: 1;
+    }
+}
+
+.select-replace {
+    --colors-content-color: #fff;
+    list-style: none;
+    max-height: 288px;
+    background: rgba(26, 26, 26, 0.8);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3), inset 0 0 1px rgb(255 255 255 / 90%);
+    backdrop-filter: blur(4px);
+    border-radius: 4px;
+    overflow-y: auto;
+    color: var(--colors-content-color);
+    &.hide-scroll{
+      max-height: 100%;
+    }
+    li {
+        padding: 10px 10px;
+        font-size: 14px;
+
+        &.un-data {
+            padding: 20px;
+            color: rgba(255, 255, 255, 0.3);
+            pointer-events: none;
+        }
+        &.active {
+            background: var(--colors-normal-back);
+            color: var(--colors-primary-base);
+        }
+
+        &:not(.active):hover {
+            cursor: pointer;
+            background-color: var(--colors-primary-base);
+        }
+    }
+}
+
+.is-hidden {
+    position: absolute;
+    left: -10000px;
+    top: -10000px;
+}
+.no-vip {
+    .file {
+        input {
+            pointer-events: none;
+        }
+        label {
+            pointer-events: none;
+        }
+    }
+}

+ 56 - 0
src/assets/scss/components/_loading.scss

@@ -0,0 +1,56 @@
+.ui-loading {
+    position: absolute;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    background-color: rgba($color: #000000, $alpha: 0.3);
+    --width: 15px;
+    --color: #fff;
+}
+.ui-loading__box {
+    position: relative;
+    width: 100px;
+    height: 100px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .default {
+        div {
+            width: var(--width);
+            height: var(--width);
+            background: var(--color);
+            border-radius: 50%;
+            display: inline-block;
+            margin-left: calc(var(--width));
+        }
+        div:nth-child(1) {
+            animation: ui-loading-default 1s -0.5s linear infinite;
+        }
+        div:nth-child(2) {
+            animation: ui-loading-default 1s -0.25s linear infinite;
+        }
+        div:nth-child(3) {
+            animation: ui-loading-default 1s 0s linear infinite;
+        }
+    }
+}
+
+@keyframes ui-loading-default {
+    0% {
+        transform: scale(1);
+        opacity: 1;
+    }
+    50% {
+        transform: scale(0.5);
+        opacity: 0.5;
+    }
+    100% {
+        transform: scale(1);
+        opacity: 0.8;
+    }
+}

+ 27 - 0
src/assets/scss/components/_men-item.scss

@@ -0,0 +1,27 @@
+
+
+.ui-menu-item {
+  height: 100%;
+  width: 100%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  color: var(--editor-men-color);
+  transition: all .3s ease;
+
+  span{
+    width: 100%;
+    text-align: center;
+    margin-top: 6px;
+    overflow: hidden;
+  }
+  &:hover{
+      color: var( --color-main-hover);
+  }
+  &.active{
+      color: var( --color-main-normal);
+      background-color: var(--editor-menu-active);
+  }
+}

+ 41 - 0
src/assets/scss/components/_message.scss

@@ -0,0 +1,41 @@
+.ui-message {
+  position: absolute;
+  left: 50%;
+  top: 70px;
+  height: 40px;
+  padding: 0 20px;
+  background: rgba(20,20,20,0.7);
+  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+  border-radius: 4px;
+  border: 1px solid #000000;
+  backdrop-filter: blur(4px);
+  color: #fff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  transition: all .5s ease;
+  opacity: 1;
+  transform: translateX(-50%);
+
+  .icon {
+    font-size: 16px;
+    margin-right: 10px;
+  }
+
+  &.success .icon {
+    color: #43c665;
+  }
+  &.warning .icon {
+    color: #f49b42;
+  }
+  &.error .icon {
+    color: #f34447;
+  }
+
+
+  &.fade-enter-from,
+  &.fade-leave-to {
+    opacity: 0;
+    transform: translateX(-50%) translateY(-100%);
+  }
+}

+ 28 - 0
src/assets/scss/components/_size-animation.scss

@@ -0,0 +1,28 @@
+.ui-size-animation {
+
+  &.height {
+    overflow: hidden;
+  }
+
+  &:not(.ready) {
+    opacity: 0;
+  }
+  &.ready {
+    transition: max-height .2s ease;
+  }
+
+  &.scale {
+    transform-origin: center top;
+    transform: scaleY(0); 
+
+    &.show {
+      transform: scaleY(1); 
+    }
+
+    &.ready {
+      transition: max-height .2s ease,
+        transform .2s ease;
+    }
+  }
+
+}

+ 27 - 0
src/assets/scss/components/_slide.scss

@@ -0,0 +1,27 @@
+
+.ui-slide {
+  position: relative;
+  height: 100%;
+
+  .right,
+  .left {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+    background-color: rgba(0,0,0,0.3);
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+  }
+  .right {
+    right: 10px;
+  }
+
+  .left {
+    left: 10px;
+  }
+}

+ 93 - 0
src/assets/scss/components/_toast.scss

@@ -0,0 +1,93 @@
+.ui-toast {
+    position: fixed;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    top: 30px;
+    left: 0;
+    right: 0;
+    min-width: 100px;
+    height: 100px;
+    overflow: hidden;
+    pointer-events:none;
+}
+.ui-toast__box {
+    color: #fff;
+    font-size: 14px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba($color: #1a1a1a, $alpha: 0.8);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.7);
+    border-radius: 4px;
+    border: 1px solid #000000;
+    padding: 8px 20px;
+    pointer-events: all;
+
+    &.fixed,
+    &.success,
+    &.error,
+    &.warn{
+        i{
+            display:inline-block;
+        }
+    }
+    &.success {
+        .icon {
+            background-image: url('#{$img-base-path}icons/toast-success.png');
+        }
+    }
+    &.error {
+        .icon {
+            background-image: url('#{$img-base-path}icons/toast-error.png');
+        }
+    }
+    &.warn {
+        .icon {
+            background-image: url('#{$img-base-path}icons/toast-warn.png');
+        }
+    }
+    &::after {
+        content: '';
+        position: absolute;
+        left: 1px;
+        right: 1px;
+        bottom: 1px;
+        top: 1px;
+        border: 1px solid rgba($color: #fff, $alpha: 0.1);
+        border-radius: 4px;
+        z-index: 0;
+        pointer-events: none;
+    }
+
+    // >i{
+    //     display: none;
+    // }
+
+    .icon{
+        margin-right: 10px;
+        font-size: 0;
+        width: 16px;
+        height: 16px;
+        background-repeat: no-repeat;
+        background-position: center center;
+        background-size: contain;
+    }
+    .close{
+        cursor: pointer;
+        font-size: 14px;
+        margin-left: 20px;
+    }
+}
+.ui-toast__msg{
+  display: flex;
+  align-items: center;
+  img{
+    width:24px;
+    height: 24px;
+  }
+}
+body[is-mobile] .ui-toast__box {
+  max-width: 80%;
+}

+ 173 - 0
src/assets/scss/components/_tree.scss

@@ -0,0 +1,173 @@
+.ui-tree {
+  /* 收缩控件大小 */
+  --ctrl-size: 14px;
+  /* 左边留白大小 */
+  --padding-size: 20px;
+  // 底部margind大小
+  --margin-size: 20px;
+  --border-style: dashed;
+  --border-width: 1px;
+  
+  // 用于计算,防止样式覆盖而失效
+  --calc-size: var(--padding-size);
+  --half-ctrl: calc(var(--ctrl-size) / 2);
+  --half-margin: calc(var(--margin-size) / 2);
+
+  &.flat {
+    .ui-tree-item {
+      --padding-size: 20px;
+      // padding-left: 0;
+      margin-left: calc(-1 * var(--padding-size));
+    }
+  }
+
+  color: var(--colors-normal-base);;
+
+  .ui-tree-item{
+    list-style: none;
+    padding-left: var(--padding-size);
+    position: relative;
+
+    &.un-children {
+      --padding-size: 0;
+    }
+  }
+
+  .ui-tree-content {
+    margin-bottom: var(--margin-size);
+    position: relative;
+  }
+
+  .ui-tree-ctrl {
+    position: absolute;
+    width: var(--ctrl-size);
+    height: var(--ctrl-size);
+    left: calc(var(--padding-size) * -1);
+    top: 50%;
+    transform: translateY(-50%);
+    border: 1px solid currentColor;
+    line-height: var(--ctrl-size);
+    border-radius: calc(var(--ctrl-size) / 6);
+    cursor: pointer;
+
+    &::before,
+    &::after {
+      content: '';
+      height: 1px;
+      width: 60%;
+      background-color: currentColor;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+    }
+
+    &::before {
+      transform: translate(-50%, -50%);
+    }
+
+    &::after {
+      transform: translate(-50%, -50%) rotateZ(90deg);
+      transition: transform .3s ease;
+    }
+
+    &.open::after {
+      transform: translate(-50%, -50%) rotateZ(90deg) scale(0);
+    }
+  }
+  .ui-tree-item-child {
+    --offset: calc(var(--calc-size) * 2);
+    width: calc(100% + var(--offset));
+    padding-left: var(--offset);
+    margin-left: calc(-1 * var(--offset));
+    padding-top: var(--margin-size);
+    margin-top: calc(-1 * var(--margin-size));
+    // overflow: hidden;
+    transition: all .3s ease !important;
+  }
+
+  &.stroke {
+    --slideWidth: calc(var(--padding-size) - var(--half-ctrl));
+
+    .ui-tree-auxiliary::after,
+    .ui-tree-auxiliary::before,
+    .ui-tree-item::before,
+    .ui-tree-content::after {
+      content: '';
+      position: absolute;
+    }
+
+    .ui-tree-content::after {
+      left: calc(var(--padding-size) * -1);
+      width: var(--slideWidth);
+      border-bottom: var(--border-width) var(--border-style) currentColor;
+      top: 50%;
+      transform: translateX(-100%) translateY(-50%);
+    }
+
+    .ui-tree-auxiliary::after,
+    .ui-tree-auxiliary::before,
+    .ui-tree-item.un-children:last-child::before,
+    .ui-tree-item:not(:last-child):before {
+      border-left: var(--border-width) var(--border-style) currentColor;
+      transform: translateX(calc(var(--slideWidth) * -1));
+      left: 0;
+    }
+
+    .ui-tree-item:not(:last-child):before  {
+      top: 0;
+      bottom: calc(-1 * var(--half-ctrl));
+    }
+
+    .ui-tree-item.un-children:last-child::before {
+      top: 0;
+      bottom: calc(50% + var(--half-margin));
+    }
+
+    .ui-tree-auxiliary::before,
+    .ui-tree-auxiliary::after {
+      height: calc(50% - var(--half-ctrl));
+      transition: height .3s ease
+    }
+
+    .ui-tree-auxiliary::before {
+      height: calc(50% - var(--half-ctrl));
+      left: calc(var(--padding-size) * -1);
+      top: var(--half-ctrl);
+    }
+
+    .ui-tree-auxiliary::after {
+      height: calc(50% - var(--half-ctrl) + var(--margin-size));
+      bottom: calc(-1 * var(--margin-size));
+    }
+    
+    .first.ui-tree-auxiliary::before {
+      display: none;
+    }
+
+    .alone .ui-tree-auxiliary::before {
+      display: inherit;
+      height: 50%;
+      top: 0;
+    }
+
+    &:not(.children) > {
+      .ui-tree-item > .ui-tree-content > .ui-tree-auxiliary::before,
+      .ui-tree-item::before,
+      .ui-tree-item > .ui-tree-content::after {
+        display: none;
+      }
+    }
+
+    .ui-tree-item.put {
+      .ui-tree-item-child {
+        padding-top: 0;
+        margin-top: 0;
+      }
+
+      > .ui-tree-content .ui-tree-auxiliary::after {
+        height: 0;
+      }
+    }
+  }
+
+}

+ 5 - 0
src/assets/scss/theme.scss

@@ -0,0 +1,5 @@
+$img-base-path: '~@/assets/img/';
+
+@import "base";
+@import "base-vars";
+@import "components";

+ 53 - 0
src/components/dialog/Alert.vue

@@ -0,0 +1,53 @@
+<template>
+    <ui-dialog>
+        <template v-slot:header>
+            <span>{{ title }}</span>
+            <i v-if="showCloseIcon" class="iconfont icon-close" @click="close"></i>
+        </template>
+        <div v-html="content"></div>
+        <template v-slot:footer v-if="showFooter">
+            <ui-button type="submit" @click="close">{{ okText }}</ui-button>
+        </template>
+    </ui-dialog>
+</template>
+<script>
+import { defineComponent } from 'vue'
+import { isFunction, omit } from '../../utils'
+export default defineComponent({
+    name: 'ui-alert',
+    props: {
+        showCloseIcon: {
+            type: Boolean,
+            default: true,
+        },
+        showFooter: {
+            type: Boolean,
+            default: true,
+        },
+        title: {
+            type: String,
+            default: '提示',
+        },
+        okText: {
+            type: String,
+            default: '确定',
+        },
+        func: Function,
+        content: String,
+        destroy: Function,
+    },
+    setup: function (props, ctx) {
+        debugger
+        const close = () => {
+            if (isFunction(props.func) && props.func() === false) {
+                return
+            }
+            isFunction(props.destroy) && props.destroy()
+        }
+        return {
+            ...omit(props, 'destroy', 'func'),
+            close,
+        }
+    },
+})
+</script>

+ 63 - 0
src/components/dialog/Confirm.vue

@@ -0,0 +1,63 @@
+<template>
+    <ui-dialog>
+        <template v-slot:header>
+            <span>{{ title }}</span>
+            <i class="iconfont icon-close" @click="close('no')"></i>
+        </template>
+        <template v-if="$slots.content">
+            <slot name="content" />
+        </template>
+        <template v-else>
+            <div class="message" v-html="content"></div>
+        </template>
+        <template v-slot:footer v-if="!hideFoot">
+            <ui-button v-if="!single" type="cancel" @click="close('no')">{{ noText }}</ui-button>
+            <ui-button type="primary" @click="close('ok')">{{ okText }}</ui-button>
+        </template>
+    </ui-dialog>
+</template>
+<script>
+import { defineComponent } from 'vue'
+import { isFunction, omit } from '../../utils'
+
+export default defineComponent({
+    name: 'ui-confirm',
+    props: {
+        title: {
+            type: String,
+            default: '提示',
+        },
+        okText: {
+            type: String,
+            default: '确定',
+        },
+        noText: {
+            type: String,
+            default: '取消',
+        },
+        single: {
+            type: Boolean,
+            default: false,
+        },
+        hideFoot: {
+            type: Boolean,
+            default: false,
+        },
+        func: Function,
+        content: String,
+        destroy: Function,
+    },
+    setup: function (props, ctx) {
+        const close = result => {
+            if (isFunction(props.func) && props.func(result) === false) {
+                return
+            }
+            isFunction(props.destroy) && props.destroy()
+        }
+        return {
+            ...omit(props, 'destroy', 'func'),
+            close,
+        }
+    },
+})
+</script>

+ 17 - 0
src/components/dialog/Dialog-content.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class="ui-dialog__box">
+      <header v-if="$slots.header">
+          <slot name="header"></slot>
+      </header>
+      <section>
+          <slot></slot>
+      </section>
+      <footer v-if="$slots.footer">
+          <slot name="footer"></slot>
+      </footer>
+  </div>
+</template>
+
+<script>
+export default { name: 'ui-dialog-content' }
+</script>

+ 27 - 0
src/components/dialog/Dialog.vue

@@ -0,0 +1,27 @@
+<template>
+    <teleport to="body">
+        <div class="ui-dialog" :style="{ zIndex: zIndex }" v-if="show">
+            <dialog-content>
+                <template v-for="(slot, name) in $slots" v-slot:[name]="raw">
+                    <slot :name="name" v-bind="raw" />
+                </template>
+            </dialog-content>
+        </div>
+    </teleport>
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+import zindex from '../../utils/zindex'
+import DialogContent from './Dialog-content.vue'
+export default defineComponent({
+    name: "ui-dialog",
+    setup: function (props, ctx) {
+        const show = ref(true);
+        return {
+            show,
+            zIndex: zindex(),
+        };
+    },
+    components: { DialogContent }
+})
+</script>

+ 70 - 0
src/components/dialog/Toast.vue

@@ -0,0 +1,70 @@
+<template>
+    <teleport to="body">
+        <transition name="slide-down" mode="out-in" appear>
+            <div class="ui-toast" :style="{ zIndex: zIndex }" v-if="show">
+                <div class="ui-toast__box" :class="[type]">
+                    <i v-if="type !== 'fixed' && type" class="icon"></i>
+                    <div class="ui-toast__msg" v-html="content"></div>
+                    <i class="iconfont icon-close close" @click="close" v-if="showClose"></i>
+                </div>
+            </div>
+        </transition>
+    </teleport>
+</template>
+<script>
+import { defineComponent, onMounted, nextTick, ref } from 'vue'
+import zindex from '../../utils/zindex'
+export default defineComponent({
+    name: 'ui-toast',
+    props: {
+        type: String,
+        delay: Number,
+        content: String,
+        destroy: Function,
+        close: Function,
+        showClose: Boolean,
+    },
+    setup: function (props, ctx) {
+        const show = ref(true)
+        const close = () => {
+            show.value = false
+            nextTick(() => {
+                if (typeof props.close == 'function') {
+                    props.close()
+                }
+                typeof props.destroy === 'function' && props.destroy()
+            })
+        }
+
+        if (props.type !== 'fixed') {
+            setTimeout(() => close(), props.delay || 3000)
+        }
+        return {
+            show,
+            type: props.type,
+            close,
+            content: props.content,
+            zIndex: zindex(),
+        }
+    },
+})
+</script>
+<style lang="scss" scoped>
+.slide-down-enter-active,
+.slide-down-leave-active {
+    will-change: transform;
+    transition: all 0.35s ease-in-out;
+}
+.slide-down-enter-from {
+    opacity: 0;
+    transform: translate3d(0, -100%, 0);
+}
+.slide-down-enter {
+    opacity: 1;
+    transform: translate3d(-50%, 100%, 0);
+}
+.slide-down-leave-active {
+    opacity: 0;
+    transform: translate3d(0, -100%, 0);
+}
+</style>

+ 67 - 0
src/components/dialog/Window.vue

@@ -0,0 +1,67 @@
+<template>
+    <ui-dialog>
+        <template v-slot:header>
+            <span style="font-size: 16px">{{ title }}</span>
+            <i class="iconfont icon-close" @click="onNo('close')" v-if="showCloseIcon"></i>
+        </template>
+        <template v-if="$slots.content">
+            <slot name="content" />
+        </template>
+        <template v-else>
+            <div class="message" v-html="content"></div>
+        </template>
+        <template v-slot:footer>
+            <ui-button type="cancel" @click="onNo" v-if="showCancelButton">{{ noText }}</ui-button>
+            <ui-button :class="{ disabled: !canSubmit }" type="submit primary" @click="onOk">{{ okText }}</ui-button>
+        </template>
+    </ui-dialog>
+</template>
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+    name: 'ui-window',
+    props: {
+        title: {
+            type: String,
+            default: '提示',
+        },
+        content: {
+            type: String,
+            default: '',
+        },
+        okText: {
+            type: String,
+            default: '确定',
+        },
+        noText: {
+            type: String,
+            default: '取消',
+        },
+        showCloseIcon: {
+            type: Boolean,
+            default: true,
+        },
+        showCancelButton: {
+            type: Boolean,
+            default: true,
+        },
+        canSubmit: {
+            type: Boolean,
+            default: true,
+        },
+    },
+    emits: ['ok', 'no'],
+    setup: function (props, ctx) {
+        const onNo = name => {
+            ctx.emit('no', name)
+        }
+        const onOk = () => {
+            ctx.emit('ok')
+        }
+        return {
+            onNo,
+            onOk,
+        }
+    },
+})
+</script>

+ 113 - 0
src/components/header/CopyLink.vue

@@ -0,0 +1,113 @@
+<template>
+    <Teleport to="body">
+        <div class="login-layer">
+            <div class="login-box">
+                <header>
+                    <h4>分享链接</h4>
+                    <span class="close" @click="emits('close')"><i class="iconfont icon-close"></i></span>
+                </header>
+                <main>
+                    <input readonly v-model="shareURL" />
+                </main>
+                <footer>
+                    <button @click="emits('close')">取消</button>
+                    <button type="submit" ref="copy" :data-clipboard-text="shareURL">复制链接</button>
+                </footer>
+            </div>
+        </div>
+    </Teleport>
+</template>
+<script setup>
+import ClipboardJS from 'clipboard'
+import { ref, onMounted } from 'vue'
+import Toast from '@/components/dialog/Toast'
+const emits = defineEmits(['close','done'])
+const shareURL = ref(window.location.href.replace('&split', '').replace('&adjust', ''))
+const showToast = ref(false)
+const copy = ref(null)
+onMounted(() => {
+    var clipboard = new ClipboardJS(copy.value)
+    clipboard.on('success', function (e) {
+        e.clearSelection()
+        showToast.value = true
+        emits('done')
+    })
+})
+</script>
+<style lang="scss" scoped>
+.login-layer {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 9999;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.login-box {
+    position: absolute;
+    width: 400px;
+    background: rgba(27, 27, 28, 0.8);
+    box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+    border-radius: 4px 4px 4px 4px;
+    opacity: 1;
+    border: 1px solid #000000;
+    backdrop-filter: blur(4px);
+    color: #fff;
+    header {
+        padding: 20px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        h4 {
+            font-size: 16px;
+            font-weight: 400;
+            margin: 0;
+            padding: 0;
+        }
+        .close {
+            cursor: pointer;
+            i {
+                font-size: 14px;
+            }
+        }
+    }
+    main {
+        padding: 40px 20px;
+        border-top: solid 1px rgba(255, 255, 255, 0.16);
+        border-bottom: solid 1px rgba(255, 255, 255, 0.16);
+        input {
+            color: #fff;
+            height: 34px;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 4px 4px 4px 4px;
+            border: none;
+            outline: none;
+            width: 100%;
+            padding: 0 7px;
+        }
+    }
+    footer {
+        height: 60px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        button {
+            cursor: pointer;
+            color: #0076f6;
+            width: 105px;
+            height: 34px;
+            border: 1px solid #0076f6;
+            margin-left: 20px;
+            border-radius: 4px;
+            background: transparent;
+            &[type='submit'] {
+                color: #fff;
+                background: #0076f6;
+            }
+        }
+    }
+}
+</style>

+ 8 - 1
src/components/header/index.vue

@@ -11,7 +11,7 @@
         <div class="user">
             <ul>
                 <li>
-                    <i class="iconfont icon-share"></i>
+                    <i @click="showLink=true;showCopyDone=false" class="iconfont icon-share"></i>
                 </li>
                 <li><em></em></li>
                 <li v-if="user" class="uinfo" @click="showDrop = true">
@@ -28,6 +28,7 @@
             </ul>
         </div>
         <Login v-if="showLogin" @close="showLogin = false" @user="info => (user = info)" />
+        <CopyLink v-if="showLink" @close="showLink=false" @done="showCopyDone=true;showLink=false" />
     </header>
     <footer v-if="props.showAdjust">
         <h4>为场景设置关联位置</h4>
@@ -37,12 +38,15 @@
             <button @click="onSetP2" :class="{ active: points.p2 }">设为P2</button>
         </div>
     </footer>
+    <Toast v-if="showCopyDone"  content="复制成功" />
 </template>
 <script setup>
 import { ref, defineProps, onMounted, watchEffect } from 'vue'
 import { http } from '@/utils/request'
 import browser from '@/utils/browser'
+import Toast from '@/components/dialog/Toast'
 import Login from './Login'
+import CopyLink from './CopyLink'
 import sync from '@/utils/sync'
 
 const props = defineProps({
@@ -53,7 +57,9 @@ const emits = defineEmits(['update'])
 
 const user = ref(null)
 const points = ref({ p1: null, p2: null })
+const showLink = ref(false)
 const showLogin = ref(false)
+const showCopyDone = ref(false)
 
 const getCurPosInfo = () => {
     let app = sync.sourceInst
@@ -216,6 +222,7 @@ footer {
     }
     i {
         font-size: 18px;
+        cursor:pointer;
     }
     em {
         margin: 0 20px;

+ 35 - 0
src/components/loading/Loading.vue

@@ -0,0 +1,35 @@
+<template>
+    <teleport :to="el">
+        <div class="ui-loading" :style="{ zIndex, ['--width']: size + 'px', ['--color']: color }">
+            <div class="ui-loading__box">
+                <div class="default">
+                    <div></div>
+                    <div></div>
+                    <div></div>
+                </div>
+            </div>
+        </div>
+    </teleport>
+</template>
+<script setup>
+import { defineProps } from 'vue'
+import getZIndex from '../../utils/zindex'
+
+defineProps({
+    el: {
+        default: 'body',
+    },
+    size: {
+        default: 15,
+    },
+    color: {
+        default: '#fff',
+    },
+})
+
+const zIndex = getZIndex()
+</script>
+
+<script>
+export default { name: 'ui-loading' }
+</script>

+ 36 - 0
src/components/loading/index.js

@@ -0,0 +1,36 @@
+import Loading from './Loading'
+import { mount } from '../../utils/componentHelper'
+
+const seat = 1
+const closeStack = []
+Loading.use = function use(app) {
+    Loading.show = function (config) {
+        if (closeStack.length) {
+            closeStack.push(seat)
+        } else {
+            const { destroy } = mount(Loading, {
+                app,
+                props: { ...config },
+            })
+            closeStack.push(destroy)
+        }
+    }
+    Loading.hide = function () {
+        if (closeStack.length) {
+            const close = closeStack.pop()
+            if (close !== seat) {
+                close()
+            }
+        }
+    }
+    Loading.hideAll = function () {
+        for (const close of closeStack) {
+            if (close !== seat) {
+                close()
+            }
+        }
+        closeStack.length = 0
+    }
+}
+
+export default Loading

+ 5 - 8
src/pages/Viewer.vue

@@ -43,7 +43,7 @@
                 </div>
             </div>
             <div class="model" v-show="!showAdjust">
-                <div class="bim" :class="{ active: bimChecked }" v-show="!fscChecked">
+                <div class="bim" :class="{ active: bimChecked,disable:!project.bimData }" v-show="!fscChecked">
                     <div @click="onBimChecked">
                         <i class="iconfont icon-BIM"></i>
                         <span>BIM</span>
@@ -526,13 +526,6 @@ main {
             align-items: center;
             justify-content: center;
 
-            &.disable {
-                opacity: 0.3;
-                cursor: default;
-                > div {
-                    pointer-events: none;
-                }
-            }
             > div {
                 width: 100%;
                 height: 100%;
@@ -544,6 +537,10 @@ main {
             &.active {
                 color: #0076f6;
             }
+            &.disable {
+                opacity: 0.5;
+                cursor: default;
+            }
             span {
                 font-size: 12px;
                 padding-top: 1px;

+ 3 - 1
src/pages/viewer.js

@@ -1,3 +1,4 @@
+import '../assets/scss/theme.scss'
 import '../assets/index.scss'
 import { createApp } from 'vue'
 import { setup } from '../utils/request'
@@ -36,4 +37,5 @@ String.prototype.toDate = function() {
 }
 
 setup()
-createApp(App).mount('#app')
+const app = createApp(App)
+app.mount('#app')

+ 39 - 0
src/utils/componentHelper.js

@@ -0,0 +1,39 @@
+/*
+ * @Author: Rindy
+ * @Date: 2021-08-19 11:02:38
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-08-30 12:22:39
+ * @Description:
+ */
+
+import { createVNode, render } from 'vue'
+
+export function mount(component, { props, children, element, app } = {}) {
+    let el = element
+    let vNode = createVNode(component, props, children)
+
+    if (app && app._context) vNode.appContext = app._context
+    if (el) render(vNode, el)
+    else if (typeof document !== 'undefined') {
+        render(vNode, (el = document.createElement('div')))
+    }
+
+    const destroy = () => {
+        if (el) render(null, el)
+        el = null
+        vNode = null
+    }
+
+    return { vNode, destroy, el }
+}
+
+export function setup(...Components) {
+    Components.forEach(Component => {
+        Component.install = function (app) {
+            Component.use && Component.use(app)
+            app.component(Component.name, Component)
+        }
+    })
+
+    return Components
+}

+ 135 - 0
src/utils/dom.js

@@ -0,0 +1,135 @@
+import { ref, onMounted, computed } from 'vue'
+import { toRawType } from './'
+
+/**
+ * 获取真实DOM的高度
+ * @returns [heightRef, VMRef, readyRef]
+ */
+export const getVMDomWH = attr => {
+    const origin = ref(0)
+    const domRef = ref(null)
+    const ready = ref(false)
+    const referWH = () => {
+        origin.value = 0
+        ready.value = false
+        if (domRef.value) {
+            setTimeout(() => {
+                origin.value = attr === 'width' ? domRef.value.offsetWidth : domRef.value.offsetHeight
+                setTimeout(() => (ready.value = true))
+            })
+        }
+    }
+
+    onMounted(referWH)
+
+    return [origin, domRef, ready, referWH]
+}
+
+/**
+ * 生成切换高度的方法
+ * @returns [VMRef, fn, maxHeightRef, originHeightRef, showRef, readyRef]
+ */
+export const changeWHFactory = (isShow = false, attr = 'height') => {
+    const [origin, domRef, ready, referWH] = getVMDomWH(attr)
+    const max = ref(0)
+    const show = computed({
+        get: () => {
+            return max.value == origin.value
+        },
+        set: val => {
+            max.value = val ? origin.value : 0
+        },
+    })
+    const changeShow = (val = !show.value) => {
+        show.value = val
+    }
+
+    onMounted(() => {
+        show.value = isShow
+    })
+
+    return [domRef, changeShow, max, origin, show, ready, referWH]
+}
+
+/**
+ * 获取父级滚动
+ * @param {*} node
+ * @returns
+ */
+export const getScrollParent = node => {
+    if (node == null) {
+        return null
+    }
+    if (node === document.documentElement) {
+        return node
+    }
+
+    const cssScrollY = getComputedStyle(node).overflowY
+    const cssScrollX = getComputedStyle(node).overflowX
+    if (node.scrollHeight > node.clientHeight || cssScrollY === 'auto' || cssScrollY === 'scroll' || cssScrollX === 'auto' || cssScrollX === 'scroll') {
+        return node
+    } else {
+        return getScrollParent(node.parentNode)
+    }
+}
+
+/**
+ * 获取所有父级滚动
+ * @param {*} origin
+ * @param {*} target
+ */
+export const getScrollParents = (origin, target) => {
+    const parents = []
+    let temporary = origin
+    while (temporary && temporary !== target && temporary !== document.documentElement && target.contains(temporary)) {
+        const scrollParent = getScrollParent(temporary)
+        if (scrollParent) {
+            if (scrollParent !== origin) {
+                parents.push(scrollParent)
+            }
+            temporary = scrollParent.parentNode
+        } else {
+            break
+        }
+    }
+
+    return parents
+}
+
+/**
+ * 获取制定dom在相对于目标中的位置
+ * @param {*} origin 或获取的DOM
+ * @param {*} target 目标DOM
+ * @param {*} isIncludeSelf 是否要包含自身宽高
+ * @returns 位置信息 {x, y}
+ */
+export const getPostionByTarget = (origin, target, isIncludeSelf = false) => {
+    const pos = { x: 0, y: 0, width: origin.offsetWidth, height: origin.offsetHeight }
+
+    let temporary = origin
+    while (temporary && temporary !== target && temporary !== document.documentElement && target.contains(temporary)) {
+        pos.x += temporary.offsetLeft + temporary.clientLeft
+        pos.y += temporary.offsetTop + temporary.clientTop
+        temporary = temporary.offsetParent
+    }
+
+    if (isIncludeSelf) {
+        pos.x += pos.width
+        pos.y += pos.height
+    }
+    return pos
+}
+
+export const normalizeUnitToStyle = unit => {
+    if (unit === void 0) {
+        return unit
+    } else if (toRawType(unit) === 'Number') {
+        return unit ? ((unit <= 1) & (unit >= 0) ? 100 * unit + '%' : unit + 'px') : void 0
+    } else if (unit.includes('px')) {
+        return normalizeUnitToStyle(parseFloat(unit))
+    } else if (unit.includes('%')) {
+        return normalizeUnitToStyle(parseFloat(unit) / 100)
+    } else {
+        return unit
+    }
+}

+ 173 - 0
src/utils/index.js

@@ -0,0 +1,173 @@
+/*
+ * @Author: Rindy
+ * @Date: 2021-09-03 11:53:21
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-09-03 15:14:25
+ * @Description:
+ */
+
+export const objectToString = Object.prototype.toString
+export const toTypeString = value => objectToString.call(value)
+
+// 获取制定对象的类型比如toRawType(1) Number
+export const toRawType = value => toTypeString(value).slice(8, -1)
+
+/**
+ * 判断是否函数
+ * @param {any} target 参数对象
+ */
+export const isFunction = target => toRawType(target) === 'Function'
+
+/**
+ * 判断是否普通对象
+ * @param {any} target 参数对象
+ */
+export function isPlainObject(target) {
+    if (!target || typeof target !== 'object' || {}.toString.call(target) != '[object Object]') {
+        return false
+    }
+    var proto = Object.getPrototypeOf(target)
+    if (proto === null) {
+        return true
+    }
+    var Ctor = {}.hasOwnProperty.call(proto, 'constructor') && proto.constructor
+    return typeof Ctor == 'function' && Ctor instanceof Ctor && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object)
+}
+
+/**
+ * 获取忽略指定属性的对象
+ * @param {Object} obj 源对象
+ * @param  {...any} props 忽略属性
+ */
+export function omit(obj, ...props) {
+    const result = { ...obj }
+    props.forEach(function (prop) {
+        delete result[prop]
+    })
+    return result
+}
+
+export const randomId = (e = 6) => {
+    var t = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
+        a = t.length,
+        n = ''
+    for (let i = 0; i < e; i++) {
+        n += t.charAt(Math.floor(Math.random() * a))
+    }
+    return n
+}
+
+/**
+ * 缓存指定方法运行结果
+ * @param {*} fn
+ * @param {*} overdue 缓存超时时间
+ * @returns
+ */
+export const cache = (fn, overdue) => {
+    const cacheMap = new WeakMap()
+
+    return function (...args) {
+        let caches = cacheMap.get(fn)
+        if (!caches) {
+            caches = []
+            cacheMap.set(fn, caches)
+        }
+
+        for (let i = 0; i < caches.length; i++) {
+            const { oldNow, ret, oldArgs } = caches[i]
+
+            if (oldArgs.length === args.length && args.every((arg, i) => arg === oldArgs[i])) {
+                if (Date.now() - oldNow > overdue) {
+                    caches.splice(i, 1)
+                    break
+                } else {
+                    return ret
+                }
+            }
+        }
+
+        const item = {
+            oldNow: Date.now(),
+            ret: fn.apply(this, args),
+            oldArgs: args,
+        }
+        caches.push(item)
+
+        setTimeout(() => {
+            const index = caches.indexOf(item)
+            if (~index) {
+                caches.splice(index, 1)
+            }
+        })
+
+        return item.ret
+    }
+}
+
+// 是否修改
+const _inRevise = (raw1, raw2, readly) => {
+    if (raw1 === raw2) return false
+
+    const rawType1 = toRawType(raw1)
+    const rawType2 = toRawType(raw2)
+
+    if (rawType1 !== rawType2) {
+        return true
+    } else if (rawType1 === 'String' || rawType1 === 'Number' || rawType1 === 'Boolean') {
+        return raw1 !== raw2
+    }
+
+    const rawsArray = Array.from(readly.values())
+    for (const raws of rawsArray) {
+        if (raws.includes(raw1) && raws.includes(raw2)) {
+            return false
+        }
+    }
+    readly.add([raw1, raw2])
+
+    if (rawType1 === 'Array') {
+        return raw1.length !== raw2.length || raw1.some((item1, i) => _inRevise(item1, raw2[i], readly))
+    } else if (rawType1 === 'Object') {
+        const rawKeys1 = Object.keys(raw1).sort()
+        const rawKeys2 = Object.keys(raw2).sort()
+
+        return _inRevise(rawKeys1, rawKeys2, readly) || rawKeys1.some(key => _inRevise(raw1[key], raw2[key], readly))
+    } else if (rawType1 === 'Map') {
+        const rawKeys1 = Array.from(raw1.keys()).sort()
+        const rawKeys2 = Array.from(raw2.keys()).sort()
+
+        return _inRevise(rawKeys1, rawKeys2, readly) || rawKeys1.some(key => _inRevise(raw1.get(key), raw2.get(key), readly))
+    } else if (rawType1 === 'Set') {
+        return inRevise(Array.from(raw1.values()), Array.from(raw2.values()))
+    } else {
+        return raw1 !== raw2
+    }
+}
+
+export const os = (function () {
+    let ua = navigator.userAgent,
+        isWindowsPhone = /(?:Windows Phone)/.test(ua),
+        isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone,
+        isAndroid = /(?:Android)/.test(ua),
+        isFireFox = /(?:Firefox)/.test(ua),
+        isChrome = /(?:Chrome|CriOS)/.test(ua),
+        isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)),
+        isPhone = /(?:iPhone)/.test(ua) && !isTablet,
+        isPc = !isPhone && !isAndroid && !isSymbian
+
+    if (isPc && navigator.maxTouchPoints > 1) {
+        isTablet = true
+    }
+    return {
+        isTablet: isTablet,
+        isPhone: isPhone,
+        isAndroid: isAndroid,
+        isPc: isPc,
+    }
+})()
+
+export const inRevise = (raw1, raw2) => _inRevise(raw1, raw2, new Set())
+
+export * from './dom'
+export * from './zindex'
+export * from './vm'

+ 15 - 0
src/utils/vm.js

@@ -0,0 +1,15 @@
+import { getCurrentInstance } from 'vue'
+
+const collectStack = []
+export const openCollect = () => {
+    collectStack.push([])
+    return () => collectStack.pop()
+}
+
+export const quiltCollect = item => {
+    if (collectStack.length) {
+        collectStack[collectStack.length - 1].push(item)
+    }
+}
+
+export const quiltCollectVM = () => quiltCollect(getCurrentInstance())

+ 5 - 0
src/utils/zindex.js

@@ -0,0 +1,5 @@
+let zindex = 10000
+
+export const getZIndex = () => ++zindex
+
+export default getZIndex

+ 31 - 0
yarn.lock

@@ -2653,6 +2653,15 @@ cli-spinners@^2.0.0:
   resolved "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a"
   integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==
 
+clipboard@^2.0.11:
+  version "2.0.11"
+  resolved "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
+  integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
+  dependencies:
+    good-listener "^1.2.2"
+    select "^1.1.2"
+    tiny-emitter "^2.0.0"
+
 clipboardy@^2.3.0:
   version "2.3.0"
   resolved "https://registry.npmmirror.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
@@ -3312,6 +3321,11 @@ delayed-stream@~1.0.0:
   resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
 
+delegate@^3.1.2:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
+  integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
+
 depd@2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -4210,6 +4224,13 @@ globby@^9.2.0:
     pify "^4.0.1"
     slash "^2.0.0"
 
+good-listener@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==
+  dependencies:
+    delegate "^3.1.2"
+
 graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.2.10"
   resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
@@ -7037,6 +7058,11 @@ select-hose@^2.0.0:
   resolved "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
   integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==
 
+select@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+  integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==
+
 selfsigned@^1.10.8:
   version "1.10.14"
   resolved "https://registry.npmmirror.com/selfsigned/-/selfsigned-1.10.14.tgz#ee51d84d9dcecc61e07e4aba34f229ab525c1574"
@@ -7691,6 +7717,11 @@ timsort@^0.3.0:
   resolved "https://registry.npmmirror.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
   integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
 
+tiny-emitter@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
+  integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
+
 to-arraybuffer@^1.0.0:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"