pipipi-pikachu il y a 5 ans
Parent
commit
39d5b1fc20

+ 203 - 151
package-lock.json

@@ -1762,6 +1762,12 @@
       "integrity": "sha1-fuMwunyq+5gJC+zoal7kQRWQTCw=",
       "dev": true
     },
+    "@types/resize-observer-browser": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npm.taobao.org/@types/resize-observer-browser/download/@types/resize-observer-browser-0.1.4.tgz",
+      "integrity": "sha1-hIeaTW1Krv3lPUspyRwMTLz/wm8=",
+      "dev": true
+    },
     "@types/serve-static": {
       "version": "1.13.8",
       "resolved": "https://registry.npm.taobao.org/@types/serve-static/download/@types/serve-static-1.13.8.tgz?cache=0&sync_timestamp=1605657732347&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fserve-static%2Fdownload%2F%40types%2Fserve-static-1.13.8.tgz",
@@ -2252,122 +2258,6 @@
         "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=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"
-          }
-        },
-        "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": {
@@ -2531,17 +2421,6 @@
             "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",
@@ -2604,18 +2483,6 @@
             "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",
@@ -2664,18 +2531,6 @@
           "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",
@@ -7462,6 +7317,122 @@
         "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",
@@ -15897,6 +15868,87 @@
         }
       }
     },
+    "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",

+ 5 - 4
package.json

@@ -24,6 +24,7 @@
     "@types/clipboard": "^2.0.1",
     "@types/crypto-js": "^4.0.1",
     "@types/jest": "^24.0.19",
+    "@types/resize-observer-browser": "^0.1.4",
     "@typescript-eslint/eslint-plugin": "^2.33.0",
     "@typescript-eslint/parser": "^2.33.0",
     "@vue/cli-plugin-babel": "~4.5.0",
@@ -36,17 +37,17 @@
     "@vue/compiler-sfc": "^3.0.0",
     "@vue/eslint-config-typescript": "^5.0.2",
     "@vue/test-utils": "^2.0.0-0",
+    "babel-plugin-import": "^1.13.3",
     "eslint": "^6.7.2",
     "eslint-plugin-vue": "^7.0.0-0",
-    "babel-plugin-import": "^1.13.3",
     "less": "^3.12.2",
     "less-loader": "^7.1.0",
     "sass": "^1.26.5",
     "sass-loader": "^8.0.2",
-    "typescript": "~3.9.3",
-    "vue-jest": "^5.0.0-0",
     "stylelint": "^13.8.0",
     "stylelint-config-standard": "^20.0.0",
-    "stylelint-webpack-plugin": "^2.1.1"
+    "stylelint-webpack-plugin": "^2.1.1",
+    "typescript": "~3.9.3",
+    "vue-jest": "^5.0.0-0"
   }
 }

+ 0 - 0
src/components/Contextmenu/index.vue


+ 35 - 0
src/components/IframeWrapper.vue

@@ -0,0 +1,35 @@
+<template>
+  <iframe 
+    class="iframe-wrapper"
+    frameborder="0" 
+    allowfullscreen="true" 
+    :src="src"
+    :width="width"
+    :height="height"
+  ></iframe>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'iframe-wrapper',
+  props: {
+    src: {
+      type: String,
+      required: true,
+    },
+    width: {
+      type: Number,
+    },
+    height: {
+      type: Number,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.iframe-wrapper {
+  transform-origin: 0 0;
+  display: block;
+}
+</style>

+ 15 - 0
src/components/SvgWrapper.vue

@@ -0,0 +1,15 @@
+<template>
+  <svg 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlnsXlink="http://www.w3.org/1999/xlink" 
+    version="1.1"
+  >
+    <slot></slot>
+  </svg>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'svg-wrapper',
+}
+</script>

+ 51 - 0
src/components/UploadInput.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="upload-input" @click="handleClick()">
+    <slot></slot>
+    <input 
+      class="file-input"
+      type="file" 
+      name="upload" 
+      ref="inputRef" 
+      :accept="accept" 
+      @change="$event => handleChange($event)"
+    >
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  name: 'upload-input',
+  props: {
+    accept: {
+      type: String,
+      default: 'image/*',
+    },
+  },
+  setup(props, { emit }) {
+    const inputRef = ref<HTMLInputElement | null>(null)
+
+    const handleClick = () => {
+      if(!inputRef.value) return
+      inputRef.value.value = ''
+      inputRef.value.click()
+    }
+    const handleChange = (e: InputEvent) => {
+      const files = (e.target as HTMLInputElement).files
+      if(files) emit('change', files)
+    }
+
+    return {
+      handleClick,
+      handleChange,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.file-input {
+  display: none;
+}
+</style>

+ 3 - 3
src/store/state.ts

@@ -9,15 +9,15 @@ export type State = {
   thumbnailsFocus: boolean;
   editorAreaFocus: boolean;
   availableFonts: string[];
-  slides: Slide[],
-  slideIndex: number,
+  slides: Slide[];
+  slideIndex: number;
 }
 
 export const state: State = {
   activeElementIdList: [],
   handleElementId: '',
   isShowGridLines: false,
-  editorAreaShowScale: 80,
+  editorAreaShowScale: 85,
   canvasScale: 1,
   thumbnailsFocus: false,
   editorAreaFocus: false,

+ 66 - 0
src/views/Editor/Canvas/MouseSelection.vue

@@ -0,0 +1,66 @@
+<template>
+  <div :class="`mouse-selection quadrant-${quadrant}`"
+    :style="{
+      top: top + 'px',
+      left: left + 'px',
+      width: width + 'px',
+      height: height + 'px',
+    }"
+  ></div>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'mouse-selection',
+  props: {
+    top: {
+      type: Number,
+      required: true,
+    },
+    left: {
+      type: Number,
+      required: true,
+    },
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    quadrant: {
+      type: Number,
+      required: true,
+      validator(value: number) {
+        return [1, 2, 3, 4].includes(value)
+      },
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.mouse-selection {
+  position: absolute;
+  background-color: rgba($themeColor, 0.25);
+  z-index: 200;
+
+  &.quadrant-1 {
+    transform-origin: 0 0;
+    transform: rotate(180deg);
+  }
+  &.quadrant-2 {
+    transform-origin: 50% 0;
+    transform: rotate(180deg);
+  }
+  &.quadrant-3 {
+    transform-origin: 0 50%;
+    transform: rotate(180deg);
+  }
+  &.quadrant-4 {
+    transform-origin: 0 0;
+    transform: rotate(0deg);
+  }
+}
+</style>

+ 172 - 5
src/views/Editor/Canvas/index.vue

@@ -1,13 +1,180 @@
 <template>
-  <div class="canvas">
-    
+  <div 
+    class="canvas" 
+    ref="canvasRef"
+    @mousedown="$event => handleClickBlankArea($event)"
+  >
+    <div 
+      class="viewport" 
+      ref="viewportRef"
+      :style="{
+        width: viewportStyles.width + 'px',
+        height: viewportStyles.height + 'px',
+        left: viewportStyles.left + 'px',
+        top: viewportStyles.top + 'px',
+        transform: `scale(${viewportStyles.scale})`,
+      }"
+    >
+      <MouseSelection 
+        v-if="mouseSelectionState.isShow"
+        :top="mouseSelectionState.top" 
+        :left="mouseSelectionState.left" 
+        :width="mouseSelectionState.width" 
+        :height="mouseSelectionState.height" 
+        :quadrant="mouseSelectionState.quadrant"
+      />
+    </div>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { computed, defineComponent, onMounted, onUnmounted, reactive, ref } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store/state'
+import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
+
+import MouseSelection from './MouseSelection.vue'
 
 export default defineComponent({
-  name: 'canvas',
+  name: 'v-canvas',
+  components: {
+    MouseSelection,
+  },
+  setup() {
+    const viewportStyles = reactive({
+      width: VIEWPORT_SIZE,
+      height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO,
+      left: 0,
+      top: 0,
+      scale: 1,
+    })
+
+    const canvasRef = ref<Element | null>(null)
+    const canvasScale = ref(1)
+
+    const store = useStore<State>()
+    const editorAreaShowScale = computed(() => store.state.editorAreaShowScale)
+    const setViewportSize = () => {
+      if(!canvasRef.value) return
+      const canvasWidth = canvasRef.value.clientWidth
+      const canvasHeight = canvasRef.value.clientHeight
+
+      if(canvasHeight / canvasWidth > VIEWPORT_ASPECT_RATIO) {
+        const viewportActualWidth = canvasWidth * (editorAreaShowScale.value / 100)
+        canvasScale.value = viewportActualWidth / VIEWPORT_SIZE
+        viewportStyles.scale = canvasScale.value
+        viewportStyles.left = (canvasWidth - viewportActualWidth) / 2
+        viewportStyles.top = (canvasHeight - viewportActualWidth * VIEWPORT_ASPECT_RATIO) / 2
+      }
+      else {
+        const viewportActualHeight = canvasHeight * (editorAreaShowScale.value / 100)
+        canvasScale.value = viewportActualHeight / (VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO)
+        viewportStyles.scale = canvasScale.value
+        viewportStyles.left = (canvasWidth - viewportActualHeight / VIEWPORT_ASPECT_RATIO) / 2
+        viewportStyles.top = (canvasHeight - viewportActualHeight) / 2
+      }
+    }
+
+    const resizeObserver = new ResizeObserver(setViewportSize)
+    onMounted(() => {
+      if(canvasRef.value) resizeObserver.observe(canvasRef.value)
+    })
+    onUnmounted(() => {
+      if(canvasRef.value) resizeObserver.unobserve(canvasRef.value)
+    })
+
+    const viewportRef = ref<Element | null>(null)
+    const mouseSelectionState = reactive({
+      isShow: false,
+      top: 0,
+      left: 0,
+      width: 0,
+      height: 0,
+      quadrant: 1,
+    })
+    const updateMouseSelection = (e: MouseEvent) => {
+      if(!viewportRef.value) return
+
+      let isMouseDown = true
+      const viewportRect = viewportRef.value.getBoundingClientRect()
+
+      const minSelectionRange = 5
+      
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+
+      const left = (startPageX - viewportRect.x) / canvasScale.value
+      const top = (startPageY - viewportRect.y) / canvasScale.value
+
+      mouseSelectionState.isShow = false
+      mouseSelectionState.quadrant = 4
+      mouseSelectionState.top = top
+      mouseSelectionState.left = left
+      mouseSelectionState.width = 0
+      mouseSelectionState.height = 0
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+
+        const offsetWidth = (currentPageX - startPageX) / canvasScale.value
+        const offsetHeight = (currentPageY - startPageY) / canvasScale.value
+
+        const width = Math.abs(offsetWidth)
+        const height = Math.abs(offsetHeight)
+
+        if( width < minSelectionRange || height < minSelectionRange ) return
+        
+        let quadrant = 0
+        if( offsetWidth > 0 && offsetHeight > 0 ) quadrant = 4
+        else if( offsetWidth < 0 && offsetHeight < 0 ) quadrant = 1
+        else if( offsetWidth > 0 && offsetHeight < 0 ) quadrant = 2
+        else if( offsetWidth < 0 && offsetHeight > 0 ) quadrant = 3
+
+        mouseSelectionState.isShow = true
+        mouseSelectionState.quadrant = quadrant
+        mouseSelectionState.width = width
+        mouseSelectionState.height = height
+      }
+
+      document.onmouseup = () => {
+        document.onmousemove = null
+        document.onmouseup = null
+        isMouseDown = false
+
+        mouseSelectionState.isShow = false
+      }
+    }
+    const handleClickBlankArea = (e: MouseEvent) => {
+      updateMouseSelection(e)
+    }
+
+    return {
+      canvasRef,
+      viewportRef,
+      viewportStyles,
+      mouseSelectionState,
+      handleClickBlankArea,
+    }
+  },
 })
-</script>
+</script>
+
+<style lang="scss" scoped>
+.canvas {
+  height: 100%;
+  user-select: none;
+  overflow: hidden;
+  background-color: #f9f9f9;
+  position: relative;
+}
+
+.viewport {
+  position: absolute;
+  transform-origin: 0 0;
+  background-color: #fff;
+  box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1);
+}
+</style>

+ 0 - 1
src/views/Editor/CanvasTool/index.vue

@@ -14,7 +14,6 @@ export default defineComponent({
 
 <style lang="scss" scoped>
 .canvas-tool {
-  height: 40px;
   border-bottom: 1px solid #eee;
   background-color: #fff;
 }

+ 15 - 9
src/views/Editor/index.vue

@@ -3,9 +3,9 @@
     <EditorHeader class="layout-header" />
     <div class="layout-content">
       <Thumbnails class="layout-content-left" />
-      <div class="layout-content-body">
-        <CanvasTool />
-        <Canvas />
+      <div class="layout-content-center">
+        <CanvasTool class="center-top" />
+        <Canvas class="center-body" />
       </div>
       <Toolbar class="layout-content-right" />
     </div>
@@ -36,22 +36,28 @@ export default defineComponent({
 <style lang="scss" scoped>
 .editor {
   height: 100%;
-  display: flex;
-  flex-direction: column;
 }
 .layout-header {
   height: 40px;
 }
 .layout-content {
-  flex: 1;
+  height: calc(100% - 40px);
   display: flex;
 }
 .layout-content-left {
-  width: 180px;
+  width: 160px;
   height: 100%;
+  flex-shrink: 0;
 }
-.layout-content-body {
-  flex: 1;
+.layout-content-center {
+  width: calc(100% - 160px - 260px);
+
+  .center-top {
+    height: 40px;
+  }
+  .center-body {
+    height: calc(100% - 40px);
+  }
 }
 .layout-content-right {
   width: 260px;

+ 2 - 1
tsconfig.json

@@ -14,7 +14,8 @@
     "baseUrl": ".",
     "types": [
       "webpack-env",
-      "jest"
+      "jest",
+      "resize-observer-browser"
     ],
     "paths": {
       "@/*": [