pipipi-pikachu 5 rokov pred
rodič
commit
dc388ac6ab

+ 151 - 197
package-lock.json

@@ -2258,6 +2258,122 @@
         "tslint": "^5.20.1",
         "webpack": "^4.0.0",
         "yorkie": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792369066&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
+          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz?cache=0&sync_timestamp=1591687000046&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchalk%2Fdownload%2Fchalk-4.1.0.tgz",
+          "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz?cache=0&sync_timestamp=1566248870121&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolor-convert%2Fdownload%2Fcolor-convert-2.0.1.tgz",
+          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
+          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
+          "dev": true,
+          "optional": true
+        },
+        "fork-ts-checker-webpack-plugin-v5": {
+          "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
+          "resolved": "https://registry.npm.taobao.org/fork-ts-checker-webpack-plugin/download/fork-ts-checker-webpack-plugin-5.2.1.tgz?cache=0&sync_timestamp=1607912103019&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffork-ts-checker-webpack-plugin%2Fdownload%2Ffork-ts-checker-webpack-plugin-5.2.1.tgz",
+          "integrity": "sha1-eTJthpeXkG+osk4qvPlCH8gFRQ0=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@babel/code-frame": "^7.8.3",
+            "@types/json-schema": "^7.0.5",
+            "chalk": "^4.1.0",
+            "cosmiconfig": "^6.0.0",
+            "deepmerge": "^4.2.2",
+            "fs-extra": "^9.0.0",
+            "memfs": "^3.1.2",
+            "minimatch": "^3.0.4",
+            "schema-utils": "2.7.0",
+            "semver": "^7.3.2",
+            "tapable": "^1.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
+          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
+          "dev": true,
+          "optional": true
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-6.0.0.tgz?cache=0&sync_timestamp=1594427484405&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-6.0.0.tgz",
+          "integrity": "sha1-bW/mVw69lqr5D8rR2vo7JWbbOpQ=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "schema-utils": {
+          "version": "2.7.0",
+          "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-2.7.0.tgz",
+          "integrity": "sha1-FxUfdtjq5n+793lgwzxnatn078c=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/json-schema": "^7.0.4",
+            "ajv": "^6.12.2",
+            "ajv-keywords": "^3.4.1"
+          }
+        },
+        "semver": {
+          "version": "7.3.4",
+          "resolved": "https://registry.npm.taobao.org/semver/download/semver-7.3.4.tgz?cache=0&sync_timestamp=1606852064928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.3.4.tgz",
+          "integrity": "sha1-J6qn0uTKdkUvmNOt0JOnLJQ+3Jc=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1606205010380&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
+          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz",
+          "integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=",
+          "dev": true,
+          "optional": true
+        }
       }
     },
     "@vue/cli-plugin-unit-jest": {
@@ -2421,6 +2537,17 @@
             "unique-filename": "^1.1.1"
           }
         },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz?cache=0&sync_timestamp=1591687000046&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchalk%2Fdownload%2Fchalk-4.1.0.tgz",
+          "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
         "cliui": {
           "version": "6.0.0",
           "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-6.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcliui%2Fdownload%2Fcliui-6.0.0.tgz",
@@ -2483,6 +2610,18 @@
             "graceful-fs": "^4.1.6"
           }
         },
+        "loader-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz",
+          "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz",
@@ -2531,6 +2670,18 @@
           "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=",
           "dev": true
         },
+        "vue-loader-v16": {
+          "version": "npm:vue-loader@16.1.1",
+          "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.1.1.tgz?cache=0&sync_timestamp=1607093677581&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.1.1.tgz",
+          "integrity": "sha1-9bKG1grGiGaExjoXoYQ5HMngGZo=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "hash-sum": "^2.0.0",
+            "loader-utils": "^2.0.0"
+          }
+        },
         "wrap-ansi": {
           "version": "6.2.0",
           "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz",
@@ -7317,122 +7468,6 @@
         "worker-rpc": "^0.1.0"
       }
     },
-    "fork-ts-checker-webpack-plugin-v5": {
-      "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
-      "resolved": "https://registry.npm.taobao.org/fork-ts-checker-webpack-plugin/download/fork-ts-checker-webpack-plugin-5.2.1.tgz?cache=0&sync_timestamp=1607084938170&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffork-ts-checker-webpack-plugin%2Fdownload%2Ffork-ts-checker-webpack-plugin-5.2.1.tgz",
-      "integrity": "sha1-eTJthpeXkG+osk4qvPlCH8gFRQ0=",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "@babel/code-frame": "^7.8.3",
-        "@types/json-schema": "^7.0.5",
-        "chalk": "^4.1.0",
-        "cosmiconfig": "^6.0.0",
-        "deepmerge": "^4.2.2",
-        "fs-extra": "^9.0.0",
-        "memfs": "^3.1.2",
-        "minimatch": "^3.0.4",
-        "schema-utils": "2.7.0",
-        "semver": "^7.3.2",
-        "tapable": "^1.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792302448&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
-          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz",
-          "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
-          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
-          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1596294337050&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz",
-          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
-          "dev": true,
-          "optional": true
-        },
-        "lru-cache": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-6.0.0.tgz",
-          "integrity": "sha1-bW/mVw69lqr5D8rR2vo7JWbbOpQ=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "yallist": "^4.0.0"
-          }
-        },
-        "schema-utils": {
-          "version": "2.7.0",
-          "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-2.7.0.tgz",
-          "integrity": "sha1-FxUfdtjq5n+793lgwzxnatn078c=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@types/json-schema": "^7.0.4",
-            "ajv": "^6.12.2",
-            "ajv-keywords": "^3.4.1"
-          }
-        },
-        "semver": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npm.taobao.org/semver/download/semver-7.3.4.tgz?cache=0&sync_timestamp=1606852122426&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.3.4.tgz",
-          "integrity": "sha1-J6qn0uTKdkUvmNOt0JOnLJQ+3Jc=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "lru-cache": "^6.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1606205010380&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
-          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        },
-        "yallist": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz",
-          "integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=",
-          "dev": true,
-          "optional": true
-        }
-      }
-    },
     "form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npm.taobao.org/form-data/download/form-data-2.3.3.tgz",
@@ -15868,87 +15903,6 @@
         }
       }
     },
-    "vue-loader-v16": {
-      "version": "npm:vue-loader@16.1.1",
-      "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.1.1.tgz?cache=0&sync_timestamp=1607093677581&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.1.1.tgz",
-      "integrity": "sha1-9bKG1grGiGaExjoXoYQ5HMngGZo=",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "chalk": "^4.1.0",
-        "hash-sum": "^2.0.0",
-        "loader-utils": "^2.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792302448&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
-          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz",
-          "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
-          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
-          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1596294337050&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz",
-          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
-          "dev": true,
-          "optional": true
-        },
-        "loader-utils": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz",
-          "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^2.1.2"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1606205010380&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
-          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "vue-router": {
       "version": "4.0.1",
       "resolved": "https://registry.npm.taobao.org/vue-router/download/vue-router-4.0.1.tgz?cache=0&sync_timestamp=1607347245114&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-router%2Fdownload%2Fvue-router-4.0.1.tgz",

+ 139 - 0
src/assets/animate/bounce.scss

@@ -0,0 +1,139 @@
+@keyframes bounceIn {
+  from,
+  20%,
+  40%,
+  60%,
+  80%,
+  to {
+    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+  }
+  0% {
+    opacity: 0;
+    transform: scale3d(0.3, 0.3, 0.3);
+  }
+  20% {
+    transform: scale3d(1.1, 1.1, 1.1);
+  }
+  40% {
+    transform: scale3d(0.9, 0.9, 0.9);
+  }
+  60% {
+    opacity: 1;
+    transform: scale3d(1.03, 1.03, 1.03);
+  }
+  80% {
+    transform: scale3d(0.97, 0.97, 0.97);
+  }
+  to {
+    opacity: 1;
+    transform: scale3d(1, 1, 1);
+  }
+}
+
+@keyframes bounceInDown {
+  from,
+  60%,
+  75%,
+  90%,
+  to {
+    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+  }
+  0% {
+    opacity: 0;
+    transform: translate3d(0, -3000px, 0);
+  }
+  60% {
+    opacity: 1;
+    transform: translate3d(0, 25px, 0);
+  }
+  75% {
+    transform: translate3d(0, -10px, 0);
+  }
+  90% {
+    transform: translate3d(0, 5px, 0);
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes bounceInLeft {
+  from,
+  60%,
+  75%,
+  90%,
+  to {
+    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+  }
+  0% {
+    opacity: 0;
+    transform: translate3d(-3000px, 0, 0);
+  }
+  60% {
+    opacity: 1;
+    transform: translate3d(25px, 0, 0);
+  }
+  75% {
+    transform: translate3d(-10px, 0, 0);
+  }
+  90% {
+    transform: translate3d(5px, 0, 0);
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes bounceInRight {
+  from,
+  60%,
+  75%,
+  90%,
+  to {
+    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+  }
+  from {
+    opacity: 0;
+    transform: translate3d(3000px, 0, 0);
+  }
+  60% {
+    opacity: 1;
+    transform: translate3d(-25px, 0, 0);
+  }
+  75% {
+    transform: translate3d(10px, 0, 0);
+  }
+  90% {
+    transform: translate3d(-5px, 0, 0);
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes bounceInUp {
+  from,
+  60%,
+  75%,
+  90%,
+  to {
+    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+  }
+  from {
+    opacity: 0;
+    transform: translate3d(0, 3000px, 0);
+  }
+  60% {
+    opacity: 1;
+    transform: translate3d(0, -20px, 0);
+  }
+  75% {
+    transform: translate3d(0, 10px, 0);
+  }
+  90% {
+    transform: translate3d(0, -5px, 0);
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}

+ 52 - 0
src/assets/animate/fade.scss

@@ -0,0 +1,52 @@
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes fadeInDown {
+  from {
+    opacity: 0;
+    transform: translate3d(0, -100%, 0);
+  }
+  to {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes fadeInLeft {
+  from {
+    opacity: 0;
+    transform: translate3d(-100%, 0, 0);
+  }
+  to {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes fadeInRight {
+  from {
+    opacity: 0;
+    transform: translate3d(100%, 0, 0);
+  }
+  to {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translate3d(0, 100%, 0);
+  }
+  to {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}

+ 43 - 0
src/assets/animate/flip.scss

@@ -0,0 +1,43 @@
+@keyframes flipInX {
+  from {
+    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+    animation-timing-function: ease-in;
+    opacity: 0;
+  }
+  40% {
+    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+    animation-timing-function: ease-in;
+  }
+  60% {
+    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+    opacity: 1;
+  }
+  80% {
+    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+  }
+  to {
+    transform: perspective(400px);
+  }
+}
+
+@keyframes flipInY {
+  from {
+    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+    animation-timing-function: ease-in;
+    opacity: 0;
+  }
+  40% {
+    transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+    animation-timing-function: ease-in;
+  }
+  60% {
+    transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+    opacity: 1;
+  }
+  80% {
+    transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+  }
+  to {
+    transform: perspective(400px);
+  }
+}

+ 103 - 0
src/assets/animate/main.scss

@@ -0,0 +1,103 @@
+@import './bounce.scss';
+@import './fade.scss';
+@import './flip.scss';
+@import './rotate.scss';
+@import './slide.scss';
+@import './zoom.scss';
+
+.animate {
+  animation-duration: 0ms;
+
+  &.duration-500 {
+    animation-duration: 500ms;
+  }
+  &.duration-1000 {
+    animation-duration: 1000ms;
+  }
+  &.duration-1500 {
+    animation-duration: 1500ms;
+  }
+  &.duration-2000 {
+    animation-duration: 2000ms;
+  }
+
+  &.bounceIn {
+    animation-name: bounceIn;
+  }
+  &.bounceInDown {
+    animation-name: bounceInDown;
+  }
+  &.bounceInLeft {
+    animation-name: bounceInLeft;
+  }
+  &.bounceInRight {
+    animation-name: bounceInRight;
+  }
+  &.bounceInUp {
+    animation-name: bounceInUp;
+  }
+  &.fadeIn {
+    animation-name: fadeIn;
+  }
+  &.fadeInDown {
+    animation-name: fadeInDown;
+  }
+  &.fadeInLeft {
+    animation-name: fadeInLeft;
+  }
+  &.fadeInRight {
+    animation-name: fadeInRight;
+  }
+  &.fadeInUp {
+    animation-name: fadeInUp;
+  }
+  &.flipInX {
+    animation-name: flipInX;
+  }
+  &.flipInY {
+    animation-name: flipInY;
+  }
+  &.rotateIn {
+    animation-name: rotateIn;
+  }
+  &.rotateInDownLeft {
+    animation-name: rotateInDownLeft;
+  }
+  &.rotateInDownRight {
+    animation-name: rotateInDownRight;
+  }
+  &.rotateInUpLeft {
+    animation-name: rotateInUpLeft;
+  }
+  &.rotateInUpRight {
+    animation-name: rotateInUpRight;
+  }
+  &.slideInDown {
+    animation-name: slideInDown;
+  }
+  &.slideInLeft {
+    animation-name: slideInLeft;
+  }
+  &.slideInRight {
+    animation-name: slideInRight;
+  }
+  &.slideInUp {
+    animation-name: slideInUp;
+  }
+  &.zoomIn {
+    animation-name: zoomIn;
+  }
+  &.zoomInDown {
+    animation-name: zoomInDown;
+  }
+  &.zoomInLeft {
+    animation-name: zoomInLeft;
+  }
+  &.zoomInRight {
+    animation-name: zoomInRight;
+  }
+  &.zoomInUp {
+    animation-name: zoomInUp;
+  }
+}
+

+ 64 - 0
src/assets/animate/rotate.scss

@@ -0,0 +1,64 @@
+@keyframes rotateIn {
+  from {
+    transform-origin: center;
+    transform: rotate3d(0, 0, 1, -200deg);
+    opacity: 0;
+  }
+  to {
+    transform-origin: center;
+    transform: translate3d(0, 0, 0);
+    opacity: 1;
+  }
+}
+
+@keyframes rotateInDownLeft {
+  from {
+    transform-origin: left bottom;
+    transform: rotate3d(0, 0, 1, -45deg);
+    opacity: 0;
+  }
+  to {
+    transform-origin: left bottom;
+    transform: translate3d(0, 0, 0);
+    opacity: 1;
+  }
+}
+
+@keyframes rotateInDownRight {
+  from {
+    transform-origin: right bottom;
+    transform: rotate3d(0, 0, 1, 45deg);
+    opacity: 0;
+  }
+  to {
+    transform-origin: right bottom;
+    transform: translate3d(0, 0, 0);
+    opacity: 1;
+  }
+}
+
+@keyframes rotateInUpLeft {
+  from {
+    transform-origin: left bottom;
+    transform: rotate3d(0, 0, 1, 45deg);
+    opacity: 0;
+  }
+  to {
+    transform-origin: left bottom;
+    transform: translate3d(0, 0, 0);
+    opacity: 1;
+  }
+}
+
+@keyframes rotateInUpRight {
+  from {
+    transform-origin: right bottom;
+    transform: rotate3d(0, 0, 1, -90deg);
+    opacity: 0;
+  }
+  to {
+    transform-origin: right bottom;
+    transform: translate3d(0, 0, 0);
+    opacity: 1;
+  }
+}

+ 39 - 0
src/assets/animate/slide.scss

@@ -0,0 +1,39 @@
+@keyframes slideInDown {
+  from {
+    transform: translate3d(0, -100%, 0);
+    visibility: visible;
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes slideInLeft {
+  from {
+    transform: translate3d(-100%, 0, 0);
+    visibility: visible;
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes slideInRight {
+  from {
+    transform: translate3d(100%, 0, 0);
+    visibility: visible;
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+@keyframes slideInUp {
+  from {
+    transform: translate3d(0, 100%, 0);
+    visibility: visible;
+  }
+  to {
+    transform: translate3d(0, 0, 0);
+  }
+}

+ 61 - 0
src/assets/animate/zoom.scss

@@ -0,0 +1,61 @@
+@keyframes zoomIn {
+  from {
+    opacity: 0;
+    transform: scale3d(0.3, 0.3, 0.3);
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+@keyframes zoomInDown {
+  from {
+    opacity: 0;
+    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
+    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+  }
+  60% {
+    opacity: 1;
+    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+  }
+}
+
+@keyframes zoomInLeft {
+  from {
+    opacity: 0;
+    transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
+    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+  }
+  60% {
+    opacity: 1;
+    transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
+    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+  }
+}
+
+@keyframes zoomInRight {
+  from {
+    opacity: 0;
+    transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
+    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+  }
+  60% {
+    opacity: 1;
+    transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
+    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+  }
+}
+
+@keyframes zoomInUp {
+  from {
+    opacity: 0;
+    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
+    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+  }
+  60% {
+    opacity: 1;
+    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+  }
+}

+ 192 - 0
src/views/_common/_element/EditableElement.vue

@@ -0,0 +1,192 @@
+<template>
+  <div 
+    class="editable-element"
+    :id="'editable-element-' + elementInfo.elId"
+    :style="{ zIndex: elementIndex }"
+  >
+    <component
+      :is="currentElementComponent"
+      :elementInfo="elementInfo"
+      :canvasScale="canvasScale"
+      :isActive="isActive"
+      :isHandleEl="isHandleEl"
+      :isMultiSelect="isMultiSelect"
+      :selectElement="selectElement"
+      :rotateElement="rotateElement"
+      :scaleElement="scaleElement"
+      :contextmenus="contextmenus"
+    ></component>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { PPTElement } from '@/types/slides'
+
+import ImageElement from './ImageElement.index.vue'
+import TextElement from './TextElement.index.vue'
+
+export default defineComponent({
+  name: 'editable-element',
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTElement>,
+      required: true,
+    },
+    elementIndex: {
+      type: Number,
+      required: true,
+    },
+    canvasScale: {
+      type: Number,
+      required: true,
+    },
+    isActive: {
+      type: Boolean,
+      required: true,
+    },
+    isHandleEl: {
+      type: Boolean,
+      required: true,
+    },
+    isMultiSelect: {
+      type: Boolean,
+      required: true,
+    },
+    selectElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove: boolean) => void>,
+      required: true,
+    },
+    rotateElement: {
+      type: Function as PropType<(element: PPTElement) => void>,
+      required: true,
+    },
+    scaleElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTElement, direction: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) => void>,
+      required: true,
+    },
+    updateZIndex: {
+      type: Function as PropType<(element: PPTElement, operation: 'up' | 'down' | 'top' | 'bottom') => void>,
+      required: true,
+    },
+    combineElements: {
+      type: Function as PropType<() => void>,
+      required: true,
+    },
+    uncombineElements: {
+      type: Function as PropType<() => void>,
+      required: true,
+    },
+    alignElement: {
+      type: Function as PropType<(direction: 'top' | 'verticalCenter' | 'bottom' | 'left' | 'horizontalCenter' | 'right') => void>,
+      required: true,
+    },
+    deleteElement: {
+      type: Function as PropType<() => void>,
+      required: true,
+    },
+    lockElement: {
+      type: Function as PropType<(element: PPTElement, handle: 'lock' | 'unlock') => void>,
+      required: true,
+    },
+    copyElement: {
+      type: Function as PropType<() => void>,
+      required: true,
+    },
+    cutElement: {
+      type: Function as PropType<() => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const currentElementComponent = computed(() => {
+      const elementTypeMap = {
+        'image': ImageElement,
+        'text': TextElement,
+      }
+      return elementTypeMap[props.elementInfo.type] || null
+    })
+
+    const contextmenus = () => {
+      if(props.elementInfo.isLock) {
+        return [{
+          text: '解锁', 
+          icon: 'icon-unlock',
+          action: () => props.lockElement(props.elementInfo, 'unlock'),
+        }]
+      }
+
+      return [
+        {
+          text: '剪切',
+          subText: 'Ctrl + X',
+          icon: 'icon-scissor',
+          action: props.cutElement,
+        },
+        {
+          text: '复制',
+          subText: 'Ctrl + C',
+          icon: 'icon-copy',
+          action: props.copyElement,
+        },
+        { divider: true },
+        {
+          text: '层级',
+          icon: 'icon-top-layer',
+          disable: props.isMultiSelect && !props.elementInfo.groupId,
+          children: [
+            { text: '置顶层', action: () => props.updateZIndex(props.elementInfo, 'top') },
+            { text: '置底层', action: () => props.updateZIndex(props.elementInfo, 'bottom') },
+            { divider: true },
+            { text: '上移一层', action: () => props.updateZIndex(props.elementInfo, 'up') },
+            { text: '下移一层', action: () => props.updateZIndex(props.elementInfo, 'down') },
+          ],
+        },
+        {
+          text: '水平对齐',
+          icon: 'icon-align-left',
+          children: [
+            { text: '水平居中', action: () => props.alignElement('horizontalCenter') },
+            { text: '左对齐', action: () => props.alignElement('left') },
+            { text: '右对齐', action: () => props.alignElement('right') },
+          ],
+        },
+        {
+          text: '垂直对齐',
+          icon: 'icon-align-bottom',
+          children: [
+            { text: '垂直居中', action: () => props.alignElement('verticalCenter') },
+            { text: '上对齐', action: () => props.alignElement('top') },
+            { text: '下对齐', action: () => props.alignElement('bottom') },
+          ],
+        },
+        { divider: true },
+        {
+          text: props.elementInfo.groupId ? '取消组合' : '组合',
+          subText: 'Ctrl + G',
+          icon: 'icon-block',
+          action: props.elementInfo.groupId ? props.uncombineElements : props.combineElements,
+          hide: !props.isMultiSelect,
+        },
+        {
+          text: '锁定',
+          subText: 'Ctrl + L',
+          icon: 'icon-lock',
+          action: () => props.lockElement(props.elementInfo, 'lock'),
+        },
+        {
+          text: '删除',
+          subText: 'Delete',
+          icon: 'icon-delete',
+          action: () => props.deleteElement(),
+        },
+      ]
+    }
+
+    return {
+      currentElementComponent,
+      contextmenus,
+    }
+  },
+})
+</script>

+ 54 - 0
src/views/_common/_element/ElementBorder.vue

@@ -0,0 +1,54 @@
+<template>
+  <SvgWrapper 
+    class="element-border"
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <path 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`" 
+      :stroke="borderColor"
+      :stroke-width="borderWidth" 
+      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></path>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'element-border',
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    borderColor: {
+      type: String,
+    },
+    borderWidth: {
+      type: Number,
+    },
+    borderStyle: {
+      type: String,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+</style>

+ 499 - 0
src/views/_common/_element/ImageElement/ImageClipHandler.vue

@@ -0,0 +1,499 @@
+<template>
+  <div 
+    class="image-clip-handler" 
+    :style="clipWrapperPositionStyle" 
+    v-click-outside="clip"
+  >
+    <img 
+      class="bottom-img" 
+      :src="imgUrl" 
+      :draggable="false" 
+      alt="" 
+      :style="bottomImgPositionStyle" 
+    />
+
+    <div 
+      class="top-img-wrapper" 
+      :style="{
+        ...topImgWrapperPositionStyle,
+        clipPath: clipPath,
+      }"
+    >
+      <img 
+        class="top-img" 
+        :src="imgUrl" 
+        :draggable="false" 
+        alt="" 
+        :style="topImgPositionStyle" 
+      />
+    </div>
+
+    <div 
+      class="operate" 
+      :style="topImgWrapperPositionStyle" 
+      @mousedown.stop="$event => moveClipRange($event)"
+    >
+      <div 
+        :class="['clip-point', point]" 
+        v-for="point in ['t-l', 't-r', 'b-l', 'b-r']" 
+        :key="point" 
+        @mousedown.stop="$event => scaleClipRange($event, point)"
+      >
+        <SvgWrapper width="12" height="12" fill="#fff" stroke="#666">
+          <path d="M 12 0 L 0 0 L 0 12 L 3 12 L 3 3 L 12 3 L 12 0 Z"></path>
+        </SvgWrapper>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
+import { KEYCODE } from '@/configs/keyCode'
+
+type ClipDataRange = [[number, number], [number, number]]
+
+interface ClipData {
+  range: ClipDataRange;
+  path: string;
+}
+
+type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
+
+export default defineComponent({
+  name: 'image-clip-handler',
+  props: {
+    imgUrl: {
+      type: String,
+      required: true,
+    },
+    clipData: {
+      type: Object as PropType<ClipData>,
+      required: true,
+    },
+    clipPath: {
+      type: String,
+      required: true,
+    },
+    canvasScale: {
+      type: Number,
+      required: true,
+    },
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    top: {
+      type: Number,
+      required: true,
+    },
+    left: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props, { emit }) {
+    const topImgWrapperPosition = reactive({
+      top: 0,
+      left: 0,
+      width: 0,
+      height: 0,
+    })
+    const clipWrapperPositionStyle = reactive({
+      top: '0',
+      left: '0',
+    })
+    const isSettingClipRange = ref(false)
+    const currentRange = ref<ClipDataRange | null>(null)
+
+    const getClipDataTransformInfo = () => {
+      const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
+
+      const widthScale = (end[0] - start[0]) / 100
+      const heightScale = (end[1] - start[1]) / 100
+      const left = start[0] / widthScale
+      const top = start[1] / heightScale
+
+      return { widthScale, heightScale, left, top }
+    }
+
+    const imgPosition = computed(() => {
+      const { widthScale, heightScale, left, top } = getClipDataTransformInfo()
+      return {
+        left: -left,
+        top: -top,
+        width: 100 / widthScale,
+        height: 100 / heightScale,
+      }
+    })
+
+    const bottomImgPositionStyle = computed(() => {
+      return {
+        top: imgPosition.value.top + '%',
+        left: imgPosition.value.left + '%',
+        width: imgPosition.value.width + '%',
+        height: imgPosition.value.height + '%',
+      }
+    })
+
+    const topImgWrapperPositionStyle = computed(() => {
+      return {
+        top: topImgWrapperPosition.top + '%',
+        left: topImgWrapperPosition.left + '%',
+        width: topImgWrapperPosition.width + '%',
+        height: topImgWrapperPosition.height + '%',
+      }
+    })
+
+    const topImgPositionStyle = computed(() => {
+      const bottomWidth = imgPosition.value.width
+      const bottomHeight = imgPosition.value.height
+      
+      const topLeft = topImgWrapperPosition.left
+      const topTop = topImgWrapperPosition.top
+      const topWidth = topImgWrapperPosition.width
+      const topHeight = topImgWrapperPosition.height
+      
+      return {
+        left: -topLeft * (100 / topWidth) + '%',
+        top: -topTop * (100 / topHeight) + '%',
+        width: bottomWidth / topWidth * 100 + '%',
+        height: bottomHeight / topHeight * 100 + '%',
+      }
+    })
+
+    const initClipPosition = () => {
+      const { left, top } = getClipDataTransformInfo()
+      topImgWrapperPosition.left = left
+      topImgWrapperPosition.top = top
+      topImgWrapperPosition.width = 100
+      topImgWrapperPosition.height = 100
+      
+      clipWrapperPositionStyle.top = -top + '%'
+      clipWrapperPositionStyle.left = -left + '%'
+    }
+
+    const clip = () => {
+      if(isSettingClipRange.value) return
+
+      if(!currentRange.value) {
+        emit('clip', null)
+        return
+      }
+
+      const { left, top } = getClipDataTransformInfo()
+
+      const position = {
+        left: (topImgWrapperPosition.left - left) / 100 * props.width,
+        top: (topImgWrapperPosition.top - top) / 100 * props.height,
+        width: (topImgWrapperPosition.width - 100) / 100 * props.width,
+        height: (topImgWrapperPosition.height - 100) / 100 * props.height,
+      }
+      emit('clip', {
+        range: currentRange.value,
+        position,
+      })
+    }
+
+    const keyboardClip = (e: KeyboardEvent) => {
+      if(e.keyCode === KEYCODE.ENTER) clip()
+    }
+
+    onMounted(() => {
+      initClipPosition()
+      document.addEventListener('keydown', keyboardClip)
+    })
+    onUnmounted(() => {
+      document.removeEventListener('keydown', keyboardClip)
+    })
+
+    const getRange = () => {
+      const retPosition = {
+        left: parseInt(topImgPositionStyle.value.left),
+        top: parseInt(topImgPositionStyle.value.top),
+        width: parseInt(topImgPositionStyle.value.width),
+        height: parseInt(topImgPositionStyle.value.height),
+      }
+
+      const widthScale = 100 / retPosition.width
+      const heightScale = 100 / retPosition.height
+
+      const start: [number, number] = [
+        -retPosition.left * widthScale,
+        -retPosition.top * heightScale,
+      ]
+      const end: [number, number] = [
+        widthScale * 100 + start[0],
+        heightScale * 100 + start[1],
+      ]
+
+      currentRange.value = [start, end]
+    }
+
+    const moveClipRange = (e: MouseEvent) => {
+      isSettingClipRange.value = true
+      let isMouseDown = true
+
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+      const bottomPosition = imgPosition.value
+      const originPositopn = {
+        left: topImgWrapperPosition.left,
+        top: topImgWrapperPosition.top,
+        width: topImgWrapperPosition.width,
+        height: topImgWrapperPosition.height,
+      }
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+
+        const moveX = (currentPageX - startPageX) / props.canvasScale / props.width * 100
+        const moveY = (currentPageY - startPageY) / props.canvasScale / props.height * 100
+
+        let targetLeft = originPositopn.left + moveX
+        let targetTop = originPositopn.top + moveY
+
+        // 范围限制
+        if(targetLeft < 0) targetLeft = 0
+        else if(targetLeft + originPositopn.width > bottomPosition.width) {
+          targetLeft = bottomPosition.width - originPositopn.width
+        }
+        if(targetTop < 0) targetTop = 0
+        else if(targetTop + originPositopn.height > bottomPosition.height) {
+          targetTop = bottomPosition.height - originPositopn.height
+        }
+        
+        topImgWrapperPosition.left = targetLeft
+        topImgWrapperPosition.top = targetTop
+      }
+
+      document.onmouseup = () => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+
+        getRange()
+
+        setTimeout(() => {
+          isSettingClipRange.value = false
+        }, 0)
+      }
+    }
+
+    const scaleClipRange = (e: MouseEvent, type: ScaleClipRangeType) => {
+      isSettingClipRange.value = true
+      let isMouseDown = true
+
+      const minWidth = 32 / props.width * 100
+      const minHeight = 32 / props.height * 100
+      
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+      const bottomPosition = imgPosition.value
+      const originPositopn = {
+        left: topImgWrapperPosition.left,
+        top: topImgWrapperPosition.top,
+        width: topImgWrapperPosition.width,
+        height: topImgWrapperPosition.height,
+      }
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+
+        let moveX = (currentPageX - startPageX) / props.canvasScale / props.width * 100
+        let moveY = (currentPageY - startPageY) / props.canvasScale / props.height * 100
+
+        let targetLeft, targetTop, targetWidth, targetHeight
+
+        // 根据不同缩放点,计算目标大小和位置,同时做大小和范围的限制
+        if(type === 't-l') {
+          if(originPositopn.left + moveX < 0) {
+            moveX = -originPositopn.left
+          }
+          if(originPositopn.top + moveY < 0) {
+            moveY = -originPositopn.top
+          }
+          if(originPositopn.width - moveX < minWidth) {
+            moveX = originPositopn.width - minWidth
+          }
+          if(originPositopn.height - moveY < minHeight) {
+            moveY = originPositopn.height - minHeight
+          }
+          targetWidth = originPositopn.width - moveX
+          targetHeight = originPositopn.height - moveY
+          targetLeft = originPositopn.left + moveX
+          targetTop = originPositopn.top + moveY
+        }
+        else if(type === 't-r') {
+          if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
+            moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
+          }
+          if(originPositopn.top + moveY < 0) {
+            moveY = -originPositopn.top
+          }
+          if(originPositopn.width + moveX < minWidth) {
+            moveX = minWidth - originPositopn.width
+          }
+          if(originPositopn.height - moveY < minHeight) {
+            moveY = originPositopn.height - minHeight
+          }
+          targetWidth = originPositopn.width + moveX
+          targetHeight = originPositopn.height - moveY
+          targetLeft = originPositopn.left
+          targetTop = originPositopn.top + moveY
+        }
+        else if(type === 'b-l') {
+          if(originPositopn.left + moveX < 0) {
+            moveX = -originPositopn.left
+          }
+          if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
+            moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
+          }
+          if(originPositopn.width - moveX < minWidth) {
+            moveX = originPositopn.width - minWidth
+          }
+          if(originPositopn.height + moveY < minHeight) {
+            moveY = minHeight - originPositopn.height
+          }
+          targetWidth = originPositopn.width - moveX
+          targetHeight = originPositopn.height + moveY
+          targetLeft = originPositopn.left + moveX
+          targetTop = originPositopn.top
+        }
+        else {
+          if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
+            moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
+          }
+          if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
+            moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
+          }
+          if(originPositopn.width + moveX < minWidth) {
+            moveX = minWidth - originPositopn.width
+          }
+          if(originPositopn.height + moveY < minHeight) {
+            moveY = minHeight - originPositopn.height
+          }
+          targetWidth = originPositopn.width + moveX
+          targetHeight = originPositopn.height + moveY
+          targetLeft = originPositopn.left
+          targetTop = originPositopn.top
+        }
+        
+        topImgWrapperPosition.left = targetLeft
+        topImgWrapperPosition.top = targetTop
+        topImgWrapperPosition.width = targetWidth
+        topImgWrapperPosition.height = targetHeight
+      }
+
+      document.onmouseup = () => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+
+        getRange()
+
+        setTimeout(() => isSettingClipRange.value = false, 0)
+      }
+    }
+
+    return {
+      clipWrapperPositionStyle,
+      bottomImgPositionStyle,
+      topImgWrapperPositionStyle,
+      topImgPositionStyle,
+      clip,
+      moveClipRange,
+      scaleClipRange,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.image-clip-handler {
+  width: 100%;
+  height: 100%;
+  position: relative;
+
+  .bottom-img {
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    opacity: .5;
+  }
+
+  img {
+    width: 100%;
+    height: 100%;
+  }
+
+  .top-img-wrapper {
+    position: absolute;
+    overflow: hidden;
+
+    img {
+      position: absolute;
+    }
+  }
+}
+
+.operate {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  cursor: move;
+}
+
+.clip-point {
+  position: absolute;
+  width: 12px;
+  height: 12px;
+  left: 0;
+  top: 0;
+  transform-origin: 0 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  svg {
+    overflow: visible;
+  }
+
+  &.t-l {
+    cursor: nwse-resize;
+    left: 0;
+    top: 0;
+  }
+  &.t-r {
+    cursor: nesw-resize;
+    left: 100%;
+    top: 0;
+    transform: rotate(90deg);
+  }
+  &.b-l {
+    cursor: nesw-resize;
+    left: 0;
+    top: 100%;
+    transform: rotate(-90deg);
+  }
+  &.b-r {
+    cursor: nwse-resize;
+    left: 100%;
+    top: 100%;
+    transform: rotate(180deg);
+  }
+}
+</style>

+ 61 - 0
src/views/_common/_element/ImageElement/ImageEllipseBorder.vue

@@ -0,0 +1,61 @@
+<template>
+  <SvgWrapper 
+    class="image-ellipse-border" 
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <ellipse 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :cx="width / 2" 
+      :cy="height / 2"
+      :rx="width / 2" 
+      :ry="height / 2"
+      :stroke="borderColor"
+      :stroke-width="borderWidth" 
+      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></ellipse>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'image-ellipse-border',
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    borderColor: {
+      type: String,
+      default: '',
+    },
+    borderWidth: {
+      type: Number,
+      default: 0,
+    },
+    borderStyle: {
+      type: String,
+      default: '',
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  left: 0;
+}
+</style>

+ 62 - 0
src/views/_common/_element/ImageElement/ImagePolygonBorder.vue

@@ -0,0 +1,62 @@
+<template>
+  <SvgWrapper 
+    class="image-polygon-border" 
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <path 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :d="createPath(width, height)"
+      :stroke="borderColor"
+      :stroke-width="borderWidth" 
+      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></path>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'image-polygon-border',
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    borderColor: {
+      type: String,
+      default: '',
+    },
+    borderWidth: {
+      type: Number,
+      default: 0,
+    },
+    borderStyle: {
+      type: String,
+      default: '',
+    },
+    createPath: {
+      type: Function,
+      required: true,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  left: 0;
+}
+</style>

+ 65 - 0
src/views/_common/_element/ImageElement/ImageRectBorder.vue

@@ -0,0 +1,65 @@
+<template>
+  <SvgWrapper 
+    class="image-rect-border" 
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <rect 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :rx="radius" 
+      :ry="radius"
+      :width="width"
+      :height="height"
+      :stroke="borderColor"
+      :stroke-width="borderWidth" 
+      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></rect>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'image-rect-border',
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    borderColor: {
+      type: String,
+      default: '',
+    },
+    borderWidth: {
+      type: Number,
+      default: 0,
+    },
+    borderStyle: {
+      type: String,
+      default: '',
+    },
+    radius: {
+      type: String,
+      default: '0',
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  left: 0;
+}
+</style>