This commit is contained in:
pipipi-pikachu 2020-12-22 17:28:14 +08:00
parent fdbb6dde16
commit 5f20da11b3
27 changed files with 561 additions and 506 deletions

348
package-lock.json generated
View File

@ -2258,6 +2258,122 @@
"tslint": "^5.20.1", "tslint": "^5.20.1",
"webpack": "^4.0.0", "webpack": "^4.0.0",
"yorkie": "^2.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",
"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=1608033330722&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": { "@vue/cli-plugin-unit-jest": {
@ -2421,6 +2537,17 @@
"unique-filename": "^1.1.1" "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": { "cliui": {
"version": "6.0.0", "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", "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" "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": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz", "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz",
@ -2531,6 +2670,18 @@
"integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=", "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=",
"dev": true "dev": true
}, },
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.1.2.tgz?cache=0&sync_timestamp=1608187974157&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.1.2.tgz",
"integrity": "sha1-XAO2xQ0qX5g8fOuhXFDXjKKymPQ=",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
},
"wrap-ansi": { "wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz",
@ -7322,122 +7473,6 @@
"worker-rpc": "^0.1.0" "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",
"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",
"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": { "form-data": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npm.taobao.org/form-data/download/form-data-2.3.3.tgz", "resolved": "https://registry.npm.taobao.org/form-data/download/form-data-2.3.3.tgz",
@ -15883,87 +15918,6 @@
} }
} }
}, },
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.1.2.tgz?cache=0&sync_timestamp=1608187974157&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.1.2.tgz",
"integrity": "sha1-XAO2xQ0qX5g8fOuhXFDXjKKymPQ=",
"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",
"integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-router": { "vue-router": {
"version": "4.0.1", "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", "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",

View File

@ -1,23 +1,24 @@
import { Ref, computed } from 'vue' import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit' import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
import { getElementListRange } from '../utils/elementRange' import { getElementListRange } from '@/utils/element'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList) const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const alignElementToCanvas = (command: ElementAlignCommand) => { const alignElementToCanvas = (command: ElementAlignCommand) => {
const viewportWidth = VIEWPORT_SIZE const viewportWidth = VIEWPORT_SIZE
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value) const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value)) const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) { for(const element of newElementList) {
if(!activeElementIdList.value.includes(element.elId)) continue if(!activeElementIdList.value.includes(element.elId)) continue

View File

@ -1,19 +1,20 @@
import { Ref, computed } from 'vue' import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
import { createRandomCode } from '@/utils/common' import { createRandomCode } from '@/utils/common'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList) const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
// 组合元素为当前所有激活元素添加一个相同的groupId // 组合元素为当前所有激活元素添加一个相同的groupId
const combineElements = () => { const combineElements = () => {
if(!activeElementList.value.length) return if(!activeElementList.value.length) return
let newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) let newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
const groupId = createRandomCode() const groupId = createRandomCode()
const combineElementList: PPTElement[] = [] const combineElementList: PPTElement[] = []
@ -41,7 +42,7 @@ export default (elementList: Ref<PPTElement[]>) => {
const hasElementInGroup = activeElementList.value.some(item => item.groupId) const hasElementInGroup = activeElementList.value.some(item => item.groupId)
if(!hasElementInGroup) return if(!hasElementInGroup) return
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) { for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId
} }

View File

@ -6,13 +6,15 @@ import { copyText, readClipboard } from '@/utils/clipboard'
import { encrypt } from '@/utils/crypto' import { encrypt } from '@/utils/crypto'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import usePasteTextClipboardData from '@/hooks/usePasteTextClipboardData' import usePasteTextClipboardData from '@/hooks/usePasteTextClipboardData'
import useDeleteElement from './useDeleteElement'
export default (deleteElement: () => void) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList) const activeElementList: Ref<PPTElement[]> = computed(() => store.getters.activeElementList)
const { pasteTextClipboardData } = usePasteTextClipboardData() const { pasteTextClipboardData } = usePasteTextClipboardData()
const { deleteElement } = useDeleteElement()
const copyElement = () => { const copyElement = () => {
if(!activeElementIdList.value.length) return if(!activeElementIdList.value.length) return
@ -24,7 +26,6 @@ export default (deleteElement: () => void) => {
copyText(text).then(() => { copyText(text).then(() => {
store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
message.success('元素已复制到剪贴板', 0.8)
}) })
} }

View File

@ -1,21 +1,22 @@
import { Ref, computed } from 'vue' import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { Slide } from '@/types/slides'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const deleteElement = () => { const deleteElement = () => {
if(!activeElementIdList.value.length) return if(!activeElementIdList.value.length) return
const newElementList = elementList.value.filter(el => !activeElementIdList.value.includes(el.elId)) const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.elId))
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList }) store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
} }
const deleteAllElements = () => { const deleteAllElements = () => {
if(!elementList.value.length) return if(!currentSlide.value.elements.length) return
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
store.commit(MutationTypes.UPDATE_SLIDE, { elements: [] }) store.commit(MutationTypes.UPDATE_SLIDE, { elements: [] })
} }

View File

@ -1,23 +1,24 @@
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { Ref, computed } from 'vue' import { Ref, computed } from 'vue'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const lockElement = (handleElement: PPTElement) => { const lockElement = () => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value)) const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
for(const element of newElementList) { for(const element of newElementList) {
if(activeElementIdList.value.includes(handleElement.elId)) element.isLock = true if(activeElementIdList.value.includes(element.elId)) element.isLock = true
} }
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList }) store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
} }
const unlockElement = (handleElement: PPTElement) => { const unlockElement = (handleElement: PPTElement) => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value)) const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
if(handleElement.groupId) { if(handleElement.groupId) {
for(const element of newElementList) { for(const element of newElementList) {

View File

@ -1,15 +1,16 @@
import { Ref, computed } from 'vue' import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { Slide } from '@/types/slides'
import { KEYS } from '@/configs/hotkey' import { KEYS } from '@/configs/hotkey'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const moveElement = (command: string) => { const moveElement = (command: string) => {
const newElementList = elementList.value.map(el => { const newElementList = currentSlide.value.elements.map(el => {
if(activeElementIdList.value.includes(el.elId)) { if(activeElementIdList.value.includes(el.elId)) {
let { left, top } = el let { left, top } = el
switch(command) { switch(command) {

View File

@ -1,11 +1,12 @@
import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { Ref } from 'vue'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit' import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit'
export default (elementList: Ref<PPTElement[]>) => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
// 获取组合元素层级范围(组合成员中的最大层级和最小层级) // 获取组合元素层级范围(组合成员中的最大层级和最小层级)
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => { const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
@ -167,10 +168,10 @@ export default (elementList: Ref<PPTElement[]>) => {
const orderElement = (element: PPTElement, command: ElementOrderCommand) => { const orderElement = (element: PPTElement, command: ElementOrderCommand) => {
let newElementList = null let newElementList = null
if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList.value, element) if(command === ElementOrderCommands.UP) newElementList = moveUpElement(currentSlide.value.elements, element)
else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(elementList.value, element) else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(currentSlide.value.elements, element)
else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(elementList.value, element) else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(currentSlide.value.elements, element)
else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(elementList.value, element) else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(currentSlide.value.elements, element)
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList }) store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
} }

View File

@ -5,6 +5,11 @@ import { decrypt } from '@/utils/crypto'
import { PPTElement, Slide } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
import { createRandomCode } from '@/utils/common' import { createRandomCode } from '@/utils/common'
interface PasteTextClipboardDataOptions {
onlySlide?: boolean;
onlyElements?: boolean;
}
export default () => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide) const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
@ -37,15 +42,18 @@ export default () => {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, Object.values(elIdMap)) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, Object.values(elIdMap))
} }
const pasteSlide = (slides: Slide[]) => { const pasteSlide = (slide: Slide) => {
console.log(slides) store.commit(MutationTypes.ADD_SLIDE, slide)
} }
const pasteText = (text: string) => { const pasteText = (text: string) => {
console.log(text) console.log(text)
} }
const pasteTextClipboardData = (text: string) => { const pasteTextClipboardData = (text: string, options?: PasteTextClipboardDataOptions) => {
const onlySlide = options?.onlySlide || false
const onlyElements = options?.onlyElements || false
let clipboardData let clipboardData
try { try {
clipboardData = JSON.parse(decrypt(text)) clipboardData = JSON.parse(decrypt(text))
@ -58,12 +66,12 @@ export default () => {
if(typeof clipboardData === 'object') { if(typeof clipboardData === 'object') {
const { type, data } = clipboardData const { type, data } = clipboardData
if(type === 'elements') pasteElement(data) if(type === 'elements' && !onlySlide) pasteElement(data)
else if(type === 'slide') pasteSlide(data) else if(type === 'slide' && !onlyElements) pasteSlide(data)
} }
// 粘贴普通文本 // 粘贴普通文本
else pasteText(clipboardData) else if(!onlyElements && !onlySlide) pasteText(clipboardData)
} }
return { return {

View File

@ -0,0 +1,19 @@
import { Ref, computed } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { Slide } from '@/types/slides'
export default () => {
const store = useStore<State>()
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const selectAllElement = () => {
const unlockedElements = currentSlide.value.elements.filter(el => !el.isLock)
const newActiveElementIdList = unlockedElements.map(el => el.elId)
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
}
return {
selectAllElement,
}
}

View File

@ -0,0 +1,78 @@
import { Ref, computed } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { Slide } from '@/types/slides'
import { createRandomCode } from '@/utils/common'
import { copyText, readClipboard } from '@/utils/clipboard'
import { encrypt } from '@/utils/crypto'
import { KEYS } from '@/configs/hotkey'
import { message } from 'ant-design-vue'
import usePasteTextClipboardData from '@/hooks/usePasteTextClipboardData'
export default () => {
const store = useStore<State>()
const slideIndex = computed(() => store.state.slideIndex)
const slidesLength = computed(() => store.state.slides.length)
const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
const { pasteTextClipboardData } = usePasteTextClipboardData()
const updateSlideIndex = (command: string) => {
let targetIndex = 0
if(command === KEYS.UP && slideIndex.value > 0) {
targetIndex = slideIndex.value - 1
}
else if(command === KEYS.DOWN && slideIndex.value < slidesLength.value - 1) {
targetIndex = slideIndex.value + 1
}
store.commit(MutationTypes.UPDATE_SLIDE_INDEX, targetIndex)
}
const copySlide = () => {
const text = encrypt(JSON.stringify({
type: 'slide',
data: currentSlide.value,
}))
copyText(text).then(() => {
store.commit(MutationTypes.SET_THUMBNAILS_FOCUS, true)
})
}
const pasteSlide = () => {
readClipboard().then(text => {
pasteTextClipboardData(text, { onlySlide: true })
}).catch(err => message.warning(err))
}
const createSlide = () => {
const emptySlide = {
id: createRandomCode(8),
elements: [],
}
store.commit(MutationTypes.ADD_SLIDE, emptySlide)
}
const copyAndPasteSlide = () => {
store.commit(MutationTypes.ADD_SLIDE, currentSlide.value)
}
const deleteSlide = () => {
store.commit(MutationTypes.DELETE_SLIDE, currentSlide.value.id)
}
const cutSlide = () => {
copySlide()
deleteSlide()
}
return {
updateSlideIndex,
copySlide,
pasteSlide,
createSlide,
copyAndPasteSlide,
deleteSlide,
cutSlide,
}
}

View File

@ -29,7 +29,7 @@ export interface State {
const state: State = { const state: State = {
activeElementIdList: [], activeElementIdList: [],
handleElementId: '', handleElementId: '',
editorAreaShowScale: 85, editorAreaShowScale: 90,
thumbnailsFocus: false, thumbnailsFocus: false,
editorAreaFocus: false, editorAreaFocus: false,
disableHotkeys: false, disableHotkeys: false,

View File

@ -56,11 +56,9 @@ export const mutations: MutationTree<State> = {
state.slides = slides state.slides = slides
}, },
[MutationTypes.ADD_SLIDE](state, data: AddSlideData) { [MutationTypes.ADD_SLIDE](state, slide: Slide) {
const { index, slide } = data const addIndex = state.slideIndex + 1
const slides = Array.isArray(slide) ? slide : [slide] state.slides.splice(addIndex, 0, slide)
const addIndex = index !== undefined ? index : (state.slideIndex + 1)
state.slides.splice(addIndex, 0, ...slides)
state.slideIndex = addIndex state.slideIndex = addIndex
}, },

View File

@ -52,4 +52,22 @@ export enum OPERATE_KEYS {
LEFT_BOTTOM = 6, LEFT_BOTTOM = 6,
BOTTOM = 7, BOTTOM = 7,
RIGHT_BOTTOM = 8, RIGHT_BOTTOM = 8,
}
export interface AlignmentLineAxis {
x: number;
y: number;
}
export interface AlignmentLineProps {
type: 'vertical' | 'horizontal';
axis: AlignmentLineAxis;
length: number;
}
export interface MultiSelectRange {
minX: number;
maxX: number;
minY: number;
maxY: number;
} }

View File

@ -1,93 +1,116 @@
import { PPTElement } from '@/types/slides' import { PPTElement } from '@/types/slides'
// 获取矩形旋转后在画布中的位置范围 // 获取矩形旋转后在画布中的位置范围
interface RotatedElementData { interface RotatedElementData {
left: number; left: number;
top: number; top: number;
width: number; width: number;
height: number; height: number;
rotate: number; rotate: number;
} }
export const getRectRotatedRange = (element: RotatedElementData) => { export const getRectRotatedRange = (element: RotatedElementData) => {
const { left, top, width, height, rotate = 0 } = element const { left, top, width, height, rotate = 0 } = element
const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2 const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180 const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180
const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180 const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180
const halfWidth = width / 2 const halfWidth = width / 2
const halfHeight = height / 2 const halfHeight = height / 2
const middleLeft = left + halfWidth const middleLeft = left + halfWidth
const middleTop = top + halfHeight const middleTop = top + halfHeight
const xAxis = [ const xAxis = [
middleLeft + radius * Math.cos(tlbraRadian), middleLeft + radius * Math.cos(tlbraRadian),
middleLeft + radius * Math.cos(trblaRadian), middleLeft + radius * Math.cos(trblaRadian),
middleLeft - radius * Math.cos(tlbraRadian), middleLeft - radius * Math.cos(tlbraRadian),
middleLeft - radius * Math.cos(trblaRadian), middleLeft - radius * Math.cos(trblaRadian),
] ]
const yAxis = [ const yAxis = [
middleTop - radius * Math.sin(tlbraRadian), middleTop - radius * Math.sin(tlbraRadian),
middleTop - radius * Math.sin(trblaRadian), middleTop - radius * Math.sin(trblaRadian),
middleTop + radius * Math.sin(tlbraRadian), middleTop + radius * Math.sin(tlbraRadian),
middleTop + radius * Math.sin(trblaRadian), middleTop + radius * Math.sin(trblaRadian),
] ]
return { return {
xRange: [Math.min(...xAxis), Math.max(...xAxis)], xRange: [Math.min(...xAxis), Math.max(...xAxis)],
yRange: [Math.min(...yAxis), Math.max(...yAxis)], yRange: [Math.min(...yAxis), Math.max(...yAxis)],
} }
} }
// 获取元素在画布中的位置范围 // 获取元素在画布中的位置范围
export const getElementRange = (element: PPTElement) => { export const getElementRange = (element: PPTElement) => {
let minX, maxX, minY, maxY let minX, maxX, minY, maxY
if(element.type === 'line') { if(element.type === 'line') {
minX = element.left minX = element.left
maxX = element.left + Math.max(element.start[0], element.end[0]) maxX = element.left + Math.max(element.start[0], element.end[0])
minY = element.top minY = element.top
maxY = element.top + Math.max(element.start[1], element.end[1]) maxY = element.top + Math.max(element.start[1], element.end[1])
} }
else if('rotate' in element && element.rotate) { else if('rotate' in element && element.rotate) {
const { left, top, width, height, rotate } = element const { left, top, width, height, rotate } = element
const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate }) const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
minX = xRange[0] minX = xRange[0]
maxX = xRange[1] maxX = xRange[1]
minY = yRange[0] minY = yRange[0]
maxY = yRange[1] maxY = yRange[1]
} }
else { else {
minX = element.left minX = element.left
maxX = element.left + element.width maxX = element.left + element.width
minY = element.top minY = element.top
maxY = element.top + element.height maxY = element.top + element.height
} }
return { minX, maxX, minY, maxY } return { minX, maxX, minY, maxY }
} }
// 获取元素集合在画布中的位置范围 // 获取元素集合在画布中的位置范围
export const getElementListRange = (elementList: PPTElement[]) => { export const getElementListRange = (elementList: PPTElement[]) => {
const leftValues: number[] = [] const leftValues: number[] = []
const topValues: number[] = [] const topValues: number[] = []
const rightValues: number[] = [] const rightValues: number[] = []
const bottomValues: number[] = [] const bottomValues: number[] = []
elementList.forEach(element => { elementList.forEach(element => {
const { minX, maxX, minY, maxY } = getElementRange(element) const { minX, maxX, minY, maxY } = getElementRange(element)
leftValues.push(minX) leftValues.push(minX)
topValues.push(minY) topValues.push(minY)
rightValues.push(maxX) rightValues.push(maxX)
bottomValues.push(maxY) bottomValues.push(maxY)
}) })
const minX = Math.min(...leftValues) const minX = Math.min(...leftValues)
const maxX = Math.max(...rightValues) const maxX = Math.max(...rightValues)
const minY = Math.min(...topValues) const minY = Math.min(...topValues)
const maxY = Math.max(...bottomValues) const maxY = Math.max(...bottomValues)
return { minX, maxX, minY, maxY } return { minX, maxX, minY, maxY }
}
export interface AlignLine {
value: number;
range: [number, number];
}
// 对齐参考线去重,对于相同位置的多条参考线,取长度范围的最小值和最大值,并基于此范围将多条参考线合并为一条
export const uniqAlignLines = (lines: AlignLine[]) => {
const uniqLines: AlignLine[] = []
lines.forEach(line => {
const index = uniqLines.findIndex(_line => _line.value === line.value)
if(index === -1) uniqLines.push(line)
else {
const uniqLine = uniqLines[index]
const rangeMin = Math.min(uniqLine.range[0], line.range[0])
const rangeMax = Math.max(uniqLine.range[1], line.range[1])
const range: [number, number] = [rangeMin, rangeMax]
const _line = { value: line.value, range }
uniqLines[index] = _line
}
})
return uniqLines
} }

View File

@ -1,5 +1,9 @@
import mitt, { Emitter } from 'mitt' import mitt, { Emitter } from 'mitt'
export enum EMITTER_EVENTS {
}
const emitter: Emitter = mitt() const emitter: Emitter = mitt()
export default emitter export default emitter

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { PropType } from 'vue' import { PropType } from 'vue'
import { AlignmentLineAxis } from './types/index' import { AlignmentLineAxis } from '@/types/edit'
export default { export default {
name: 'alignment-line', name: 'alignment-line',

View File

@ -26,14 +26,12 @@ import { computed, defineComponent, reactive, PropType, watchEffect, toRefs } fr
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State } from '@/store' import { State } from '@/store'
import { PPTElement, ElementTypes } from '@/types/slides' import { PPTElement, ElementTypes } from '@/types/slides'
import { getElementListRange } from './utils/elementRange' import { getElementListRange } from '@/utils/element'
import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes, OPERATE_KEYS } from '@/types/edit' import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes, MultiSelectRange, OPERATE_KEYS } from '@/types/edit'
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue' import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue' import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import { MultiSelectRange } from './types/index'
export default defineComponent({ export default defineComponent({
name: 'multi-select-operate', name: 'multi-select-operate',
components: { components: {

View File

@ -2,10 +2,9 @@ import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { ElementTypes, PPTElement } from '@/types/slides' import { ElementTypes, PPTElement } from '@/types/slides'
import { AlignmentLineProps } from '../types/index' import { AlignmentLineProps } from '@/types/edit'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getRectRotatedRange } from '../utils/elementRange' import { getRectRotatedRange, AlignLine, uniqAlignLines } from '@/utils/element'
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
export default ( export default (
elementList: Ref<PPTElement[]>, elementList: Ref<PPTElement[]>,

View File

@ -2,7 +2,7 @@ import { Ref, reactive } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides' import { PPTElement } from '@/types/slides'
import { getElementRange } from '../utils/elementRange' import { getElementRange } from '@/utils/element'
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => { export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => {
const store = useStore<State>() const store = useStore<State>()

View File

@ -2,10 +2,9 @@ import { computed, Ref } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { ElementTypes, PPTElement, PPTImageElement, PPTLineElement, PPTShapeElement } from '@/types/slides' import { ElementTypes, PPTElement, PPTImageElement, PPTLineElement, PPTShapeElement } from '@/types/slides'
import { OPERATE_KEYS, ElementScaleHandler } from '@/types/edit' import { OPERATE_KEYS, ElementScaleHandler, AlignmentLineProps, MultiSelectRange } from '@/types/edit'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { AlignLine, uniqAlignLines } from '../utils/alignLines' import { AlignLine, uniqAlignLines } from '@/utils/element'
import { AlignmentLineProps, MultiSelectRange } from '../types/index'
// 计算元素被旋转一定角度后,八个操作点的新坐标 // 计算元素被旋转一定角度后,八个操作点的新坐标
interface RotateElementData { interface RotateElementData {

View File

@ -56,15 +56,6 @@
:selectElement="selectElement" :selectElement="selectElement"
:rotateElement="rotateElement" :rotateElement="rotateElement"
:scaleElement="scaleElement" :scaleElement="scaleElement"
:orderElement="orderElement"
:combineElements="combineElements"
:uncombineElements="uncombineElements"
:alignElementToCanvas="alignElementToCanvas"
:deleteElement="deleteElement"
:lockElement="lockElement"
:unlockElement="unlockElement"
:copyElement="copyElement"
:cutElement="cutElement"
/> />
</div> </div>
</div> </div>
@ -76,21 +67,19 @@ import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { ContextmenuItem } from '@/components/Contextmenu/types' import { ContextmenuItem } from '@/components/Contextmenu/types'
import { PPTElement, Slide } from '@/types/slides' import { PPTElement, Slide } from '@/types/slides'
import { AlignmentLineProps } from './types/index' import { AlignmentLineProps } from '@/types/edit'
import useViewportSize from './hooks/useViewportSize' import useViewportSize from './hooks/useViewportSize'
import useLockElement from './hooks/useLockElement' import useMouseSelection from './hooks/useMouseSelection'
import useDeleteElement from './hooks/useDeleteElement' import useDropImageElement from './hooks/useDropImageElement'
import useCombineElement from './hooks/useCombineElement'
import useOrderElement from './hooks/useOrderElement'
import useAlignElementToCanvas from './hooks/useAlignElementToCanvas'
import useCopyAndPasteElement from './hooks/useCopyAndPasteElement'
import useRotateElement from './hooks/useRotateElement' import useRotateElement from './hooks/useRotateElement'
import useScaleElement from './hooks/useScaleElement' import useScaleElement from './hooks/useScaleElement'
import useSelectElement from './hooks/useSelectElement' import useSelectElement from './hooks/useSelectElement'
import useDragElement from './hooks/useDragElement' import useDragElement from './hooks/useDragElement'
import useMouseSelection from './hooks/useMouseSelection'
import useDropImageElement from './hooks/useDropImageElement' import useDeleteElement from '@/hooks/useDeleteElement'
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
import useSelectAllElement from '@/hooks/useSelectAllElement'
import EditableElement from '@/views/_common/_element/EditableElement.vue' import EditableElement from '@/views/_common/_element/EditableElement.vue'
import MouseSelection from './MouseSelection.vue' import MouseSelection from './MouseSelection.vue'
@ -137,15 +126,13 @@ export default defineComponent({
const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef, canvasScale) const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef, canvasScale)
const { dragElement } = useDragElement(elementList, activeGroupElementId, canvasScale, alignmentLines) const { dragElement } = useDragElement(elementList, activeGroupElementId, canvasScale, alignmentLines)
const { selectElement, selectAllElement } = useSelectElement(elementList, activeGroupElementId, dragElement) const { selectElement } = useSelectElement(elementList, activeGroupElementId, dragElement)
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, canvasScale, activeGroupElementId, alignmentLines) const { scaleElement, scaleMultiElement } = useScaleElement(elementList, canvasScale, activeGroupElementId, alignmentLines)
const { rotateElement } = useRotateElement(elementList, viewportRef, canvasScale) const { rotateElement } = useRotateElement(elementList, viewportRef, canvasScale)
const { orderElement } = useOrderElement(elementList)
const { alignElementToCanvas } = useAlignElementToCanvas(elementList) const { selectAllElement } = useSelectAllElement()
const { combineElements, uncombineElements } = useCombineElement(elementList) const { deleteAllElements } = useDeleteElement()
const { deleteElement, deleteAllElements } = useDeleteElement(elementList) const { pasteElement } = useCopyAndPasteElement()
const { lockElement, unlockElement } = useLockElement(elementList)
const { copyElement, cutElement, pasteElement } = useCopyAndPasteElement(deleteElement)
const handleClickBlankArea = (e: MouseEvent) => { const handleClickBlankArea = (e: MouseEvent) => {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
@ -195,15 +182,6 @@ export default defineComponent({
rotateElement, rotateElement,
scaleElement, scaleElement,
scaleMultiElement, scaleMultiElement,
orderElement,
combineElements,
uncombineElements,
alignElementToCanvas,
deleteElement,
lockElement,
unlockElement,
copyElement,
cutElement,
contextmenus, contextmenus,
} }
}, },

View File

@ -1,17 +0,0 @@
export interface AlignmentLineAxis {
x: number;
y: number;
}
export interface AlignmentLineProps {
type: 'vertical' | 'horizontal';
axis: AlignmentLineAxis;
length: number;
}
export interface MultiSelectRange {
minX: number;
maxX: number;
minY: number;
maxY: number;
}

View File

@ -1,22 +0,0 @@
export interface AlignLine {
value: number;
range: [number, number];
}
// 对齐参考线去重,对于相同位置的多条参考线,取长度范围的最小值和最大值,并基于此范围将多条参考线合并为一条
export const uniqAlignLines = (lines: AlignLine[]) => {
const uniqLines: AlignLine[] = []
lines.forEach(line => {
const index = uniqLines.findIndex(_line => _line.value === line.value)
if(index === -1) uniqLines.push(line)
else {
const uniqLine = uniqLines[index]
const rangeMin = Math.min(uniqLine.range[0], line.range[0])
const rangeMax = Math.max(uniqLine.range[1], line.range[1])
const range: [number, number] = [rangeMin, rangeMax]
const _line = { value: line.value, range }
uniqLines[index] = _line
}
})
return uniqLines
}

View File

@ -23,7 +23,7 @@
@mousedown="changSlideIndex(index)" @mousedown="changSlideIndex(index)"
v-contextmenu="contextmenus" v-contextmenu="contextmenus"
> >
<div class="slide-index">{{ fillDigit(index + 1) }}</div> <div class="slide-index">{{ fillDigit(index + 1, 2) }}</div>
<div class="thumbnail"></div> <div class="thumbnail"></div>
</div> </div>
</template> </template>
@ -38,6 +38,7 @@ import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { fillDigit } from '@/utils/common' import { fillDigit } from '@/utils/common'
import { ContextmenuItem } from '@/components/Contextmenu/types' import { ContextmenuItem } from '@/components/Contextmenu/types'
import useSlideHandler from '@/hooks/useSlideHandler'
export default defineComponent({ export default defineComponent({
name: 'thumbnails', name: 'thumbnails',
@ -49,6 +50,15 @@ export default defineComponent({
const slides = computed(() => store.state.slides) const slides = computed(() => store.state.slides)
const slideIndex = computed(() => store.state.slideIndex) const slideIndex = computed(() => store.state.slideIndex)
const {
copySlide,
pasteSlide,
createSlide,
copyAndPasteSlide,
deleteSlide,
cutSlide,
} = useSlideHandler()
const changSlideIndex = (index: number) => { const changSlideIndex = (index: number) => {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
@ -75,25 +85,6 @@ export default defineComponent({
store.commit(MutationTypes.UPDATE_SLIDE_INDEX, newIndex) store.commit(MutationTypes.UPDATE_SLIDE_INDEX, newIndex)
} }
const cutSlide = () => {
console.log('cutSlide')
}
const copySlide = () => {
console.log('copySlide')
}
const pasteSlide = () => {
console.log('pasteSlide')
}
const createSlide = () => {
console.log('createSlide')
}
const copyAndPasteSlide = () => {
console.log('copyAndPasteSlide')
}
const deleteSlide = () => {
console.log('deleteSlide')
}
const contextmenus = (): ContextmenuItem[] => { const contextmenus = (): ContextmenuItem[] => {
return [ return [
{ {

View File

@ -2,50 +2,98 @@ import { computed, onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { KEYS } from '@/configs/hotkey' import { KEYS } from '@/configs/hotkey'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import useSlideHandler from '@/hooks/useSlideHandler'
import useLockElement from '@/hooks/useLockElement'
import useDeleteElement from '@/hooks/useDeleteElement'
import useCombineElement from '@/hooks/useCombineElement'
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
import useSelectAllElement from '@/hooks/useSelectAllElement'
import useMoveElement from '@/hooks/useMoveElement'
export default () => { export default () => {
const store = useStore<State>() const store = useStore<State>()
const ctrlKeyActive = computed(() => store.state.ctrlKeyState) const ctrlKeyActive = computed(() => store.state.ctrlKeyState)
const shiftKeyActive = computed(() => store.state.shiftKeyState) const shiftKeyActive = computed(() => store.state.shiftKeyState)
const disableHotkeys = computed(() => store.state.disableHotkeys)
const activeElementIdList = computed(() => store.state.activeElementIdList)
const editorAreaFocus = computed(() => store.state.editorAreaFocus) const editorAreaFocus = computed(() => store.state.editorAreaFocus)
const thumbnailsFocus = computed(() => store.state.thumbnailsFocus) const thumbnailsFocus = computed(() => store.state.thumbnailsFocus)
const {
updateSlideIndex,
copySlide,
createSlide,
deleteSlide,
cutSlide,
} = useSlideHandler()
const { combineElements, uncombineElements } = useCombineElement()
const { deleteElement } = useDeleteElement()
const { lockElement } = useLockElement()
const { copyElement, cutElement } = useCopyAndPasteElement()
const { selectAllElement } = useSelectAllElement()
const { moveElement } = useMoveElement()
const copy = () => { const copy = () => {
message.success('copy') if(disableHotkeys.value) return
if(thumbnailsFocus.value) copySlide()
else if(activeElementIdList.value.length) copyElement()
} }
const cut = () => { const cut = () => {
message.success('cut') if(disableHotkeys.value) return
if(thumbnailsFocus.value) cutSlide()
else if(activeElementIdList.value.length) cutElement()
} }
const undo = () => { const undo = () => {
message.success('undo') message.success('undo')
} }
const redo = () => { const redo = () => {
message.success('redo') message.success('redo')
} }
const selectAll = () => { const selectAll = () => {
message.success('selectAll') if(!editorAreaFocus.value && disableHotkeys.value) return
selectAllElement()
} }
const lock = () => { const lock = () => {
message.success('lock') if(!editorAreaFocus.value && disableHotkeys.value) return
lockElement()
} }
const combine = () => { const combine = () => {
message.success('combine') if(!editorAreaFocus.value && disableHotkeys.value) return
combineElements()
} }
const uncombine = () => { const uncombine = () => {
message.success('uncombine') if(!editorAreaFocus.value && disableHotkeys.value) return
uncombineElements()
} }
const remove = () => { const remove = () => {
message.success('remove') if(disableHotkeys.value) return
if(thumbnailsFocus.value) deleteSlide()
else if(activeElementIdList.value.length) deleteElement()
} }
const move = (key: string) => { const move = (key: string) => {
message.success(`move: ${key}`) if(disableHotkeys.value) return
if(thumbnailsFocus.value && (key === KEYS.UP || key === KEYS.DOWN)) {
updateSlideIndex(key)
}
else if(activeElementIdList.value.length) moveElement(key)
} }
const create = () => { const create = () => {
message.success('create') if(!thumbnailsFocus.value || disableHotkeys.value) return
createSlide()
} }
const keydownListener = (e: KeyboardEvent) => { const keydownListener = (e: KeyboardEvent) => {

View File

@ -27,13 +27,14 @@ import { computed, defineComponent, PropType } from 'vue'
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides' import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides'
import { ContextmenuItem } from '@/components/Contextmenu/types' import { ContextmenuItem } from '@/components/Contextmenu/types'
import { import useLockElement from '@/hooks/useLockElement'
ElementOrderCommand, import useDeleteElement from '@/hooks/useDeleteElement'
ElementOrderCommands, import useCombineElement from '@/hooks/useCombineElement'
ElementAlignCommand, import useOrderElement from '@/hooks/useOrderElement'
ElementAlignCommands, import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
ElementScaleHandler, import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
} from '@/types/edit'
import { ElementOrderCommands, ElementAlignCommands, ElementScaleHandler } from '@/types/edit'
import ImageElement from './ImageElement/index.vue' import ImageElement from './ImageElement/index.vue'
import TextElement from './TextElement/index.vue' import TextElement from './TextElement/index.vue'
@ -85,42 +86,6 @@ export default defineComponent({
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => void>, type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => void>,
required: true, required: true,
}, },
orderElement: {
type: Function as PropType<(element: PPTElement, command: ElementOrderCommand) => void>,
required: true,
},
combineElements: {
type: Function as PropType<() => void>,
required: true,
},
uncombineElements: {
type: Function as PropType<() => void>,
required: true,
},
alignElementToCanvas: {
type: Function as PropType<(command: ElementAlignCommand) => void>,
required: true,
},
deleteElement: {
type: Function as PropType<() => void>,
required: true,
},
lockElement: {
type: Function as PropType<(element: PPTElement) => void>,
required: true,
},
unlockElement: {
type: Function as PropType<(element: PPTElement) => void>,
required: true,
},
copyElement: {
type: Function as PropType<() => void>,
required: true,
},
cutElement: {
type: Function as PropType<() => void>,
required: true,
},
}, },
setup(props) { setup(props) {
const currentElementComponent = computed(() => { const currentElementComponent = computed(() => {
@ -131,12 +96,19 @@ export default defineComponent({
return elementTypeMap[props.elementInfo.type] || null return elementTypeMap[props.elementInfo.type] || null
}) })
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { combineElements, uncombineElements } = useCombineElement()
const { deleteElement } = useDeleteElement()
const { lockElement, unlockElement } = useLockElement()
const { copyElement, cutElement } = useCopyAndPasteElement()
const contextmenus = (): ContextmenuItem[] => { const contextmenus = (): ContextmenuItem[] => {
if(props.elementInfo.isLock) { if(props.elementInfo.isLock) {
return [{ return [{
text: '解锁', text: '解锁',
icon: 'icon-unlock', icon: 'icon-unlock',
handler: () => props.unlockElement(props.elementInfo), handler: () => unlockElement(props.elementInfo),
}] }]
} }
@ -145,13 +117,13 @@ export default defineComponent({
text: '剪切', text: '剪切',
subText: 'Ctrl + X', subText: 'Ctrl + X',
icon: 'icon-scissor', icon: 'icon-scissor',
handler: props.cutElement, handler: cutElement,
}, },
{ {
text: '复制', text: '复制',
subText: 'Ctrl + C', subText: 'Ctrl + C',
icon: 'icon-copy', icon: 'icon-copy',
handler: props.copyElement, handler: copyElement,
}, },
{ divider: true }, { divider: true },
{ {
@ -159,29 +131,29 @@ export default defineComponent({
icon: 'icon-top-layer', icon: 'icon-top-layer',
disable: props.isMultiSelect && !props.elementInfo.groupId, disable: props.isMultiSelect && !props.elementInfo.groupId,
children: [ children: [
{ text: '置顶层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.TOP) }, { text: '置顶层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP) },
{ text: '置底层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) }, { text: '置底层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
{ divider: true }, { divider: true },
{ text: '上移一层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.UP) }, { text: '上移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.UP) },
{ text: '下移一层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.DOWN) }, { text: '下移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
], ],
}, },
{ {
text: '水平对齐', text: '水平对齐',
icon: 'icon-align-left', icon: 'icon-align-left',
children: [ children: [
{ text: '水平居中', handler: () => props.alignElementToCanvas(ElementAlignCommands.HORIZONTAL) }, { text: '水平居中', handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
{ text: '左对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.LEFT) }, { text: '左对齐', handler: () => alignElementToCanvas(ElementAlignCommands.LEFT) },
{ text: '右对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.RIGHT) }, { text: '右对齐', handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT) },
], ],
}, },
{ {
text: '垂直对齐', text: '垂直对齐',
icon: 'icon-align-bottom', icon: 'icon-align-bottom',
children: [ children: [
{ text: '垂直居中', handler: () => props.alignElementToCanvas(ElementAlignCommands.VERTICAL) }, { text: '垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL) },
{ text: '上对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.TOP) }, { text: '上对齐', handler: () => alignElementToCanvas(ElementAlignCommands.TOP) },
{ text: '下对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.BOTTOM) }, { text: '下对齐', handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM) },
], ],
}, },
{ divider: true }, { divider: true },
@ -189,20 +161,20 @@ export default defineComponent({
text: props.elementInfo.groupId ? '取消组合' : '组合', text: props.elementInfo.groupId ? '取消组合' : '组合',
subText: 'Ctrl + G', subText: 'Ctrl + G',
icon: 'icon-block', icon: 'icon-block',
handler: props.elementInfo.groupId ? props.uncombineElements : props.combineElements, handler: props.elementInfo.groupId ? uncombineElements : combineElements,
hide: !props.isMultiSelect, hide: !props.isMultiSelect,
}, },
{ {
text: '锁定', text: '锁定',
subText: 'Ctrl + L', subText: 'Ctrl + L',
icon: 'icon-lock', icon: 'icon-lock',
handler: () => props.lockElement(props.elementInfo), handler: lockElement,
}, },
{ {
text: '删除', text: '删除',
subText: 'Delete', subText: 'Delete',
icon: 'icon-delete', icon: 'icon-delete',
handler: props.deleteElement, handler: deleteElement,
}, },
] ]
} }