refactor: script setup 语法重构

This commit is contained in:
pipipi-pikachu 2022-06-17 17:28:49 +08:00
parent 1c4ad8eebd
commit 16acbc6333
143 changed files with 7408 additions and 9249 deletions

View File

@ -6,6 +6,7 @@ module.exports = {
root: true,
env: {
node: true,
'vue/setup-compiler-macros': true,
},
extends: [
'plugin:vue/vue3-essential',
@ -60,6 +61,7 @@ module.exports = {
'no-useless-return': 'error',
'array-bracket-spacing': 'error',
'no-useless-escape': 'off',
'no-unused-vars': 'off',
'no-eval': 'error',
'no-var': 'error',
'no-with': 'error',
@ -73,6 +75,10 @@ module.exports = {
'{}': false,
},
}],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-reserved-component-names': 'off',
},
overrides: [
{

117
package-lock.json generated
View File

@ -7007,37 +7007,112 @@
}
},
"eslint-plugin-vue": {
"version": "7.20.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-vue/download/eslint-plugin-vue-7.20.0.tgz?cache=0&sync_timestamp=1635570504787&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-plugin-vue%2Fdownload%2Feslint-plugin-vue-7.20.0.tgz",
"integrity": "sha1-mMIYhaa/3wcTw6kpV6Wv6q7tklM=",
"version": "9.1.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.1.0.tgz",
"integrity": "sha512-EPCeInPicQ/YyfOWJDr1yfEeSNoFCMzUus107lZyYi37xejdOolNzS5MXGXp8+9bkoKZMdv/1AcZzQebME6r+g==",
"dev": true,
"requires": {
"eslint-utils": "^2.1.0",
"eslint-utils": "^3.0.0",
"natural-compare": "^1.4.0",
"semver": "^6.3.0",
"vue-eslint-parser": "^7.10.0"
"nth-check": "^2.0.1",
"postcss-selector-parser": "^6.0.9",
"semver": "^7.3.5",
"vue-eslint-parser": "^9.0.1",
"xml-name-validator": "^4.0.0"
},
"dependencies": {
"eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.nlark.com/eslint-utils/download/eslint-utils-2.1.0.tgz",
"integrity": "sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc=",
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
"ms": "2.1.2"
}
},
"eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
}
},
"eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz?cache=0&sync_timestamp=1636378420914&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-1.3.0.tgz",
"integrity": "sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=",
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1616463550093&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
"integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
"espree": {
"version": "9.3.2",
"resolved": "https://registry.npmmirror.com/espree/-/espree-9.3.2.tgz",
"integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
"dev": true,
"requires": {
"acorn": "^8.7.1",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
}
},
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dev": true,
"requires": {
"boolbase": "^1.0.0"
}
},
"postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
}
},
"vue-eslint-parser": {
"version": "9.0.2",
"resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.0.2.tgz",
"integrity": "sha512-uCPQwTGjOtAYrwnU+76pYxalhjsh7iFBsHwBqDHiOPTxtICDaraO4Szw54WFTNZTAEsgHHzqFOu1mmnBOBRzDA==",
"dev": true,
"requires": {
"debug": "^4.3.4",
"eslint-scope": "^7.1.1",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^7.3.6"
},
"dependencies": {
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
}
}
},
@ -17370,6 +17445,12 @@
"async-limiter": "~1.0.0"
}
},
"xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.nlark.com/xtend/download/xtend-4.0.2.tgz",

View File

@ -73,7 +73,7 @@
"@vue/test-utils": "^2.0.0-0",
"babel-plugin-import": "^1.13.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.1.0",
"eslint-plugin-vue": "^9.1.0",
"husky": "^7.0.2",
"less": "^4.1.1",
"less-loader": "^7.1.0",

View File

@ -1,11 +1,11 @@
<template>
<Screen v-if="screening" />
<Editor v-else-if="isPC" />
<Editor v-else-if="_isPC" />
<Mobile v-else />
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue'
<script lang="ts" setup>
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useScreenStore, useMainStore, useSnapshotStore } from '@/store'
import { LOCALSTORAGE_KEY_DISCARDED_DB } from '@/configs/storage'
@ -15,44 +15,31 @@ import Editor from './views/Editor/index.vue'
import Screen from './views/Screen/index.vue'
import Mobile from './views/Mobile/index.vue'
export default defineComponent({
name: 'app',
components: {
Editor,
Screen,
Mobile,
},
setup() {
const mainStore = useMainStore()
const snapshotStore = useSnapshotStore()
const { databaseId } = storeToRefs(mainStore)
const { screening } = storeToRefs(useScreenStore())
const _isPC = isPC()
if (process.env.NODE_ENV === 'production') {
window.onbeforeunload = () => false
}
const mainStore = useMainStore()
const snapshotStore = useSnapshotStore()
const { databaseId } = storeToRefs(mainStore)
const { screening } = storeToRefs(useScreenStore())
onMounted(() => {
snapshotStore.initSnapshotDatabase()
mainStore.setAvailableFonts()
})
if (process.env.NODE_ENV === 'production') {
window.onbeforeunload = () => false
}
// localStorage indexedDB ID
window.addEventListener('unload', () => {
const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
onMounted(() => {
snapshotStore.initSnapshotDatabase()
mainStore.setAvailableFonts()
})
discardedDBList.push(databaseId.value)
// localStorage indexedDB ID
window.addEventListener('unload', () => {
const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
const newDiscardedDB = JSON.stringify(discardedDBList)
localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
})
discardedDBList.push(databaseId.value)
return {
screening,
isPC: isPC(),
}
},
const newDiscardedDB = JSON.stringify(discardedDBList)
localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
})
</script>

View File

@ -4,16 +4,11 @@
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'checkbox-button',
props: {
checked: {
type: Boolean,
default: false,
},
<script lang="ts" setup>
defineProps({
checked: {
type: Boolean,
default: false,
},
})
</script>

View File

@ -4,12 +4,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
export default defineComponent({
name: 'checkbox-button-group',
})
</script>
<style lang="scss" scoped>

View File

@ -16,75 +16,63 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref } from 'vue'
<script lang="ts" setup>
import { computed, onUnmounted, PropType, ref } from 'vue'
import Checkboard from './Checkboard.vue'
import { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'alpha',
components: {
Checkboard,
},
emits: ['colorChange'],
props: {
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const color = computed(() => props.value)
const gradientColor = computed(() => {
const rgbaStr = [color.value.r, color.value.g, color.value.b].join(',')
return `linear-gradient(to right, rgba(${rgbaStr}, 0) 0%, rgba(${rgbaStr}, 1) 100%)`
})
const alphaRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!alphaRef.value) return
const containerWidth = alphaRef.value.clientWidth
const xOffset = alphaRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let a
if (left < 0) a = 0
else if (left > containerWidth) a = 1
else a = Math.round(left * 100 / containerWidth) / 100
if (color.value.a !== a) {
emit('colorChange', {
r: color.value.r,
g: color.value.g,
b: color.value.b,
a: a,
})
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
alphaRef,
gradientColor,
handleMouseDown,
color,
}
const props = defineProps({
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
})
const emit = defineEmits<{
(event: 'colorChange', payload: ColorFormats.RGBA): void
}>()
const color = computed(() => props.value)
const gradientColor = computed(() => {
const rgbaStr = [color.value.r, color.value.g, color.value.b].join(',')
return `linear-gradient(to right, rgba(${rgbaStr}, 0) 0%, rgba(${rgbaStr}, 1) 100%)`
})
const alphaRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!alphaRef.value) return
const containerWidth = alphaRef.value.clientWidth
const xOffset = alphaRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let a
if (left < 0) a = 0
else if (left > containerWidth) a = 1
else a = Math.round(left * 100 / containerWidth) / 100
if (color.value.a !== a) {
emit('colorChange', {
r: color.value.r,
g: color.value.g,
b: color.value.b,
a: a,
})
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
</script>
<style lang="scss" scoped>

View File

@ -2,8 +2,23 @@
<div class="checkerboard" :style="bgStyle"></div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps({
size: {
type: Number,
default: 8,
},
white: {
type: String,
default: '#fff',
},
grey: {
type: String,
default: '#e6e6e6',
},
})
const checkboardCache = {}
@ -32,33 +47,9 @@ const getCheckboard = (white: string, grey: string, size: number) => {
return checkboard
}
export default defineComponent({
name: 'checkboard',
emits: ['colorChange'],
props: {
size: {
type: Number,
default: 8,
},
white: {
type: String,
default: '#fff',
},
grey: {
type: String,
default: '#e6e6e6',
},
},
setup(props) {
const bgStyle = computed(() => {
const checkboard = getCheckboard(props.white, props.grey, props.size)
return { backgroundImage: `url(${checkboard})` }
})
return {
bgStyle,
}
},
const bgStyle = computed(() => {
const checkboard = getCheckboard(props.white, props.grey, props.size)
return { backgroundImage: `url(${checkboard})` }
})
</script>

View File

@ -8,38 +8,32 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'editable-input',
emits: ['colorChange'],
props: {
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const val = computed(() => {
let _hex = ''
if (props.value.a < 1) _hex = tinycolor(props.value).toHex8String().toUpperCase()
else _hex = tinycolor(props.value).toHexString().toUpperCase()
return _hex.replace('#', '')
})
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
if (value.length >= 6) emit('colorChange', tinycolor(value).toRgb())
}
return {
val,
handleInput,
}
const props = defineProps({
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
})
const emit = defineEmits<{
(event: 'colorChange', payload: ColorFormats.RGBA): void
}>()
const val = computed(() => {
let _hex = ''
if (props.value.a < 1) _hex = tinycolor(props.value).toHex8String().toUpperCase()
else _hex = tinycolor(props.value).toHexString().toUpperCase()
return _hex.replace('#', '')
})
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
if (value.length >= 6) emit('colorChange', tinycolor(value).toRgb())
}
</script>
<style lang="scss" scoped>

View File

@ -15,91 +15,83 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, onUnmounted, PropType, ref, watch } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'hue',
emits: ['colorChange'],
props: {
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
hue: {
type: Number,
required: true,
},
const props = defineProps({
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
setup(props, { emit }) {
const oldHue = ref(0)
const pullDirection = ref('')
const color = computed(() => {
const hsla = tinycolor(props.value).toHsl()
if (props.hue !== -1) hsla.h = props.hue
return hsla
})
const pointerLeft = computed(() => {
if (color.value.h === 0 && pullDirection.value === 'right') return '100%'
return color.value.h * 100 / 360 + '%'
})
watch(() => props.value, () => {
const hsla = tinycolor(props.value).toHsl()
const h = hsla.s === 0 ? props.hue : hsla.h
if (h !== 0 && h - oldHue.value > 0) pullDirection.value = 'right'
if (h !== 0 && h - oldHue.value < 0) pullDirection.value = 'left'
oldHue.value = h
})
const hueRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!hueRef.value) return
const containerWidth = hueRef.value.clientWidth
const xOffset = hueRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let h, percent
if (left < 0) h = 0
else if (left > containerWidth) h = 360
else {
percent = left * 100 / containerWidth
h = 360 * percent / 100
}
if (props.hue === -1 || color.value.h !== h) {
emit('colorChange', {
h,
l: color.value.l,
s: color.value.s,
a: color.value.a,
})
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
hueRef,
handleMouseDown,
pointerLeft,
}
hue: {
type: Number,
required: true,
},
})
const emit = defineEmits<{
(event: 'colorChange', payload: ColorFormats.HSLA): void
}>()
const oldHue = ref(0)
const pullDirection = ref('')
const color = computed(() => {
const hsla = tinycolor(props.value).toHsl()
if (props.hue !== -1) hsla.h = props.hue
return hsla
})
const pointerLeft = computed(() => {
if (color.value.h === 0 && pullDirection.value === 'right') return '100%'
return color.value.h * 100 / 360 + '%'
})
watch(() => props.value, () => {
const hsla = tinycolor(props.value).toHsl()
const h = hsla.s === 0 ? props.hue : hsla.h
if (h !== 0 && h - oldHue.value > 0) pullDirection.value = 'right'
if (h !== 0 && h - oldHue.value < 0) pullDirection.value = 'left'
oldHue.value = h
})
const hueRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!hueRef.value) return
const containerWidth = hueRef.value.clientWidth
const xOffset = hueRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let h, percent
if (left < 0) h = 0
else if (left > containerWidth) h = 360
else {
percent = left * 100 / containerWidth
h = 360 * percent / 100
}
if (props.hue === -1 || color.value.h !== h) {
emit('colorChange', {
h,
l: color.value.l,
s: color.value.s,
a: color.value.a,
})
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
</script>
<style lang="scss" scoped>

View File

@ -18,83 +18,73 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref } from 'vue'
<script lang="ts" setup>
import { computed, onUnmounted, PropType, ref } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
import { throttle, clamp } from 'lodash'
export default defineComponent({
name: 'saturation',
emits: ['colorChange'],
props: {
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
hue: {
type: Number,
required: true,
},
const props = defineProps({
value: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
setup(props, { emit }) {
const color = computed(() => {
const hsva = tinycolor(props.value).toHsv()
if (props.hue !== -1) hsva.h = props.hue
return hsva
})
const bgColor = computed(() => `hsl(${color.value.h}, 100%, 50%)`)
const pointerTop = computed(() => (-(color.value.v * 100) + 1) + 100 + '%')
const pointerLeft = computed(() => color.value.s * 100 + '%')
const emitChangeEvent = throttle(function(param) {
emit('colorChange', param)
}, 20, { leading: true, trailing: false })
const saturationRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!saturationRef.value) return
const containerWidth = saturationRef.value.clientWidth
const containerHeight = saturationRef.value.clientHeight
const xOffset = saturationRef.value.getBoundingClientRect().left + window.pageXOffset
const yOffset = saturationRef.value.getBoundingClientRect().top + window.pageYOffset
const left = clamp(e.pageX - xOffset, 0, containerWidth)
const top = clamp(e.pageY - yOffset, 0, containerHeight)
const saturation = left / containerWidth
const bright = clamp(-(top / containerHeight) + 1, 0, 1)
emitChangeEvent({
h: color.value.h,
s: saturation,
v: bright,
a: color.value.a,
})
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
saturationRef,
bgColor,
handleMouseDown,
pointerTop,
pointerLeft,
}
hue: {
type: Number,
required: true,
},
})
const emit = defineEmits<{
(event: 'colorChange', payload: ColorFormats.HSVA): void
}>()
const color = computed(() => {
const hsva = tinycolor(props.value).toHsv()
if (props.hue !== -1) hsva.h = props.hue
return hsva
})
const bgColor = computed(() => `hsl(${color.value.h}, 100%, 50%)`)
const pointerTop = computed(() => (-(color.value.v * 100) + 1) + 100 + '%')
const pointerLeft = computed(() => color.value.s * 100 + '%')
const emitChangeEvent = throttle(function(param: ColorFormats.HSVA) {
emit('colorChange', param)
}, 20, { leading: true, trailing: false })
const saturationRef = ref<HTMLElement>()
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if (!saturationRef.value) return
const containerWidth = saturationRef.value.clientWidth
const containerHeight = saturationRef.value.clientHeight
const xOffset = saturationRef.value.getBoundingClientRect().left + window.pageXOffset
const yOffset = saturationRef.value.getBoundingClientRect().top + window.pageYOffset
const left = clamp(e.pageX - xOffset, 0, containerWidth)
const top = clamp(e.pageY - yOffset, 0, containerHeight)
const saturation = left / containerWidth
const bright = clamp(-(top / containerHeight) + 1, 0, 1)
emitChangeEvent({
h: color.value.h,
s: saturation,
v: bright,
a: color.value.a,
})
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
</script>
<style lang="scss" scoped>

View File

@ -72,8 +72,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
import { debounce } from 'lodash'
import { toCanvas } from 'html-to-image'
@ -86,6 +86,17 @@ import EditableInput from './EditableInput.vue'
import { message } from 'ant-design-vue'
const props = defineProps({
modelValue: {
type: String,
default: '#e86b99',
},
})
const emit = defineEmits<{
(event: 'update:modelValue', payload: string): void
}>()
const RECENT_COLORS = 'RECENT_COLORS'
const presetColorConfig = [
@ -132,170 +143,138 @@ const getPresetColors = () => {
const themeColors = ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c']
const standardColors = ['#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57', '#00afee', '#0071be', '#00215f', '#72349d']
export default defineComponent({
name: 'color-picker',
components: {
Alpha,
Checkboard,
Hue,
Saturation,
EditableInput,
const hue = ref(-1)
const recentColors = ref<string[]>([])
const color = computed({
get() {
return tinycolor(props.modelValue).toRgb()
},
emits: ['update:modelValue'],
props: {
modelValue: {
type: String,
default: '#e86b99',
},
},
setup(props, { emit }) {
const hue = ref(-1)
const recentColors = ref<string[]>([])
const color = computed({
get() {
return tinycolor(props.modelValue).toRgb()
},
set(rgba: ColorFormats.RGBA) {
const rgbaString = `rgba(${[rgba.r, rgba.g, rgba.b, rgba.a].join(',')})`
emit('update:modelValue', rgbaString)
},
})
const presetColors = getPresetColors()
const currentColor = computed(() => {
return `rgba(${[color.value.r, color.value.g, color.value.b, color.value.a].join(',')})`
})
const selectPresetColor = (colorString: string) => {
hue.value = tinycolor(colorString).toHsl().h
emit('update:modelValue', colorString)
}
// 使
const updateRecentColorsCache = debounce(function() {
const _color = tinycolor(color.value).toRgbString()
if (!recentColors.value.includes(_color)) {
recentColors.value = [_color, ...recentColors.value]
const maxLength = 10
if (recentColors.value.length > maxLength) {
recentColors.value = recentColors.value.slice(0, maxLength)
}
}
}, 300, { trailing: true })
onMounted(() => {
const recentColorsCache = localStorage.getItem(RECENT_COLORS)
if (recentColorsCache) recentColors.value = JSON.parse(recentColorsCache)
})
watch(recentColors, () => {
const recentColorsCache = JSON.stringify(recentColors.value)
localStorage.setItem(RECENT_COLORS, recentColorsCache)
})
const changeColor = (value: ColorFormats.RGBA | ColorFormats.HSLA | ColorFormats.HSVA) => {
if ('h' in value) {
hue.value = value.h
color.value = tinycolor(value).toRgb()
}
else {
hue.value = tinycolor(value).toHsl().h
color.value = value
}
updateRecentColorsCache()
}
const pickColor = () => {
const targetRef: HTMLElement | null = document.querySelector('.canvas')
if (!targetRef) return
const maskRef = document.createElement('div')
maskRef.style.cssText = 'position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 9999; cursor: wait;'
document.body.appendChild(maskRef)
const colorBlockRef = document.createElement('div')
colorBlockRef.style.cssText = 'position: absolute; top: -100px; left: -100px; width: 16px; height: 16px; border: 1px solid #000; z-index: 999'
maskRef.appendChild(colorBlockRef)
const { left, top, width, height } = targetRef.getBoundingClientRect()
const filter = (node: HTMLElement) => {
if (node.tagName && node.tagName.toUpperCase() === 'FOREIGNOBJECT') return false
if (node.classList && node.classList.contains('operate')) return false
return true
}
toCanvas(targetRef, { filter, fontEmbedCSS: '', width, height, canvasWidth: width, canvasHeight: height, pixelRatio: 1 }).then(canvasRef => {
canvasRef.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; cursor: crosshair;`
maskRef.style.cursor = 'default'
maskRef.appendChild(canvasRef)
const ctx = canvasRef.getContext('2d')
if (!ctx) return
let currentColor = ''
const handleMousemove = (e: MouseEvent) => {
const x = e.x
const y = e.y
const mouseX = x - left
const mouseY = y - top
const [r, g, b, a] = ctx.getImageData(mouseX, mouseY, 1, 1).data
currentColor = `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(2)})`
colorBlockRef.style.left = x + 10 + 'px'
colorBlockRef.style.top = y + 10 + 'px'
colorBlockRef.style.backgroundColor = currentColor
}
const handleMouseleave = () => {
currentColor = ''
colorBlockRef.style.left = '-100px'
colorBlockRef.style.top = '-100px'
colorBlockRef.style.backgroundColor = ''
}
const handleMousedown = (e: MouseEvent) => {
if (currentColor && e.button === 0) {
const tColor = tinycolor(currentColor)
hue.value = tColor.toHsl().h
color.value = tColor.toRgb()
updateRecentColorsCache()
}
document.body.removeChild(maskRef)
canvasRef.removeEventListener('mousemove', handleMousemove)
canvasRef.removeEventListener('mouseleave', handleMouseleave)
window.removeEventListener('mousedown', handleMousedown)
}
canvasRef.addEventListener('mousemove', handleMousemove)
canvasRef.addEventListener('mouseleave', handleMouseleave)
window.addEventListener('mousedown', handleMousedown)
}).catch(() => {
message.error('取色吸管初始化失败')
document.body.removeChild(maskRef)
})
}
return {
themeColors,
standardColors,
presetColors,
color,
hue,
currentColor,
changeColor,
selectPresetColor,
recentColors,
pickColor,
}
set(rgba: ColorFormats.RGBA) {
const rgbaString = `rgba(${[rgba.r, rgba.g, rgba.b, rgba.a].join(',')})`
emit('update:modelValue', rgbaString)
},
})
const presetColors = getPresetColors()
const currentColor = computed(() => {
return `rgba(${[color.value.r, color.value.g, color.value.b, color.value.a].join(',')})`
})
const selectPresetColor = (colorString: string) => {
hue.value = tinycolor(colorString).toHsl().h
emit('update:modelValue', colorString)
}
// 使
const updateRecentColorsCache = debounce(function() {
const _color = tinycolor(color.value).toRgbString()
if (!recentColors.value.includes(_color)) {
recentColors.value = [_color, ...recentColors.value]
const maxLength = 10
if (recentColors.value.length > maxLength) {
recentColors.value = recentColors.value.slice(0, maxLength)
}
}
}, 300, { trailing: true })
onMounted(() => {
const recentColorsCache = localStorage.getItem(RECENT_COLORS)
if (recentColorsCache) recentColors.value = JSON.parse(recentColorsCache)
})
watch(recentColors, () => {
const recentColorsCache = JSON.stringify(recentColors.value)
localStorage.setItem(RECENT_COLORS, recentColorsCache)
})
const changeColor = (value: ColorFormats.RGBA | ColorFormats.HSLA | ColorFormats.HSVA) => {
if ('h' in value) {
hue.value = value.h
color.value = tinycolor(value).toRgb()
}
else {
hue.value = tinycolor(value).toHsl().h
color.value = value
}
updateRecentColorsCache()
}
const pickColor = () => {
const targetRef: HTMLElement | null = document.querySelector('.canvas')
if (!targetRef) return
const maskRef = document.createElement('div')
maskRef.style.cssText = 'position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 9999; cursor: wait;'
document.body.appendChild(maskRef)
const colorBlockRef = document.createElement('div')
colorBlockRef.style.cssText = 'position: absolute; top: -100px; left: -100px; width: 16px; height: 16px; border: 1px solid #000; z-index: 999'
maskRef.appendChild(colorBlockRef)
const { left, top, width, height } = targetRef.getBoundingClientRect()
const filter = (node: HTMLElement) => {
if (node.tagName && node.tagName.toUpperCase() === 'FOREIGNOBJECT') return false
if (node.classList && node.classList.contains('operate')) return false
return true
}
toCanvas(targetRef, { filter, fontEmbedCSS: '', width, height, canvasWidth: width, canvasHeight: height, pixelRatio: 1 }).then(canvasRef => {
canvasRef.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; cursor: crosshair;`
maskRef.style.cursor = 'default'
maskRef.appendChild(canvasRef)
const ctx = canvasRef.getContext('2d')
if (!ctx) return
let currentColor = ''
const handleMousemove = (e: MouseEvent) => {
const x = e.x
const y = e.y
const mouseX = x - left
const mouseY = y - top
const [r, g, b, a] = ctx.getImageData(mouseX, mouseY, 1, 1).data
currentColor = `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(2)})`
colorBlockRef.style.left = x + 10 + 'px'
colorBlockRef.style.top = y + 10 + 'px'
colorBlockRef.style.backgroundColor = currentColor
}
const handleMouseleave = () => {
currentColor = ''
colorBlockRef.style.left = '-100px'
colorBlockRef.style.top = '-100px'
colorBlockRef.style.backgroundColor = ''
}
const handleMousedown = (e: MouseEvent) => {
if (currentColor && e.button === 0) {
const tColor = tinycolor(currentColor)
hue.value = tColor.toHsl().h
color.value = tColor.toRgb()
updateRecentColorsCache()
}
document.body.removeChild(maskRef)
canvasRef.removeEventListener('mousemove', handleMousemove)
canvasRef.removeEventListener('mouseleave', handleMouseleave)
window.removeEventListener('mousedown', handleMousedown)
}
canvasRef.addEventListener('mousemove', handleMousemove)
canvasRef.addEventListener('mouseleave', handleMouseleave)
window.addEventListener('mousedown', handleMousedown)
}).catch(() => {
message.error('取色吸管初始化失败')
document.body.removeChild(maskRef)
})
}
</script>
<style lang="scss" scoped>

View File

@ -30,21 +30,18 @@
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script lang="ts" setup>
import { PropType } from 'vue'
import { ContextmenuItem } from './types'
export default defineComponent({
name: 'menu-content',
props: {
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
handleClickMenuItem: {
type: Function,
required: true,
},
defineProps({
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
handleClickMenuItem: {
type: Function,
required: true,
},
})
</script>

View File

@ -20,71 +20,59 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { ContextmenuItem, Axis } from './types'
import MenuContent from './MenuContent.vue'
export default defineComponent({
name: 'contextmenu',
components: {
MenuContent,
const props = defineProps({
axis: {
type: Object as PropType<Axis>,
required: true,
},
props: {
axis: {
type: Object as PropType<Axis>,
required: true,
},
el: {
type: Object as PropType<HTMLElement>,
required: true,
},
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
removeContextmenu: {
type: Function,
required: true,
},
el: {
type: Object as PropType<HTMLElement>,
required: true,
},
setup(props) {
const style = computed(() => {
const MENU_WIDTH = 170
const MENU_HEIGHT = 30
const DIVIDER_HEIGHT = 11
const PADDING = 5
const { x, y } = props.axis
const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
const dividerCount = props.menus.filter(menu => menu.divider).length
const menuWidth = MENU_WIDTH
const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
const screenWidth = document.body.clientWidth
const screenHeight = document.body.clientHeight
return {
left: screenWidth <= x + menuWidth ? x - menuWidth : x,
top: screenHeight <= y + menuHeight ? y - menuHeight : y,
}
})
const handleClickMenuItem = (item: ContextmenuItem) => {
if (item.disable) return
if (item.children && !item.handler) return
if (item.handler) item.handler(props.el)
props.removeContextmenu()
}
return {
style,
handleClickMenuItem,
}
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
removeContextmenu: {
type: Function,
required: true,
},
})
const style = computed(() => {
const MENU_WIDTH = 170
const MENU_HEIGHT = 30
const DIVIDER_HEIGHT = 11
const PADDING = 5
const { x, y } = props.axis
const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
const dividerCount = props.menus.filter(menu => menu.divider).length
const menuWidth = MENU_WIDTH
const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
const screenWidth = document.body.clientWidth
const screenHeight = document.body.clientHeight
return {
left: screenWidth <= x + menuWidth ? x - menuWidth : x,
top: screenHeight <= y + menuHeight ? y - menuHeight : y,
}
})
const handleClickMenuItem = (item: ContextmenuItem) => {
if (item.disable) return
if (item.children && !item.handler) return
if (item.handler) item.handler(props.el)
props.removeContextmenu()
}
</script>
<style lang="scss">

View File

@ -12,38 +12,31 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
export default defineComponent({
name: 'file-input',
emits: ['change'],
props: {
accept: {
type: String,
default: 'image/*',
},
},
setup(props, { emit }) {
const inputRef = ref<HTMLInputElement>()
const handleClick = () => {
if (!inputRef.value) return
inputRef.value.value = ''
inputRef.value.click()
}
const handleChange = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (files) emit('change', files)
}
return {
handleClick,
handleChange,
inputRef,
}
const props = defineProps({
accept: {
type: String,
default: 'image/*',
},
})
const emit = defineEmits<{
(event: 'change', payload: FileList): void
}>()
const inputRef = ref<HTMLInputElement>()
const handleClick = () => {
if (!inputRef.value) return
inputRef.value.value = ''
inputRef.value.click()
}
const handleChange = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (files) emit('change', files)
}
</script>
<style lang="scss" scoped>

View File

@ -2,20 +2,15 @@
<div class="fullscreen-spin" v-if="loading"><Spin :tip="tip" size="large" /></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'fullscreen-spin',
props: {
loading: {
type: Boolean,
default: false,
},
tip: {
type: String,
default: '',
},
<script lang="ts" setup>
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
tip: {
type: String,
default: '',
},
})
</script>

View File

@ -19,53 +19,43 @@
</svg>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { hfmath } from './hfmath'
export default defineComponent({
name: 'formula-content',
props: {
latex: {
type: String,
required: true,
},
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
const props = defineProps({
latex: {
type: String,
required: true,
},
setup(props) {
const box = ref({ x: 0, y: 0, w: 0, h: 0 })
const pathd = ref('')
watch(() => props.latex, () => {
const eq = new hfmath(props.latex)
pathd.value = eq.pathd({})
box.value = eq.box({})
}, { immediate: true })
const scale = computed(() => {
const boxW = box.value.w + 32
const boxH = box.value.h + 32
if (boxW > props.width || boxH > props.height) {
if (boxW / boxH > props.width / props.height) return props.width / boxW
return props.height / boxH
}
return 1
})
return {
box,
pathd,
scale,
}
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
})
const box = ref({ x: 0, y: 0, w: 0, h: 0 })
const pathd = ref('')
watch(() => props.latex, () => {
const eq = new hfmath(props.latex)
pathd.value = eq.pathd({})
box.value = eq.box({})
}, { immediate: true })
const scale = computed(() => {
const boxW = box.value.w + 32
const boxH = box.value.h + 32
if (boxW > props.width || boxH > props.height) {
if (boxW / boxH > props.width / props.height) return props.width / boxW
return props.height / boxH
}
return 1
})
</script>

View File

@ -2,30 +2,22 @@
<div class="symbol-content" v-html="svg"></div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { hfmath } from './hfmath'
export default defineComponent({
name: 'symbol-content',
props: {
latex: {
type: String,
required: true,
},
},
setup(props) {
const svg = computed(() => {
const eq = new hfmath(props.latex)
return eq.svg({
SCALE_X: 10,
SCALE_Y: 10,
})
})
return {
svg,
}
const props = defineProps({
latex: {
type: String,
required: true,
},
})
const svg = computed(() => {
const eq = new hfmath(props.latex)
return eq.svg({
SCALE_X: 10,
SCALE_Y: 10,
})
})
</script>

View File

@ -65,8 +65,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { hfmath } from './hfmath'
import { FORMULA_LIST, SYMBOL_LIST } from '@/configs/latex'
@ -83,72 +83,64 @@ const tabs: Tab[] = [
{ label: '预置公式', value: 'formula' },
]
export default defineComponent({
name: 'latex-editor',
emits: ['update', 'close'],
components: {
FormulaContent,
SymbolContent,
},
props: {
value: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const latex = ref('')
const toolbarState = ref<'symbol' | 'formula'>('symbol')
const textAreaRef = ref<HTMLTextAreaElement>()
interface LatexResult {
latex: string
path: string
w: number
h: number
}
const selectedSymbolKey = ref(SYMBOL_LIST[0].type)
const symbolPool = computed(() => {
const selectedSymbol = SYMBOL_LIST.find(item => item.type === selectedSymbolKey.value)
return selectedSymbol?.children || []
})
onMounted(() => {
if (props.value) latex.value = props.value
})
const update = () => {
if (!latex.value) return
const eq = new hfmath(latex.value)
const pathd = eq.pathd({})
const box = eq.box({})
emit('update', {
latex: latex.value,
path: pathd,
w: box.w + 32,
h: box.h + 32,
})
}
const close = () => emit('close')
const insertSymbol = (latex: string) => {
if (!textAreaRef.value) return
textAreaRef.value.focus()
document.execCommand('insertText', false, latex)
}
return {
tabs,
latex,
toolbarState,
selectedSymbolKey,
formulaList: FORMULA_LIST,
symbolList: SYMBOL_LIST,
symbolPool,
textAreaRef,
update,
close,
insertSymbol,
}
const props = defineProps({
value: {
type: String,
default: '',
},
})
const emit = defineEmits<{
(event: 'update', payload: LatexResult): void
(event: 'close'): void
}>()
const formulaList = FORMULA_LIST
const symbolList = SYMBOL_LIST
const latex = ref('')
const toolbarState = ref<'symbol' | 'formula'>('symbol')
const textAreaRef = ref<HTMLTextAreaElement>()
const selectedSymbolKey = ref(SYMBOL_LIST[0].type)
const symbolPool = computed(() => {
const selectedSymbol = SYMBOL_LIST.find(item => item.type === selectedSymbolKey.value)
return selectedSymbol?.children || []
})
onMounted(() => {
if (props.value) latex.value = props.value
})
const update = () => {
if (!latex.value) return
const eq = new hfmath(latex.value)
const pathd = eq.pathd({})
const box = eq.box({})
emit('update', {
latex: latex.value,
path: pathd,
w: box.w + 32,
h: box.h + 32,
})
}
const close = () => emit('close')
const insertSymbol = (latex: string) => {
if (!textAreaRef.value) return
textAreaRef.value.focus()
document.execCommand('insertText', false, latex)
}
</script>
<style lang="scss" scoped>

View File

@ -55,294 +55,277 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
import { throttle } from 'lodash'
export default defineComponent({
name: 'writing-board',
props: {
color: {
type: String,
default: '#ffcc00',
},
model: {
type: String as PropType<'pen' | 'eraser' | 'mark'>,
default: 'pen',
},
blackboard: {
type: Boolean,
default: false,
},
const props = defineProps({
color: {
type: String,
default: '#ffcc00',
},
setup(props) {
let ctx: CanvasRenderingContext2D | null = null
const writingBoardRef = ref<HTMLElement>()
const canvasRef = ref<HTMLCanvasElement>()
const penSize = ref(6)
const rubberSize = ref(80)
const markSize = ref(24)
let lastPos = {
x: 0,
y: 0,
}
let isMouseDown = false
let lastTime = 0
let lastLineWidth = -1
//
const mouse = ref({
x: 0,
y: 0,
})
//
const mouseInCanvas = ref(false)
// canvas
const canvasWidth = ref(0)
const canvasHeight = ref(0)
const widthScale = computed(() => canvasRef.value ? canvasWidth.value / canvasRef.value.width : 1)
const heightScale = computed(() => canvasRef.value ? canvasHeight.value / canvasRef.value.height : 1)
const updateCanvasSize = () => {
if (!writingBoardRef.value) return
canvasWidth.value = writingBoardRef.value.clientWidth
canvasHeight.value = writingBoardRef.value.clientHeight
}
const resizeObserver = new ResizeObserver(updateCanvasSize)
onMounted(() => {
if (writingBoardRef.value) resizeObserver.observe(writingBoardRef.value)
})
onUnmounted(() => {
if (writingBoardRef.value) resizeObserver.unobserve(writingBoardRef.value)
})
//
const initCanvas = () => {
if (!canvasRef.value || !writingBoardRef.value) return
ctx = canvasRef.value.getContext('2d')
if (!ctx) return
canvasRef.value.width = writingBoardRef.value.clientWidth
canvasRef.value.height = writingBoardRef.value.clientHeight
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
}
onMounted(initCanvas)
// canvas ctx
const updateCtx = () => {
if (!ctx) return
if (props.model === 'mark') {
ctx.globalCompositeOperation = 'xor'
ctx.globalAlpha = 0.5
}
else if (props.model === 'pen') {
ctx.globalCompositeOperation = 'source-over'
ctx.globalAlpha = 1
}
}
watch(() => props.model, updateCtx)
//
const draw = (posX: number, posY: number, lineWidth: number) => {
if (!ctx) return
const lastPosX = lastPos.x
const lastPosY = lastPos.y
ctx.lineWidth = lineWidth
ctx.strokeStyle = props.color
ctx.beginPath()
ctx.moveTo(lastPosX, lastPosY)
ctx.lineTo(posX, posY)
ctx.stroke()
ctx.closePath()
}
//
const erase = (posX: number, posY: number) => {
if (!ctx || !canvasRef.value) return
const lastPosX = lastPos.x
const lastPosY = lastPos.y
const radius = rubberSize.value / 2
const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
ctx.save()
ctx.beginPath()
ctx.arc(posX, posY, radius, 0, Math.PI * 2)
ctx.clip()
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
ctx.restore()
ctx.save()
ctx.beginPath()
ctx.moveTo(...rectPoint1)
ctx.lineTo(...rectPoint3)
ctx.lineTo(...rectPoint4)
ctx.lineTo(...rectPoint2)
ctx.closePath()
ctx.clip()
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
ctx.restore()
}
//
const getDistance = (posX: number, posY: number) => {
const lastPosX = lastPos.x
const lastPosY = lastPos.y
return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
}
// st
const getLineWidth = (s: number, t: number) => {
const maxV = 10
const minV = 0.1
const maxWidth = penSize.value
const minWidth = 3
const v = s / t
let lineWidth
if (v <= minV) lineWidth = maxWidth
else if (v >= maxV) lineWidth = minWidth
else lineWidth = maxWidth - v / maxV * maxWidth
if (lastLineWidth === -1) return lineWidth
return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
}
//
const handleMove = (x: number, y: number) => {
const time = new Date().getTime()
if (props.model === 'pen') {
const s = getDistance(x, y)
const t = time - lastTime
const lineWidth = getLineWidth(s, t)
draw(x, y, lineWidth)
lastLineWidth = lineWidth
}
else if (props.model === 'mark') draw(x, y, markSize.value)
else erase(x, y)
lastPos = { x, y }
lastTime = new Date().getTime()
}
// canvas
const getMouseOffsetPosition = (e: MouseEvent | TouchEvent) => {
if (!canvasRef.value) return [0, 0]
const event = e instanceof MouseEvent ? e : e.changedTouches[0]
const canvasRect = canvasRef.value.getBoundingClientRect()
const x = event.pageX - canvasRect.x
const y = event.pageY - canvasRect.y
return [x, y]
}
//
// /
const handleMousedown = (e: MouseEvent | TouchEvent) => {
const [mouseX, mouseY] = getMouseOffsetPosition(e)
const x = mouseX / widthScale.value
const y = mouseY / heightScale.value
isMouseDown = true
lastPos = { x, y }
lastTime = new Date().getTime()
if (!(e instanceof MouseEvent)) {
mouse.value = { x: mouseX, y: mouseY }
mouseInCanvas.value = true
}
}
// /
const handleMousemove = (e: MouseEvent | TouchEvent) => {
const [mouseX, mouseY] = getMouseOffsetPosition(e)
const x = mouseX / widthScale.value
const y = mouseY / heightScale.value
mouse.value = { x: mouseX, y: mouseY }
if (isMouseDown) handleMove(x, y)
}
// /
const handleMouseup = () => {
if (!isMouseDown) return
isMouseDown = false
}
//
const clearCanvas = () => {
if (!ctx || !canvasRef.value) return
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
}
// DataURL
const getImageDataURL = () => {
return canvasRef.value?.toDataURL()
}
// DataURL canvas
const setImageDataURL = (imageDataURL: string) => {
const img = new Image()
img.src = imageDataURL
img.onload = () => {
if (!ctx) return
ctx.drawImage(img, 0, 0)
}
}
//
const mousewheelListener = throttle(function(e: WheelEvent) {
if (props.model === 'eraser') {
if (e.deltaY < 0 && rubberSize.value < 200) rubberSize.value += 20
else if (e.deltaY > 0 && rubberSize.value > 20) rubberSize.value -= 20
}
if (props.model === 'pen') {
if (e.deltaY < 0 && penSize.value < 10) penSize.value += 2
else if (e.deltaY > 0 && penSize.value > 4) penSize.value -= 2
}
if (props.model === 'mark') {
if (e.deltaY < 0 && markSize.value < 40) markSize.value += 4
else if (e.deltaY > 0 && markSize.value > 16) markSize.value -= 4
}
}, 300, { leading: true, trailing: false })
return {
mouse,
mouseInCanvas,
penSize,
rubberSize,
markSize,
writingBoardRef,
canvasRef,
canvasWidth,
canvasHeight,
handleMousedown,
handleMousemove,
handleMouseup,
clearCanvas,
getImageDataURL,
setImageDataURL,
mousewheelListener,
}
model: {
type: String as PropType<'pen' | 'eraser' | 'mark'>,
default: 'pen',
},
blackboard: {
type: Boolean,
default: false,
},
})
let ctx: CanvasRenderingContext2D | null = null
const writingBoardRef = ref<HTMLElement>()
const canvasRef = ref<HTMLCanvasElement>()
const penSize = ref(6)
const rubberSize = ref(80)
const markSize = ref(24)
let lastPos = {
x: 0,
y: 0,
}
let isMouseDown = false
let lastTime = 0
let lastLineWidth = -1
//
const mouse = ref({
x: 0,
y: 0,
})
//
const mouseInCanvas = ref(false)
// canvas
const canvasWidth = ref(0)
const canvasHeight = ref(0)
const widthScale = computed(() => canvasRef.value ? canvasWidth.value / canvasRef.value.width : 1)
const heightScale = computed(() => canvasRef.value ? canvasHeight.value / canvasRef.value.height : 1)
const updateCanvasSize = () => {
if (!writingBoardRef.value) return
canvasWidth.value = writingBoardRef.value.clientWidth
canvasHeight.value = writingBoardRef.value.clientHeight
}
const resizeObserver = new ResizeObserver(updateCanvasSize)
onMounted(() => {
if (writingBoardRef.value) resizeObserver.observe(writingBoardRef.value)
})
onUnmounted(() => {
if (writingBoardRef.value) resizeObserver.unobserve(writingBoardRef.value)
})
//
const initCanvas = () => {
if (!canvasRef.value || !writingBoardRef.value) return
ctx = canvasRef.value.getContext('2d')
if (!ctx) return
canvasRef.value.width = writingBoardRef.value.clientWidth
canvasRef.value.height = writingBoardRef.value.clientHeight
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
}
onMounted(initCanvas)
// canvas ctx
const updateCtx = () => {
if (!ctx) return
if (props.model === 'mark') {
ctx.globalCompositeOperation = 'xor'
ctx.globalAlpha = 0.5
}
else if (props.model === 'pen') {
ctx.globalCompositeOperation = 'source-over'
ctx.globalAlpha = 1
}
}
watch(() => props.model, updateCtx)
//
const draw = (posX: number, posY: number, lineWidth: number) => {
if (!ctx) return
const lastPosX = lastPos.x
const lastPosY = lastPos.y
ctx.lineWidth = lineWidth
ctx.strokeStyle = props.color
ctx.beginPath()
ctx.moveTo(lastPosX, lastPosY)
ctx.lineTo(posX, posY)
ctx.stroke()
ctx.closePath()
}
//
const erase = (posX: number, posY: number) => {
if (!ctx || !canvasRef.value) return
const lastPosX = lastPos.x
const lastPosY = lastPos.y
const radius = rubberSize.value / 2
const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
ctx.save()
ctx.beginPath()
ctx.arc(posX, posY, radius, 0, Math.PI * 2)
ctx.clip()
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
ctx.restore()
ctx.save()
ctx.beginPath()
ctx.moveTo(...rectPoint1)
ctx.lineTo(...rectPoint3)
ctx.lineTo(...rectPoint4)
ctx.lineTo(...rectPoint2)
ctx.closePath()
ctx.clip()
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
ctx.restore()
}
//
const getDistance = (posX: number, posY: number) => {
const lastPosX = lastPos.x
const lastPosY = lastPos.y
return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
}
// st
const getLineWidth = (s: number, t: number) => {
const maxV = 10
const minV = 0.1
const maxWidth = penSize.value
const minWidth = 3
const v = s / t
let lineWidth
if (v <= minV) lineWidth = maxWidth
else if (v >= maxV) lineWidth = minWidth
else lineWidth = maxWidth - v / maxV * maxWidth
if (lastLineWidth === -1) return lineWidth
return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
}
//
const handleMove = (x: number, y: number) => {
const time = new Date().getTime()
if (props.model === 'pen') {
const s = getDistance(x, y)
const t = time - lastTime
const lineWidth = getLineWidth(s, t)
draw(x, y, lineWidth)
lastLineWidth = lineWidth
}
else if (props.model === 'mark') draw(x, y, markSize.value)
else erase(x, y)
lastPos = { x, y }
lastTime = new Date().getTime()
}
// canvas
const getMouseOffsetPosition = (e: MouseEvent | TouchEvent) => {
if (!canvasRef.value) return [0, 0]
const event = e instanceof MouseEvent ? e : e.changedTouches[0]
const canvasRect = canvasRef.value.getBoundingClientRect()
const x = event.pageX - canvasRect.x
const y = event.pageY - canvasRect.y
return [x, y]
}
//
// /
const handleMousedown = (e: MouseEvent | TouchEvent) => {
const [mouseX, mouseY] = getMouseOffsetPosition(e)
const x = mouseX / widthScale.value
const y = mouseY / heightScale.value
isMouseDown = true
lastPos = { x, y }
lastTime = new Date().getTime()
if (!(e instanceof MouseEvent)) {
mouse.value = { x: mouseX, y: mouseY }
mouseInCanvas.value = true
}
}
// /
const handleMousemove = (e: MouseEvent | TouchEvent) => {
const [mouseX, mouseY] = getMouseOffsetPosition(e)
const x = mouseX / widthScale.value
const y = mouseY / heightScale.value
mouse.value = { x: mouseX, y: mouseY }
if (isMouseDown) handleMove(x, y)
}
// /
const handleMouseup = () => {
if (!isMouseDown) return
isMouseDown = false
}
//
const clearCanvas = () => {
if (!ctx || !canvasRef.value) return
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
}
// DataURL
const getImageDataURL = () => {
return canvasRef.value?.toDataURL()
}
// DataURL canvas
const setImageDataURL = (imageDataURL: string) => {
const img = new Image()
img.src = imageDataURL
img.onload = () => {
if (!ctx) return
ctx.drawImage(img, 0, 0)
}
}
//
const mousewheelListener = throttle(function(e: WheelEvent) {
if (props.model === 'eraser') {
if (e.deltaY < 0 && rubberSize.value < 200) rubberSize.value += 20
else if (e.deltaY > 0 && rubberSize.value > 20) rubberSize.value -= 20
}
if (props.model === 'pen') {
if (e.deltaY < 0 && penSize.value < 10) penSize.value += 2
else if (e.deltaY > 0 && penSize.value > 4) penSize.value -= 2
}
if (props.model === 'mark') {
if (e.deltaY < 0 && markSize.value < 40) markSize.value += 4
else if (e.deltaY > 0 && markSize.value > 16) markSize.value -= 4
}
}, 300, { leading: true, trailing: false })
defineExpose({
clearCanvas,
getImageDataURL,
setImageDataURL,
})
</script>

View File

@ -62,7 +62,7 @@ export default () => {
}
// 导入pptist文件
const importSpecificFile = (files: File[], cover = false) => {
const importSpecificFile = (files: FileList, cover = false) => {
const file = files[0]
const reader = new FileReader()

2
src/shims-vue.d.ts vendored
View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>

View File

@ -4,47 +4,37 @@
</div>
</template>
<script lang="ts">
import { computed, PropType, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { AlignmentLineAxis } from '@/types/edit'
export default defineComponent({
name: 'alignment-line',
props: {
type: {
type: String as PropType<'vertical' | 'horizontal'>,
required: true,
},
axis: {
type: Object as PropType<AlignmentLineAxis>,
required: true,
},
length: {
type: Number,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
const props = defineProps({
type: {
type: String as PropType<'vertical' | 'horizontal'>,
required: true,
},
setup(props) {
// 线
const left = computed(() => props.axis.x * props.canvasScale + 'px')
const top = computed(() => props.axis.y * props.canvasScale + 'px')
// 线
const sizeStyle = computed(() => {
if (props.type === 'vertical') return { height: props.length * props.canvasScale + 'px' }
return { width: props.length * props.canvasScale + 'px' }
})
return {
left,
top,
sizeStyle,
}
axis: {
type: Object as PropType<AlignmentLineAxis>,
required: true,
},
length: {
type: Number,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
})
// 线
const left = computed(() => props.axis.x * props.canvasScale + 'px')
const top = computed(() => props.axis.y * props.canvasScale + 'px')
// 线
const sizeStyle = computed(() => {
if (props.type === 'vertical') return { height: props.length * props.canvasScale + 'px' }
return { width: props.length * props.canvasScale + 'px' }
})
</script>

View File

@ -16,8 +16,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { ElementTypes, PPTElement } from '@/types/slides'
import { ContextmenuItem } from '@/components/Contextmenu/types'
@ -41,151 +41,142 @@ import LatexElement from '@/views/components/element/LatexElement/index.vue'
import VideoElement from '@/views/components/element/VideoElement/index.vue'
import AudioElement from '@/views/components/element/AudioElement/index.vue'
export default defineComponent({
name: 'editable-element',
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
elementIndex: {
type: Number,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
selectElement: {
type: Function as PropType<(e: MouseEvent | TouchEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
setup(props) {
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
}
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, pasteElement, cutElement } = useCopyAndPasteElement()
const { selectAllElement } = useSelectAllElement()
const contextmenus = (): ContextmenuItem[] => {
if (props.elementInfo.lock) {
return [{
text: '解锁',
handler: () => unlockElement(props.elementInfo),
}]
}
return [
{
text: '剪切',
subText: 'Ctrl + X',
handler: cutElement,
},
{
text: '复制',
subText: 'Ctrl + C',
handler: copyElement,
},
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteElement,
},
{ divider: true },
{
text: '水平居中',
handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL),
children: [
{ text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER), },
{ text: '水平居中', handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
{ text: '左对齐', handler: () => alignElementToCanvas(ElementAlignCommands.LEFT) },
{ text: '右对齐', handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT) },
],
},
{
text: '垂直居中',
handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL),
children: [
{ text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER) },
{ text: '垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL) },
{ text: '顶部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.TOP) },
{ text: '底部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM) },
],
},
{ divider: true },
{
text: '置于顶层',
disable: props.isMultiSelect && !props.elementInfo.groupId,
handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP),
children: [
{ text: '置于顶层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP) },
{ text: '上移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.UP) },
],
},
{
text: '置于底层',
disable: props.isMultiSelect && !props.elementInfo.groupId,
handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM),
children: [
{ text: '置于底层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
{ text: '下移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
],
},
{ divider: true },
{
text: '设置链接',
handler: props.openLinkDialog,
},
{
text: props.elementInfo.groupId ? '取消组合' : '组合',
subText: 'Ctrl + G',
handler: props.elementInfo.groupId ? uncombineElements : combineElements,
hide: !props.isMultiSelect,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllElement,
},
{
text: '锁定',
subText: 'Ctrl + L',
handler: lockElement,
},
{
text: '删除',
subText: 'Delete',
handler: deleteElement,
},
]
}
return {
currentElementComponent,
contextmenus,
}
elementIndex: {
type: Number,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
selectElement: {
type: Function as PropType<(e: MouseEvent | TouchEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
})
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
}
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, pasteElement, cutElement } = useCopyAndPasteElement()
const { selectAllElement } = useSelectAllElement()
const contextmenus = (): ContextmenuItem[] => {
if (props.elementInfo.lock) {
return [{
text: '解锁',
handler: () => unlockElement(props.elementInfo),
}]
}
return [
{
text: '剪切',
subText: 'Ctrl + X',
handler: cutElement,
},
{
text: '复制',
subText: 'Ctrl + C',
handler: copyElement,
},
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteElement,
},
{ divider: true },
{
text: '水平居中',
handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL),
children: [
{ text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER), },
{ text: '水平居中', handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
{ text: '左对齐', handler: () => alignElementToCanvas(ElementAlignCommands.LEFT) },
{ text: '右对齐', handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT) },
],
},
{
text: '垂直居中',
handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL),
children: [
{ text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER) },
{ text: '垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL) },
{ text: '顶部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.TOP) },
{ text: '底部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM) },
],
},
{ divider: true },
{
text: '置于顶层',
disable: props.isMultiSelect && !props.elementInfo.groupId,
handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP),
children: [
{ text: '置于顶层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP) },
{ text: '上移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.UP) },
],
},
{
text: '置于底层',
disable: props.isMultiSelect && !props.elementInfo.groupId,
handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM),
children: [
{ text: '置于底层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
{ text: '下移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
],
},
{ divider: true },
{
text: '设置链接',
handler: props.openLinkDialog,
},
{
text: props.elementInfo.groupId ? '取消组合' : '组合',
subText: 'Ctrl + G',
handler: props.elementInfo.groupId ? uncombineElements : combineElements,
hide: !props.isMultiSelect,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllElement,
},
{
text: '锁定',
subText: 'Ctrl + L',
handler: lockElement,
},
{
text: '删除',
subText: 'Delete',
handler: deleteElement,
},
]
}
</script>

View File

@ -25,195 +25,184 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useKeyboardStore } from '@/store'
import { CreateElementSelectionData } from '@/types/edit'
export default defineComponent({
name: 'element-create-selection',
emits: ['created'],
setup(props, { emit }) {
const mainStore = useMainStore()
const { creatingElement } = storeToRefs(mainStore)
const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore())
const emit = defineEmits<{
(event: 'created', payload: CreateElementSelectionData): void
}>()
const start = ref<[number, number]>()
const end = ref<[number, number]>()
const mainStore = useMainStore()
const { creatingElement } = storeToRefs(mainStore)
const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore())
const selectionRef = ref<HTMLElement>()
const offset = ref({
x: 0,
y: 0,
})
onMounted(() => {
if (!selectionRef.value) return
const { x, y } = selectionRef.value.getBoundingClientRect()
offset.value = { x, y }
})
const start = ref<[number, number]>()
const end = ref<[number, number]>()
//
//
const createSelection = (e: MouseEvent) => {
let isMouseDown = true
const selectionRef = ref<HTMLElement>()
const offset = ref({
x: 0,
y: 0,
})
onMounted(() => {
if (!selectionRef.value) return
const { x, y } = selectionRef.value.getBoundingClientRect()
offset.value = { x, y }
})
const startPageX = e.pageX
const startPageY = e.pageY
start.value = [startPageX, startPageY]
//
//
const createSelection = (e: MouseEvent) => {
let isMouseDown = true
document.onmousemove = e => {
if (!creatingElement.value || !isMouseDown) return
const startPageX = e.pageX
const startPageY = e.pageY
start.value = [startPageX, startPageY]
let currentPageX = e.pageX
let currentPageY = e.pageY
document.onmousemove = e => {
if (!creatingElement.value || !isMouseDown) return
// CtrlShift
// 线线
if (ctrlOrShiftKeyActive.value) {
const moveX = currentPageX - startPageX
const moveY = currentPageY - startPageY
let currentPageX = e.pageX
let currentPageY = e.pageY
//
const absX = Math.abs(moveX)
const absY = Math.abs(moveY)
// CtrlShift
// 线线
if (ctrlOrShiftKeyActive.value) {
const moveX = currentPageX - startPageX
const moveY = currentPageY - startPageY
if (creatingElement.value.type === 'shape') {
//
const absX = Math.abs(moveX)
const absY = Math.abs(moveY)
//
const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
if (creatingElement.value.type === 'shape') {
if (absX > absY) {
currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX
}
else {
currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY
}
}
//
const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
else if (creatingElement.value.type === 'line') {
if (absX > absY) currentPageY = startPageY
else currentPageX = startPageX
}
}
end.value = [currentPageX, currentPageY]
}
document.onmouseup = e => {
document.onmousemove = null
document.onmouseup = null
if (e.button === 2) {
setTimeout(() => mainStore.setCreatingElement(null), 0)
return
}
isMouseDown = false
const endPageX = e.pageX
const endPageY = e.pageY
const minSize = 30
if (
creatingElement.value?.type === 'line' &&
(Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize)
) {
emit('created', {
start: start.value,
end: end.value,
})
}
else if (
creatingElement.value?.type !== 'line' &&
(Math.abs(endPageX - startPageX) >= minSize && Math.abs(endPageY - startPageY) >= minSize)
) {
emit('created', {
start: start.value,
end: end.value,
})
if (absX > absY) {
currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX
}
else {
const defaultSize = 200
const minX = Math.min(endPageX, startPageX)
const minY = Math.min(endPageY, startPageY)
const maxX = Math.max(endPageX, startPageX)
const maxY = Math.max(endPageY, startPageY)
const offsetX = maxX - minX >= minSize ? maxX - minX : defaultSize
const offsetY = maxY - minY >= minSize ? maxY - minY : defaultSize
emit('created', {
start: [minX, minY],
end: [minX + offsetX, minY + offsetY],
})
currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY
}
}
else if (creatingElement.value.type === 'line') {
if (absX > absY) currentPageY = startPageY
else currentPageX = startPageX
}
}
// 线线使
const lineData = computed(() => {
if (!start.value || !end.value) return null
if (!creatingElement.value || creatingElement.value.type !== 'line') return null
end.value = [currentPageX, currentPageY]
}
const [_startX, _startY] = start.value
const [_endX, _endY] = end.value
const minX = Math.min(_startX, _endX)
const maxX = Math.max(_startX, _endX)
const minY = Math.min(_startY, _endY)
const maxY = Math.max(_startY, _endY)
document.onmouseup = e => {
document.onmousemove = null
document.onmouseup = null
const svgWidth = maxX - minX >= 24 ? maxX - minX : 24
const svgHeight = maxY - minY >= 24 ? maxY - minY : 24
const startX = _startX === minX ? 0 : maxX - minX
const startY = _startY === minY ? 0 : maxY - minY
const endX = _endX === minX ? 0 : maxX - minX
const endY = _endY === minY ? 0 : maxY - minY
const path = `M${startX}, ${startY} L${endX}, ${endY}`
return {
svgWidth,
svgHeight,
startX,
startY,
endX,
endY,
path,
}
})
//
const position = computed(() => {
if (!start.value || !end.value) return {}
const [startX, startY] = start.value
const [endX, endY] = end.value
const minX = Math.min(startX, endX)
const maxX = Math.max(startX, endX)
const minY = Math.min(startY, endY)
const maxY = Math.max(startY, endY)
const width = maxX - minX
const height = maxY - minY
return {
left: minX - offset.value.x + 'px',
top: minY - offset.value.y + 'px',
width: width + 'px',
height: height + 'px',
}
})
return {
selectionRef,
start,
end,
creatingElement,
createSelection,
lineData,
position,
if (e.button === 2) {
setTimeout(() => mainStore.setCreatingElement(null), 0)
return
}
},
isMouseDown = false
const endPageX = e.pageX
const endPageY = e.pageY
const minSize = 30
if (
creatingElement.value?.type === 'line' &&
(Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize)
) {
emit('created', {
start: start.value!,
end: end.value!,
})
}
else if (
creatingElement.value?.type !== 'line' &&
(Math.abs(endPageX - startPageX) >= minSize && Math.abs(endPageY - startPageY) >= minSize)
) {
emit('created', {
start: start.value!,
end: end.value!,
})
}
else {
const defaultSize = 200
const minX = Math.min(endPageX, startPageX)
const minY = Math.min(endPageY, startPageY)
const maxX = Math.max(endPageX, startPageX)
const maxY = Math.max(endPageY, startPageY)
const offsetX = maxX - minX >= minSize ? maxX - minX : defaultSize
const offsetY = maxY - minY >= minSize ? maxY - minY : defaultSize
emit('created', {
start: [minX, minY],
end: [minX + offsetX, minY + offsetY],
})
}
}
}
// 线线使
const lineData = computed(() => {
if (!start.value || !end.value) return null
if (!creatingElement.value || creatingElement.value.type !== 'line') return null
const [_startX, _startY] = start.value
const [_endX, _endY] = end.value
const minX = Math.min(_startX, _endX)
const maxX = Math.max(_startX, _endX)
const minY = Math.min(_startY, _endY)
const maxY = Math.max(_startY, _endY)
const svgWidth = maxX - minX >= 24 ? maxX - minX : 24
const svgHeight = maxY - minY >= 24 ? maxY - minY : 24
const startX = _startX === minX ? 0 : maxX - minX
const startY = _startY === minY ? 0 : maxY - minY
const endX = _endX === minX ? 0 : maxX - minX
const endY = _endY === minY ? 0 : maxY - minY
const path = `M${startX}, ${startY} L${endX}, ${endY}`
return {
svgWidth,
svgHeight,
startX,
startY,
endX,
endY,
path,
}
})
//
const position = computed(() => {
if (!start.value || !end.value) return {}
const [startX, startY] = start.value
const [endX, endY] = end.value
const minX = Math.min(startX, endX)
const maxX = Math.max(startX, endX)
const minY = Math.min(startY, endY)
const maxY = Math.max(startY, endY)
const width = maxX - minX
const height = maxY - minY
return {
left: minX - offset.value.x + 'px',
top: minY - offset.value.y + 'px',
width: width + 'px',
height: height + 'px',
}
})
</script>

View File

@ -13,55 +13,44 @@
</svg>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import tinycolor from 'tinycolor2'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { VIEWPORT_SIZE } from '@/configs/canvas'
import { SlideBackground } from '@/types/slides'
export default defineComponent({
name: 'grid-lines',
setup() {
const { canvasScale } = storeToRefs(useMainStore())
const { currentSlide, viewportRatio } = storeToRefs(useSlidesStore())
const { canvasScale } = storeToRefs(useMainStore())
const { currentSlide, viewportRatio } = storeToRefs(useSlidesStore())
const background = computed<SlideBackground | undefined>(() => currentSlide.value?.background)
const background = computed<SlideBackground | undefined>(() => currentSlide.value?.background)
// 线
const gridColor = computed(() => {
const bgColor = background.value?.color || '#fff'
const colorList = ['#000', '#fff']
return tinycolor.mostReadable(bgColor, colorList, { includeFallbackColors: true }).setAlpha(.5).toRgbString()
})
const gridSize = 50
//
const getPath = () => {
const maxX = VIEWPORT_SIZE
const maxY = VIEWPORT_SIZE * viewportRatio.value
let path = ''
for (let i = 0; i <= Math.floor(maxY / gridSize); i++) {
path += `M0 ${i * gridSize} L${maxX} ${i * gridSize} `
}
for (let i = 0; i <= Math.floor(maxX / gridSize); i++) {
path += `M${i * gridSize} 0 L${i * gridSize} ${maxY} `
}
return path
}
return {
canvasScale,
gridColor,
width: VIEWPORT_SIZE,
height: VIEWPORT_SIZE * viewportRatio.value,
path: getPath(),
}
},
// 线
const gridColor = computed(() => {
const bgColor = background.value?.color || '#fff'
const colorList = ['#000', '#fff']
return tinycolor.mostReadable(bgColor, colorList, { includeFallbackColors: true }).setAlpha(.5).toRgbString()
})
const gridSize = 50
//
const getPath = () => {
const maxX = VIEWPORT_SIZE
const maxY = VIEWPORT_SIZE * viewportRatio.value
let path = ''
for (let i = 0; i <= Math.floor(maxY / gridSize); i++) {
path += `M0 ${i * gridSize} L${maxX} ${i * gridSize} `
}
for (let i = 0; i <= Math.floor(maxX / gridSize); i++) {
path += `M${i * gridSize} 0 L${i * gridSize} ${maxY} `
}
return path
}
const path = getPath()
</script>
<style lang="scss" scoped>

View File

@ -37,8 +37,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElementLink } from '@/types/slides'
@ -52,70 +52,54 @@ interface TabItem {
label: string
}
export default defineComponent({
name: 'link-dialog',
emits: ['close'],
components: {
ThumbnailSlide,
},
setup(props, { emit }) {
const { handleElement } = storeToRefs(useMainStore())
const { slides } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'close'): void
}>()
const type = ref<TypeKey>('web')
const address = ref('')
const slideId = ref('')
const { handleElement } = storeToRefs(useMainStore())
const { slides } = storeToRefs(useSlidesStore())
slideId.value = slides.value[0].id
const type = ref<TypeKey>('web')
const address = ref('')
const slideId = ref('')
const selectedSlide = computed(() => {
if (!slideId.value) return null
slideId.value = slides.value[0].id
return slides.value.find(item => item.id === slideId.value) || null
})
const selectedSlide = computed(() => {
if (!slideId.value) return null
const tabs: TabItem[] = [
{ key: 'web', label: '网页链接' },
{ key: 'slide', label: '幻灯片页面' },
]
const { setLink } = useLink()
onMounted(() => {
if (handleElement.value?.link) {
if (handleElement.value.link.type === 'web') address.value = handleElement.value.link.target
else if (handleElement.value.link.type === 'slide') slideId.value = handleElement.value.link.target
type.value = handleElement.value.link.type
}
})
const close = () => emit('close')
const save = () => {
const link: PPTElementLink = {
type: type.value,
target: type.value === 'web' ? address.value : slideId.value,
}
if (handleElement.value) {
const success = setLink(handleElement.value, link)
if (success) close()
else address.value = ''
}
}
return {
slides,
tabs,
type,
address,
slideId,
selectedSlide,
close,
save,
}
},
return slides.value.find(item => item.id === slideId.value) || null
})
const tabs: TabItem[] = [
{ key: 'web', label: '网页链接' },
{ key: 'slide', label: '幻灯片页面' },
]
const { setLink } = useLink()
onMounted(() => {
if (handleElement.value?.link) {
if (handleElement.value.link.type === 'web') address.value = handleElement.value.link.target
else if (handleElement.value.link.type === 'slide') slideId.value = handleElement.value.link.target
type.value = handleElement.value.link.type
}
})
const close = () => emit('close')
const save = () => {
const link: PPTElementLink = {
type: type.value,
target: type.value === 'web' ? address.value : slideId.value,
}
if (handleElement.value) {
const success = setLink(handleElement.value, link)
if (success) close()
else address.value = ''
}
}
</script>
<style lang="scss" scoped>

View File

@ -9,36 +9,31 @@
></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'mouse-selection',
props: {
top: {
type: Number,
required: true,
<script lang="ts" setup>
defineProps({
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)
},
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>

View File

@ -2,21 +2,18 @@
<div :class="['border-line', type, { 'wide': isWide }]"></div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { PropType } from 'vue'
import { OperateBorderLines } from '@/types/edit'
export default defineComponent({
name: 'border-line',
props: {
type: {
type: String as PropType<OperateBorderLines>,
required: true,
},
isWide: {
type: Boolean,
default: false,
},
defineProps({
type: {
type: String as PropType<OperateBorderLines>,
required: true,
},
isWide: {
type: Boolean,
default: false,
},
})
</script>

View File

@ -28,7 +28,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTShapeElement, PPTVideoElement, PPTLatexElement, PPTAudioElement } from '@/types/slides'
@ -41,47 +47,30 @@ import BorderLine from './BorderLine.vue'
type PPTElement = PPTShapeElement | PPTVideoElement | PPTLatexElement | PPTAudioElement
export default defineComponent({
name: 'common-element-operate',
inheritAttrs: false,
components: {
RotateHandler,
ResizeHandler,
BorderLine,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const cannotRotate = computed(() => ['video', 'audio'].includes(props.elementInfo.type))
return {
scaleWidth,
resizeHandlers,
borderLines,
cannotRotate,
}
rotateElement: {
type: Function as PropType<(element: PPTElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandlers) => void>,
required: true,
},
})
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const cannotRotate = computed(() => ['video', 'audio'].includes(props.elementInfo.type))
</script>

View File

@ -27,7 +27,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTImageElement } from '@/types/slides'
@ -38,49 +44,32 @@ import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'image-element-operate',
inheritAttrs: false,
components: {
RotateHandler,
ResizeHandler,
BorderLine,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTImageElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTImageElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTImageElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
return {
isCliping,
scaleWidth,
resizeHandlers,
borderLines,
}
rotateElement: {
type: Function as PropType<(element: PPTImageElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandlers) => void>,
required: true,
},
})
const { canvasScale, clipingImageElementId } = storeToRefs(useMainStore())
const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
</script>
<style lang="scss" scoped>

View File

@ -34,7 +34,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTLineElement } from '@/types/slides'
@ -42,89 +48,74 @@ import { OperateLineHandlers } from '@/types/edit'
import ResizeHandler from './ResizeHandler.vue'
export default defineComponent({
name: 'line-element-operate',
inheritAttrs: false,
components: {
ResizeHandler,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTLineElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTLineElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
dragLineElement: {
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
dragLineElement: {
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void>,
required: true,
},
})
const svgWidth = computed(() => Math.max(props.elementInfo.start[0], props.elementInfo.end[0]))
const svgHeight = computed(() => Math.max(props.elementInfo.start[1], props.elementInfo.end[1]))
const { canvasScale } = storeToRefs(useMainStore())
const resizeHandlers = computed(() => {
const handlers = [
{
handler: OperateLineHandlers.START,
style: {
left: props.elementInfo.start[0] * canvasScale.value + 'px',
top: props.elementInfo.start[1] * canvasScale.value + 'px',
}
},
{
handler: OperateLineHandlers.END,
style: {
left: props.elementInfo.end[0] * canvasScale.value + 'px',
top: props.elementInfo.end[1] * canvasScale.value + 'px',
}
},
]
const svgWidth = computed(() => Math.max(props.elementInfo.start[0], props.elementInfo.end[0]))
const svgHeight = computed(() => Math.max(props.elementInfo.start[1], props.elementInfo.end[1]))
if (props.elementInfo.curve || props.elementInfo.broken) {
const ctrlHandler = (props.elementInfo.curve || props.elementInfo.broken) as [number, number]
handlers.push({
handler: OperateLineHandlers.C,
style: {
left: ctrlHandler[0] * canvasScale.value + 'px',
top: ctrlHandler[1] * canvasScale.value + 'px',
}
})
const resizeHandlers = computed(() => {
const handlers = [
{
handler: OperateLineHandlers.START,
style: {
left: props.elementInfo.start[0] * canvasScale.value + 'px',
top: props.elementInfo.start[1] * canvasScale.value + 'px',
}
else if (props.elementInfo.cubic) {
const [ctrlHandler1, ctrlHandler2] = props.elementInfo.cubic
handlers.push({
handler: OperateLineHandlers.C1,
style: {
left: ctrlHandler1[0] * canvasScale.value + 'px',
top: ctrlHandler1[1] * canvasScale.value + 'px',
}
})
handlers.push({
handler: OperateLineHandlers.C2,
style: {
left: ctrlHandler2[0] * canvasScale.value + 'px',
top: ctrlHandler2[1] * canvasScale.value + 'px',
}
})
},
{
handler: OperateLineHandlers.END,
style: {
left: props.elementInfo.end[0] * canvasScale.value + 'px',
top: props.elementInfo.end[1] * canvasScale.value + 'px',
}
},
]
return handlers
if (props.elementInfo.curve || props.elementInfo.broken) {
const ctrlHandler = (props.elementInfo.curve || props.elementInfo.broken) as [number, number]
handlers.push({
handler: OperateLineHandlers.C,
style: {
left: ctrlHandler[0] * canvasScale.value + 'px',
top: ctrlHandler[1] * canvasScale.value + 'px',
}
})
}
else if (props.elementInfo.cubic) {
const [ctrlHandler1, ctrlHandler2] = props.elementInfo.cubic
handlers.push({
handler: OperateLineHandlers.C1,
style: {
left: ctrlHandler1[0] * canvasScale.value + 'px',
top: ctrlHandler1[1] * canvasScale.value + 'px',
}
})
handlers.push({
handler: OperateLineHandlers.C2,
style: {
left: ctrlHandler2[0] * canvasScale.value + 'px',
top: ctrlHandler2[1] * canvasScale.value + 'px',
}
})
}
return {
svgWidth,
svgHeight,
canvasScale,
resizeHandlers,
}
},
return handlers
})
</script>

View File

@ -10,43 +10,31 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTElement, PPTElementLink } from '@/types/slides'
import useLink from '@/hooks/useLink'
export default defineComponent({
name: 'link-handler',
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
link: {
type: Object as PropType<PPTElementLink>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
const { removeLink } = useLink()
const height = computed(() => props.elementInfo.type === 'line' ? 0 : props.elementInfo.height)
return {
canvasScale,
height,
removeLink,
}
link: {
type: Object as PropType<PPTElementLink>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
})
const { canvasScale } = storeToRefs(useMainStore())
const { removeLink } = useLink()
const height = computed(() => props.elementInfo.type === 'line' ? 0 : props.elementInfo.height)
</script>
<style lang="scss" scoped>

View File

@ -20,8 +20,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, PropType, watchEffect } from 'vue'
<script lang="ts" setup>
import { computed, ref, PropType, watchEffect } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTElement } from '@/types/slides'
@ -32,65 +32,49 @@ import useCommonOperate from '../hooks/useCommonOperate'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'multi-select-operate',
components: {
ResizeHandler,
BorderLine,
const props = defineProps({
elementList: {
type: Array as PropType<PPTElement[]>,
required: true,
},
props: {
elementList: {
type: Array as PropType<PPTElement[]>,
required: true,
},
scaleMultiElement: {
type: Function as PropType<(e: MouseEvent, range: MultiSelectRange, command: OperateResizeHandlers) => void>,
required: true,
},
scaleMultiElement: {
type: Function as PropType<(e: MouseEvent, range: MultiSelectRange, command: OperateResizeHandlers) => void>,
required: true,
},
setup(props) {
const { activeElementIdList, canvasScale } = storeToRefs(useMainStore())
})
const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.id)))
const { activeElementIdList, canvasScale } = storeToRefs(useMainStore())
const range = ref({
minX: 0,
maxX: 0,
minY: 0,
maxY: 0,
})
const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.id)))
// 线
const width = computed(() => (range.value.maxX - range.value.minX) * canvasScale.value)
const height = computed(() => (range.value.maxY - range.value.minY) * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(width, height)
const range = ref({
minX: 0,
maxX: 0,
minY: 0,
maxY: 0,
})
//
const setRange = () => {
const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value)
range.value = { minX, maxX, minY, maxY }
}
watchEffect(setRange)
// 线
const width = computed(() => (range.value.maxX - range.value.minX) * canvasScale.value)
const height = computed(() => (range.value.maxY - range.value.minY) * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(width, height)
//
const disableResize = computed(() => {
return localActiveElementList.value.some(item => {
if (
(item.type === 'image' || item.type === 'shape') &&
!item.rotate
) return false
return true
})
})
//
const setRange = () => {
const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value)
range.value = { minX, maxX, minY, maxY }
}
watchEffect(setRange)
return {
range,
canvasScale,
borderLines,
disableResize,
resizeHandlers,
}
},
//
const disableResize = computed(() => {
return localActiveElementList.value.some(item => {
if (
(item.type === 'image' || item.type === 'shape') &&
!item.rotate
) return false
return true
})
})
</script>

View File

@ -2,41 +2,33 @@
<div :class="['resize-handler', rotateClassName, type]"></div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { OperateResizeHandlers } from '@/types/edit'
export default defineComponent({
name: 'resize-handler',
props: {
type: {
type: String as PropType<OperateResizeHandlers>,
default: '',
},
rotate: {
type: Number,
default: 0,
},
const props = defineProps({
type: {
type: String as PropType<OperateResizeHandlers>,
default: '',
},
setup(props) {
const rotateClassName = computed(() => {
const prefix = 'rotate-'
const rotate = props.rotate
if (rotate > -22.5 && rotate <= 22.5) return prefix + 0
else if (rotate > 22.5 && rotate <= 67.5) return prefix + 45
else if (rotate > 67.5 && rotate <= 112.5) return prefix + 90
else if (rotate > 112.5 && rotate <= 157.5) return prefix + 135
else if (rotate > 157.5 || rotate <= -157.5) return prefix + 0
else if (rotate > -157.5 && rotate <= -112.5) return prefix + 45
else if (rotate > -112.5 && rotate <= -67.5) return prefix + 90
else if (rotate > -67.5 && rotate <= -22.5) return prefix + 135
return prefix + 0
})
rotate: {
type: Number,
default: 0,
},
})
return {
rotateClassName,
}
},
const rotateClassName = computed(() => {
const prefix = 'rotate-'
const rotate = props.rotate
if (rotate > -22.5 && rotate <= 22.5) return prefix + 0
else if (rotate > 22.5 && rotate <= 67.5) return prefix + 45
else if (rotate > 67.5 && rotate <= 112.5) return prefix + 90
else if (rotate > 112.5 && rotate <= 157.5) return prefix + 135
else if (rotate > 157.5 || rotate <= -157.5) return prefix + 0
else if (rotate > -157.5 && rotate <= -112.5) return prefix + 45
else if (rotate > -112.5 && rotate <= -67.5) return prefix + 90
else if (rotate > -67.5 && rotate <= -22.5) return prefix + 135
return prefix + 0
})
</script>

View File

@ -2,10 +2,8 @@
<div class="rotate-handler"></div>
</template>
<script lang="ts">
export default {
name: 'rotate-handler',
}
<script lang="ts" setup>
</script>
<style lang="scss" scoped>

View File

@ -27,7 +27,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTShapeElement } from '@/types/slides'
@ -38,44 +44,28 @@ import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'shape-element-operate',
inheritAttrs: false,
components: {
RotateHandler,
ResizeHandler,
BorderLine,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTShapeElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTShapeElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTShapeElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTShapeElement, command: OperateResizeHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
return {
scaleWidth,
resizeHandlers,
borderLines,
}
rotateElement: {
type: Function as PropType<(element: PPTShapeElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTShapeElement, command: OperateResizeHandlers) => void>,
required: true,
},
})
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
</script>

View File

@ -27,7 +27,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTTableElement } from '@/types/slides'
@ -38,47 +44,31 @@ import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'table-element-operate',
inheritAttrs: false,
components: {
RotateHandler,
ResizeHandler,
BorderLine,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTTableElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTTableElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTTableElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTableElement, command: OperateResizeHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
const outlineWidth = computed(() => props.elementInfo.outline.width || 1)
const scaleWidth = computed(() => (props.elementInfo.width + outlineWidth.value) * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
return {
scaleWidth,
textElementResizeHandlers,
borderLines,
}
rotateElement: {
type: Function as PropType<(element: PPTTableElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTableElement, command: OperateResizeHandlers) => void>,
required: true,
},
})
const { canvasScale } = storeToRefs(useMainStore())
const outlineWidth = computed(() => props.elementInfo.outline.width || 1)
const scaleWidth = computed(() => (props.elementInfo.width + outlineWidth.value) * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
</script>

View File

@ -27,7 +27,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { PPTTextElement } from '@/types/slides'
@ -38,45 +44,29 @@ import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'text-element-operate',
inheritAttrs: false,
components: {
RotateHandler,
ResizeHandler,
BorderLine,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTTextElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void>,
required: true,
},
handlerVisible: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
return {
scaleWidth,
textElementResizeHandlers,
borderLines,
}
rotateElement: {
type: Function as PropType<(element: PPTTextElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandlers) => void>,
required: true,
},
})
const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
</script>

View File

@ -36,8 +36,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue'
<script lang="ts" setup>
import { PropType, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { ElementTypes, PPTElement, PPTLineElement, PPTVideoElement, PPTAudioElement } from '@/types/slides'
@ -51,90 +51,74 @@ import TableElementOperate from './TableElementOperate.vue'
import CommonElementOperate from './CommonElementOperate.vue'
import LinkHandler from './LinkHandler.vue'
export default defineComponent({
name: 'operate',
components: {
LinkHandler,
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: Exclude<PPTElement, PPTLineElement | PPTVideoElement | PPTAudioElement>) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void>,
required: true,
},
dragLineElement: {
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
setup(props) {
const { canvasScale, toolbarState } = storeToRefs(useMainStore())
const { formatedAnimations } = storeToRefs(useSlidesStore())
const currentOperateComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElementOperate,
[ElementTypes.TEXT]: TextElementOperate,
[ElementTypes.SHAPE]: ShapeElementOperate,
[ElementTypes.LINE]: LineElementOperate,
[ElementTypes.TABLE]: TableElementOperate,
[ElementTypes.CHART]: CommonElementOperate,
[ElementTypes.LATEX]: CommonElementOperate,
[ElementTypes.VIDEO]: CommonElementOperate,
[ElementTypes.AUDIO]: CommonElementOperate,
}
return elementTypeMap[props.elementInfo.type] || null
})
const elementIndexListInAnimation = computed(() => {
const indexList = []
for (let i = 0; i < formatedAnimations.value.length; i++) {
const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
if (elIds.includes(props.elementInfo.id)) indexList.push(i)
}
return indexList
})
const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
return {
currentOperateComponent,
canvasScale,
toolbarState,
elementIndexListInAnimation,
rotate,
height,
}
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
rotateElement: {
type: Function as PropType<(element: Exclude<PPTElement, PPTLineElement | PPTVideoElement | PPTAudioElement>) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void>,
required: true,
},
dragLineElement: {
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => void>,
required: true,
},
openLinkDialog: {
type: Function as PropType<() => void>,
required: true,
},
})
const { canvasScale, toolbarState } = storeToRefs(useMainStore())
const { formatedAnimations } = storeToRefs(useSlidesStore())
const currentOperateComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElementOperate,
[ElementTypes.TEXT]: TextElementOperate,
[ElementTypes.SHAPE]: ShapeElementOperate,
[ElementTypes.LINE]: LineElementOperate,
[ElementTypes.TABLE]: TableElementOperate,
[ElementTypes.CHART]: CommonElementOperate,
[ElementTypes.LATEX]: CommonElementOperate,
[ElementTypes.VIDEO]: CommonElementOperate,
[ElementTypes.AUDIO]: CommonElementOperate,
}
return elementTypeMap[props.elementInfo.type] || null
})
const elementIndexListInAnimation = computed(() => {
const indexList = []
for (let i = 0; i < formatedAnimations.value.length; i++) {
const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
if (elIds.includes(props.elementInfo.id)) indexList.push(i)
}
return indexList
})
const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
</script>
<style lang="scss" scoped>

View File

@ -36,8 +36,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
@ -48,25 +48,17 @@ interface ViewportStyles {
height: number
}
export default defineComponent({
props: {
viewportStyles: {
type: Object as PropType<ViewportStyles>,
required: true,
},
const props = defineProps({
viewportStyles: {
type: Object as PropType<ViewportStyles>,
required: true,
},
setup(props) {
const { canvasScale } = storeToRefs(useMainStore())
})
const markerSize = computed(() => {
return props.viewportStyles.width * canvasScale.value / 10
})
const { canvasScale } = storeToRefs(useMainStore())
return {
canvasScale,
markerSize,
}
},
const markerSize = computed(() => {
return props.viewportStyles.width * canvasScale.value / 10
})
</script>

View File

@ -7,32 +7,19 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { SlideBackground } from '@/types/slides'
import GridLines from './GridLines.vue'
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
export default defineComponent({
name: 'viewport-background',
components: {
GridLines,
},
setup() {
const { showGridLines } = storeToRefs(useMainStore())
const { currentSlide } = storeToRefs(useSlidesStore())
const background = computed<SlideBackground | undefined>(() => currentSlide.value?.background)
const { showGridLines } = storeToRefs(useMainStore())
const { currentSlide } = storeToRefs(useSlidesStore())
const background = computed<SlideBackground | undefined>(() => currentSlide.value?.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)
return {
showGridLines,
backgroundStyle,
}
},
})
const { backgroundStyle } = useSlideBackgroundStyle(background)
</script>
<style lang="scss" scoped>

View File

@ -91,8 +91,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, provide, ref, watch, watchEffect } from 'vue'
<script lang="ts" setup>
import { onMounted, provide, ref, watch, watchEffect } from 'vue'
import { throttle } from 'lodash'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
@ -130,192 +130,146 @@ import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
import Operate from './Operate/index.vue'
import LinkDialog from './LinkDialog.vue'
export default defineComponent({
name: 'editor-canvas',
components: {
EditableElement,
MouseSelection,
ViewportBackground,
AlignmentLine,
Ruler,
ElementCreateSelection,
MultiSelectOperate,
Operate,
LinkDialog,
},
setup() {
const mainStore = useMainStore()
const {
activeElementIdList,
activeGroupElementId,
handleElementId,
editorAreaFocus,
showGridLines,
showRuler,
creatingElement,
canvasScale,
} = storeToRefs(mainStore)
const { currentSlide } = storeToRefs(useSlidesStore())
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
const mainStore = useMainStore()
const {
activeElementIdList,
activeGroupElementId,
handleElementId,
editorAreaFocus,
showGridLines,
showRuler,
creatingElement,
canvasScale,
} = storeToRefs(mainStore)
const { currentSlide } = storeToRefs(useSlidesStore())
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
const viewportRef = ref<HTMLElement>()
const alignmentLines = ref<AlignmentLineProps[]>([])
const viewportRef = ref<HTMLElement>()
const alignmentLines = ref<AlignmentLineProps[]>([])
const linkDialogVisible = ref(false)
const openLinkDialog = () => linkDialogVisible.value = true
const linkDialogVisible = ref(false)
const openLinkDialog = () => linkDialogVisible.value = true
watch(handleElementId, () => {
mainStore.setActiveGroupElementId('')
})
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
watchEffect(setLocalElementList)
const canvasRef = ref<HTMLElement>()
const { dragViewport, viewportStyles } = useViewportSize(canvasRef)
useDropImageOrText(canvasRef)
const { mouseSelection, mouseSelectionVisible, mouseSelectionQuadrant, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
const { dragElement } = useDragElement(elementList, alignmentLines, canvasScale)
const { dragLineElement } = useDragLineElement(elementList)
const { selectElement } = useSelectElement(elementList, dragElement)
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, alignmentLines, canvasScale)
const { rotateElement } = useRotateElement(elementList, viewportRef)
const { selectAllElement } = useSelectAllElement()
const { deleteAllElements } = useDeleteElement()
const { pasteElement } = useCopyAndPasteElement()
const { enterScreeningFromStart } = useScreening()
const { updateSlideIndex } = useSlideHandler()
//
// 退
onMounted(() => {
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
})
//
const handleClickBlankArea = (e: MouseEvent) => {
mainStore.setActiveElementIdList([])
if (!spaceKeyState.value) updateMouseSelection(e)
else dragViewport(e)
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
removeAllRanges()
}
//
const removeEditorAreaFocus = () => {
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
}
//
const { scaleCanvas } = useScaleCanvas()
const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false })
const throttleUpdateSlideIndex = throttle(updateSlideIndex, 300, { leading: true, trailing: false })
const handleMousewheelCanvas = (e: WheelEvent) => {
e.preventDefault()
// Ctrl
if (ctrlKeyState.value) {
if (e.deltaY > 0) throttleScaleCanvas('-')
else if (e.deltaY < 0) throttleScaleCanvas('+')
}
//
else {
if (e.deltaY > 0) throttleUpdateSlideIndex(KEYS.DOWN)
else if (e.deltaY < 0) throttleUpdateSlideIndex(KEYS.UP)
}
}
// 线
const toggleGridLines = () => {
mainStore.setGridLinesState(!showGridLines.value)
}
//
const toggleRuler = () => {
mainStore.setRulerState(!showRuler.value)
}
//
const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteElement,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllElement,
},
{
text: '网格线',
subText: showGridLines.value ? '√' : '',
handler: toggleGridLines,
},
{
text: '标尺',
subText: showRuler.value ? '√' : '',
handler: toggleRuler,
},
{
text: '重置当前页',
handler: deleteAllElements,
},
{ divider: true },
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
provide(injectKeySlideScale, canvasScale)
return {
elementList,
activeElementIdList,
handleElementId,
activeGroupElementId,
canvasRef,
viewportRef,
viewportStyles,
canvasScale,
mouseSelection,
mouseSelectionVisible,
mouseSelectionQuadrant,
creatingElement,
alignmentLines,
linkDialogVisible,
spaceKeyState,
showRuler,
openLinkDialog,
handleClickBlankArea,
removeEditorAreaFocus,
insertElementFromCreateSelection,
selectElement,
rotateElement,
scaleElement,
dragLineElement,
scaleMultiElement,
handleMousewheelCanvas,
contextmenus,
}
},
watch(handleElementId, () => {
mainStore.setActiveGroupElementId('')
})
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
watchEffect(setLocalElementList)
const canvasRef = ref<HTMLElement>()
const { dragViewport, viewportStyles } = useViewportSize(canvasRef)
useDropImageOrText(canvasRef)
const { mouseSelection, mouseSelectionVisible, mouseSelectionQuadrant, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
const { dragElement } = useDragElement(elementList, alignmentLines, canvasScale)
const { dragLineElement } = useDragLineElement(elementList)
const { selectElement } = useSelectElement(elementList, dragElement)
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, alignmentLines, canvasScale)
const { rotateElement } = useRotateElement(elementList, viewportRef)
const { selectAllElement } = useSelectAllElement()
const { deleteAllElements } = useDeleteElement()
const { pasteElement } = useCopyAndPasteElement()
const { enterScreeningFromStart } = useScreening()
const { updateSlideIndex } = useSlideHandler()
//
// 退
onMounted(() => {
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
})
//
const handleClickBlankArea = (e: MouseEvent) => {
mainStore.setActiveElementIdList([])
if (!spaceKeyState.value) updateMouseSelection(e)
else dragViewport(e)
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
removeAllRanges()
}
//
const removeEditorAreaFocus = () => {
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
}
//
const { scaleCanvas } = useScaleCanvas()
const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false })
const throttleUpdateSlideIndex = throttle(updateSlideIndex, 300, { leading: true, trailing: false })
const handleMousewheelCanvas = (e: WheelEvent) => {
e.preventDefault()
// Ctrl
if (ctrlKeyState.value) {
if (e.deltaY > 0) throttleScaleCanvas('-')
else if (e.deltaY < 0) throttleScaleCanvas('+')
}
//
else {
if (e.deltaY > 0) throttleUpdateSlideIndex(KEYS.DOWN)
else if (e.deltaY < 0) throttleUpdateSlideIndex(KEYS.UP)
}
}
// 线
const toggleGridLines = () => {
mainStore.setGridLinesState(!showGridLines.value)
}
//
const toggleRuler = () => {
mainStore.setRulerState(!showRuler.value)
}
//
const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteElement,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllElement,
},
{
text: '网格线',
subText: showGridLines.value ? '√' : '',
handler: toggleGridLines,
},
{
text: '标尺',
subText: showRuler.value ? '√' : '',
handler: toggleRuler,
},
{
text: '重置当前页',
handler: deleteAllElements,
},
{ divider: true },
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
provide(injectKeySlideScale, canvasScale)
</script>
<style lang="scss" scoped>

View File

@ -14,26 +14,18 @@
</ul>
</template>
<script lang="ts">
<script lang="ts" setup>
import { PresetChartType } from '@/types/slides'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'chart-pool',
emits: ['select'],
setup(props, { emit }) {
const chartList: PresetChartType[] = ['bar', 'horizontalBar', 'line', 'area', 'scatter', 'pie', 'ring']
const emit = defineEmits<{
(event: 'select', payload: PresetChartType): void
}>()
const selectChart = (chart: PresetChartType) => {
emit('select', chart)
}
const chartList: PresetChartType[] = ['bar', 'horizontalBar', 'line', 'area', 'scatter', 'pie', 'ring']
return {
chartList,
selectChart,
}
},
})
const selectChart = (chart: PresetChartType) => {
emit('select', chart)
}
</script>
<style lang="scss" scoped>

View File

@ -1,6 +1,6 @@
<template>
<div class="line-pool">
<div class="category" v-for="(item, i) in lineList" :key="item.type">
<div class="category" v-for="(item, i) in LINE_LIST" :key="item.type">
<div class="category-name">{{item.type}}</div>
<div class="line-list">
<div class="line-item" v-for="(line, j) in item.children" :key="j">
@ -48,31 +48,18 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { LINE_LIST, LinePoolItem } from '@/configs/lines'
import LinePointMarker from '@/views/components/element/LineElement/LinePointMarker.vue'
export default defineComponent({
name: 'line-pool',
emits: ['select'],
components: {
LinePointMarker,
},
setup(props, { emit }) {
const lineList = LINE_LIST
const emit = defineEmits<{
(event: 'select', payload: LinePoolItem): void
}>()
const selectLine = (line: LinePoolItem) => {
emit('select', line)
}
return {
lineList,
selectLine,
}
},
})
const selectLine = (line: LinePoolItem) => {
emit('select', line)
}
</script>
<style lang="scss" scoped>

View File

@ -28,8 +28,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
type TypeKey = 'video' | 'audio'
@ -38,43 +38,33 @@ interface TabItem {
label: string
}
export default defineComponent({
name: 'media-input',
emits: ['insertVideo', 'insertAudio', 'close'],
setup(props, { emit }) {
const type = ref<TypeKey>('video')
const emit = defineEmits<{
(event: 'insertVideo', payload: string): void
(event: 'insertAudio', payload: string): void
(event: 'close'): void
}>()
const videoSrc = ref('https://mazwai.com/videvo_files/video/free/2019-01/small_watermarked/181004_04_Dolphins-Whale_06_preview.webm')
const audioSrc = ref('https://freesound.org/data/previews/614/614107_11861866-lq.mp3')
const type = ref<TypeKey>('video')
const tabs: TabItem[] = [
{ key: 'video', label: '视频' },
{ key: 'audio', label: '音频' },
]
const videoSrc = ref('https://mazwai.com/videvo_files/video/free/2019-01/small_watermarked/181004_04_Dolphins-Whale_06_preview.webm')
const audioSrc = ref('https://freesound.org/data/previews/614/614107_11861866-lq.mp3')
const insertVideo = () => {
if (!videoSrc.value) return message.error('请先输入正确的视频地址')
emit('insertVideo', videoSrc.value)
}
const tabs: TabItem[] = [
{ key: 'video', label: '视频' },
{ key: 'audio', label: '音频' },
]
const insertAudio = () => {
if (!audioSrc.value) return message.error('请先输入正确的频地址')
emit('insertAudio', audioSrc.value)
}
const insertVideo = () => {
if (!videoSrc.value) return message.error('请先输入正确的频地址')
emit('insertVideo', videoSrc.value)
}
const close = () => emit('close')
const insertAudio = () => {
if (!audioSrc.value) return message.error('请先输入正确的音频地址')
emit('insertAudio', audioSrc.value)
}
return {
type,
videoSrc,
audioSrc,
tabs,
insertVideo,
insertAudio,
close,
}
},
})
const close = () => emit('close')
</script>
<style lang="scss" scoped>

View File

@ -1,6 +1,6 @@
<template>
<div class="shape-pool">
<div class="category" v-for="item in shapeList" :key="item.type">
<div class="category" v-for="item in SHAPE_LIST" :key="item.type">
<div class="category-name">{{item.type}}</div>
<div class="shape-list">
<div class="shape-item" v-for="(shape, index) in item.children" :key="index">
@ -33,26 +33,16 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { SHAPE_LIST, ShapePoolItem } from '@/configs/shapes'
export default defineComponent({
name: 'shape-pool',
emits: ['select'],
setup(props, { emit }) {
const shapeList = SHAPE_LIST
const emit = defineEmits<{
(event: 'select', payload: ShapePoolItem): void
}>()
const selectShape = (shape: ShapePoolItem) => {
emit('select', shape)
}
return {
shapeList,
selectShape,
}
},
})
const selectShape = (shape: ShapePoolItem) => {
emit('select', shape)
}
</script>
<style lang="scss" scoped>

View File

@ -51,49 +51,42 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
export default defineComponent({
name: 'table-generator',
emits: ['insert', 'close'],
setup(props, { emit }) {
const endCell = ref<number[]>([])
const customRow = ref(3)
const customCol = ref(3)
const isCustom = ref(false)
interface InsertData {
row: number
col: number
}
const handleClickTable = () => {
if (!endCell.value.length) return
const [row, col] = endCell.value
emit('insert', { row, col })
}
const emit = defineEmits<{
(event: 'insert', payload: InsertData): void
(event: 'close'): void
}>()
const insertCustomTable = () => {
if (customRow.value < 1 || customRow.value > 20) return message.warning('行数/列数必须在0~20之间')
if (customCol.value < 1 || customCol.value > 20) return message.warning('行数/列数必须在0~20之间')
emit('insert', { row: customRow.value, col: customCol.value })
isCustom.value = false
}
const endCell = ref<number[]>([])
const customRow = ref(3)
const customCol = ref(3)
const isCustom = ref(false)
const close = () => {
emit('close')
isCustom.value = false
}
const handleClickTable = () => {
if (!endCell.value.length) return
const [row, col] = endCell.value
emit('insert', { row, col })
}
return {
endCell,
customRow,
customCol,
handleClickTable,
insertCustomTable,
isCustom,
close,
}
},
})
const insertCustomTable = () => {
if (customRow.value < 1 || customRow.value > 20) return message.warning('行数/列数必须在0~20之间')
if (customCol.value < 1 || customCol.value > 20) return message.warning('行数/列数必须在0~20之间')
emit('insert', { row: customRow.value, col: customCol.value })
isCustom.value = false
}
const close = () => {
emit('close')
isCustom.value = false
}
</script>
<style lang="scss" scoped>

View File

@ -106,8 +106,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSnapshotStore } from '@/store'
import { getImageDataURL } from '@/utils/image'
@ -124,115 +124,73 @@ import TableGenerator from './TableGenerator.vue'
import MediaInput from './MediaInput.vue'
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
export default defineComponent({
name: 'canvas-tool',
components: {
ShapePool,
LinePool,
ChartPool,
TableGenerator,
MediaInput,
LaTeXEditor,
},
setup() {
const mainStore = useMainStore()
const { creatingElement } = storeToRefs(mainStore)
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
const mainStore = useMainStore()
const { creatingElement } = storeToRefs(mainStore)
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
const { redo, undo } = useHistorySnapshot()
const { redo, undo } = useHistorySnapshot()
const {
scaleCanvas,
setCanvasScalePercentage,
resetCanvas,
canvasScalePercentage,
} = useScaleCanvas()
const canvasScalePresetList = [200, 150, 100, 80, 50]
const canvasScaleVisible = ref(false)
const {
scaleCanvas,
setCanvasScalePercentage,
resetCanvas,
canvasScalePercentage,
} = useScaleCanvas()
const applyCanvasPresetScale = (value: number) => {
setCanvasScalePercentage(value)
canvasScaleVisible.value = false
}
const canvasScalePresetList = [200, 150, 100, 80, 50]
const canvasScaleVisible = ref(false)
const {
createImageElement,
createChartElement,
createTableElement,
createLatexElement,
createVideoElement,
createAudioElement,
} = useCreateElement()
const applyCanvasPresetScale = (value: number) => {
setCanvasScalePercentage(value)
canvasScaleVisible.value = false
}
const insertImageElement = (files: File[]) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
}
const {
createImageElement,
createChartElement,
createTableElement,
createLatexElement,
createVideoElement,
createAudioElement,
} = useCreateElement()
const shapePoolVisible = ref(false)
const linePoolVisible = ref(false)
const chartPoolVisible = ref(false)
const tableGeneratorVisible = ref(false)
const mediaInputVisible = ref(false)
const latexEditorVisible = ref(false)
const insertImageElement = (files: FileList) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
}
//
const drawText = () => {
mainStore.setCreatingElement({
type: 'text',
})
}
const shapePoolVisible = ref(false)
const linePoolVisible = ref(false)
const chartPoolVisible = ref(false)
const tableGeneratorVisible = ref(false)
const mediaInputVisible = ref(false)
const latexEditorVisible = ref(false)
//
const drawShape = (shape: ShapePoolItem) => {
mainStore.setCreatingElement({
type: 'shape',
data: shape,
})
shapePoolVisible.value = false
}
//
const drawText = () => {
mainStore.setCreatingElement({
type: 'text',
})
}
// 线
const drawLine = (line: LinePoolItem) => {
mainStore.setCreatingElement({
type: 'line',
data: line,
})
linePoolVisible.value = false
}
//
const drawShape = (shape: ShapePoolItem) => {
mainStore.setCreatingElement({
type: 'shape',
data: shape,
})
shapePoolVisible.value = false
}
return {
scaleCanvas,
resetCanvas,
canvasScalePercentage,
canvasScaleVisible,
canvasScalePresetList,
applyCanvasPresetScale,
canUndo,
canRedo,
redo,
undo,
insertImageElement,
shapePoolVisible,
linePoolVisible,
chartPoolVisible,
tableGeneratorVisible,
mediaInputVisible,
latexEditorVisible,
creatingElement,
drawText,
drawShape,
drawLine,
createChartElement,
createTableElement,
createLatexElement,
createVideoElement,
createAudioElement,
}
},
})
// 线
const drawLine = (line: LinePoolItem) => {
mainStore.setCreatingElement({
type: 'line',
data: line,
})
linePoolVisible.value = false
}
</script>
<style lang="scss" scoped>

View File

@ -1,6 +1,6 @@
<template>
<div class="hotkey-doc">
<template v-for="item in hotkeys" :key="item.type">
<template v-for="item in HOTKEY_DOC" :key="item.type">
<div class="title">{{item.type}}</div>
<div class="hotkey-item" v-for="hotkey in item.children" :key="hotkey.label">
<div class="label">{{hotkey.label}}</div>
@ -10,18 +10,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { HOTKEY_DOC } from '@/configs/hotkey'
export default defineComponent({
name: 'hotkey-doc',
setup() {
return {
hotkeys: HOTKEY_DOC,
}
},
})
<script lang="ts" setup>
import { HOTKEY_DOC } from '@/configs/hotkey'
</script>
<style lang="scss" scoped>

View File

@ -78,8 +78,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import useScreening from '@/hooks/useScreening'
@ -89,55 +89,29 @@ import useExport from '@/hooks/useExport'
import HotkeyDoc from './HotkeyDoc.vue'
export default defineComponent({
name: 'editor-header',
components: {
HotkeyDoc,
},
setup() {
const mainStore = useMainStore()
const { showGridLines, showRuler } = storeToRefs(mainStore)
const mainStore = useMainStore()
const { showGridLines, showRuler } = storeToRefs(mainStore)
const { enterScreening, enterScreeningFromStart } = useScreening()
const { createSlide, deleteSlide, resetSlides } = useSlideHandler()
const { redo, undo } = useHistorySnapshot()
const { importSpecificFile } = useExport()
const { enterScreening, enterScreeningFromStart } = useScreening()
const { createSlide, deleteSlide, resetSlides } = useSlideHandler()
const { redo, undo } = useHistorySnapshot()
const { importSpecificFile } = useExport()
const setDialogForExport = mainStore.setDialogForExport
const setDialogForExport = mainStore.setDialogForExport
const toggleGridLines = () => {
mainStore.setGridLinesState(!showGridLines.value)
}
const toggleGridLines = () => {
mainStore.setGridLinesState(!showGridLines.value)
}
const toggleRuler = () => {
mainStore.setRulerState(!showRuler.value)
}
const toggleRuler = () => {
mainStore.setRulerState(!showRuler.value)
}
const hotkeyDrawerVisible = ref(false)
const hotkeyDrawerVisible = ref(false)
const goIssues = () => {
window.open('https://github.com/pipipi-pikachu/PPTist/issues')
}
return {
redo,
undo,
showGridLines,
showRuler,
hotkeyDrawerVisible,
importSpecificFile,
setDialogForExport,
enterScreening,
enterScreeningFromStart,
createSlide,
deleteSlide,
toggleGridLines,
toggleRuler,
resetSlides,
goIssues,
}
},
})
const goIssues = () => {
window.open('https://github.com/pipipi-pikachu/PPTist/issues')
}
</script>
<style lang="scss" scoped>

View File

@ -75,62 +75,44 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useExport from '@/hooks/useExport'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'export-img-dialog',
components: {
ThumbnailSlide,
},
setup(props, { emit }) {
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'close'): void
}>()
const imageThumbnailsRef = ref<HTMLElement>()
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const format = ref<'jpeg' | 'png'>('jpeg')
const quality = ref(1)
const ignoreWebfont = ref(true)
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const renderSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const imageThumbnailsRef = ref<HTMLElement>()
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const format = ref<'jpeg' | 'png'>('jpeg')
const quality = ref(1)
const ignoreWebfont = ref(true)
const close = () => emit('close')
const { exportImage, exporting } = useExport()
const expImage = () => {
if (!imageThumbnailsRef.value) return
exportImage(imageThumbnailsRef.value, format.value, quality.value, ignoreWebfont.value)
}
return {
imageThumbnailsRef,
slides,
rangeType,
range,
format,
quality,
ignoreWebfont,
renderSlides,
exporting,
expImage,
close,
}
},
const renderSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const close = () => emit('close')
const { exportImage, exporting } = useExport()
const expImage = () => {
if (!imageThumbnailsRef.value) return
exportImage(imageThumbnailsRef.value, format.value, quality.value, ignoreWebfont.value)
}
</script>
<style lang="scss" scoped>

View File

@ -11,28 +11,18 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useExport from '@/hooks/useExport'
export default defineComponent({
name: 'export-json-dialog',
setup(props, { emit }) {
const close = () => emit('close')
const emit = defineEmits<{
(event: 'close'): void
}>()
const { slides } = storeToRefs(useSlidesStore())
const { exportJSON } = useExport()
return {
slides,
exportJSON,
close,
}
},
})
const { slides } = storeToRefs(useSlidesStore())
const { exportJSON } = useExport()
const close = () => emit('close')
</script>
<style lang="scss" scoped>

View File

@ -60,51 +60,36 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { print } from '@/utils/print'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'export-pdf-dialog',
components: {
ThumbnailSlide,
},
setup(props, { emit }) {
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'close'): void
}>()
const pdfThumbnailsRef = ref<HTMLElement>()
const rangeType = ref<'all' | 'current'>('all')
const count = ref(1)
const padding = ref(true)
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const close = () => emit('close')
const pdfThumbnailsRef = ref<HTMLElement>()
const rangeType = ref<'all' | 'current'>('all')
const count = ref(1)
const padding = ref(true)
const expPDF = () => {
if (!pdfThumbnailsRef.value) return
const pageSize = {
width: 1600,
height: rangeType.value === 'all' ? 900 * count.value : 900,
margin: padding.value ? 50 : 0,
}
print(pdfThumbnailsRef.value, pageSize)
}
return {
pdfThumbnailsRef,
slides,
currentSlide,
rangeType,
count,
padding,
expPDF,
close,
}
},
})
const close = () => emit('close')
const expPDF = () => {
if (!pdfThumbnailsRef.value) return
const pageSize = {
width: 1600,
height: rangeType.value === 'all' ? 900 * count.value : 900,
margin: padding.value ? 50 : 0,
}
print(pdfThumbnailsRef.value, pageSize)
}
</script>
<style lang="scss" scoped>

View File

@ -39,46 +39,34 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useExport from '@/hooks/useExport'
export default defineComponent({
name: 'export-pptx-dialog',
setup(props, { emit }) {
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'close'): void
}>()
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const masterOverwrite = ref(true)
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const selectedSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const { exportPPTX, exporting } = useExport()
const close = () => emit('close')
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const masterOverwrite = ref(true)
const { exportPPTX, exporting } = useExport()
return {
slides,
rangeType,
range,
masterOverwrite,
exporting,
selectedSlides,
exportPPTX,
close,
}
},
const selectedSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const close = () => emit('close')
</script>
<style lang="scss" scoped>

View File

@ -34,43 +34,33 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useExport from '@/hooks/useExport'
export default defineComponent({
name: 'export-pptist-dialog',
setup(props, { emit }) {
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'close'): void
}>()
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const selectedSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const { exportSpecificFile } = useExport()
const close = () => emit('close')
const rangeType = ref<'all' | 'current' | 'custom'>('all')
const range = ref<[number, number]>([1, slides.value.length])
const { exportSpecificFile } = useExport()
return {
slides,
rangeType,
range,
selectedSlides,
exportSpecificFile,
close,
}
},
const selectedSlides = computed(() => {
if (rangeType.value === 'all') return slides.value
if (rangeType.value === 'current') return [currentSlide.value]
return slides.value.filter((item, index) => {
const [min, max] = range.value
return index >= min - 1 && index <= max - 1
})
})
const close = () => emit('close')
</script>
<style lang="scss" scoped>

View File

@ -15,8 +15,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { DialogForExportTypes } from '@/types/export'
@ -32,40 +32,28 @@ interface TabItem {
label: string
}
export default defineComponent({
name: 'export-dialog',
setup() {
const mainStore = useMainStore()
const { dialogForExport } = storeToRefs(mainStore)
const mainStore = useMainStore()
const { dialogForExport } = storeToRefs(mainStore)
const setDialogForExport = mainStore.setDialogForExport
const setDialogForExport = mainStore.setDialogForExport
const tabs: TabItem[] = [
{ key: 'pptist', label: '导出 pptist 文件' },
{ key: 'pptx', label: '导出 PPTX' },
{ key: 'image', label: '导出图片' },
{ key: 'json', label: '导出 JSON' },
{ key: 'pdf', label: '打印 / 导出 PDF' },
]
const tabs: TabItem[] = [
{ key: 'pptist', label: '导出 pptist 文件' },
{ key: 'pptx', label: '导出 PPTX' },
{ key: 'image', label: '导出图片' },
{ key: 'json', label: '导出 JSON' },
{ key: 'pdf', label: '打印 / 导出 PDF' },
]
const currentDialogComponent = computed(() => {
const dialogMap = {
'image': ExportImage,
'json': ExportJSON,
'pdf': ExportPDF,
'pptx': ExportPPTX,
'pptist': ExportSpecificFile,
}
return dialogMap[dialogForExport.value] || null
})
return {
currentDialogComponent,
tabs,
dialogForExport,
setDialogForExport,
}
},
const currentDialogComponent = computed(() => {
const dialogMap = {
'image': ExportImage,
'json': ExportJSON,
'pdf': ExportPDF,
'pptx': ExportPPTX,
'pptist': ExportSpecificFile,
}
return dialogMap[dialogForExport.value] || null
})
</script>

View File

@ -12,64 +12,57 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
export default defineComponent({
name: 'remark',
emits: ['update:height'],
props: {
height: {
type: Number,
required: true,
},
},
setup(props, { emit }) {
const slidesStore = useSlidesStore()
const { currentSlide } = storeToRefs(slidesStore)
const remark = computed(() => currentSlide.value?.remark || '')
const handleInput = (e: Event) => {
const value = (e.target as HTMLTextAreaElement).value
slidesStore.updateSlide({ remark: value })
}
const resize = (e: MouseEvent) => {
let isMouseDown = true
const startPageY = e.pageY
const originHeight = props.height
document.onmousemove = e => {
if (!isMouseDown) return
const currentPageY = e.pageY
const moveY = currentPageY - startPageY
let newHeight = -moveY + originHeight
if (newHeight < 40) newHeight = 40
if (newHeight > 120) newHeight = 120
emit('update:height', newHeight)
}
document.onmouseup = () => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
}
}
return {
remark,
handleInput,
resize,
}
const props = defineProps({
height: {
type: Number,
required: true,
},
})
const emit = defineEmits<{
(event: 'update:height', payload: number): void
}>()
const slidesStore = useSlidesStore()
const { currentSlide } = storeToRefs(slidesStore)
const remark = computed(() => currentSlide.value?.remark || '')
const handleInput = (e: Event) => {
const value = (e.target as HTMLTextAreaElement).value
slidesStore.updateSlide({ remark: value })
}
const resize = (e: MouseEvent) => {
let isMouseDown = true
const startPageY = e.pageY
const originHeight = props.height
document.onmousemove = e => {
if (!isMouseDown) return
const currentPageY = e.pageY
const moveY = currentPageY - startPageY
let newHeight = -moveY + originHeight
if (newHeight < 40) newHeight = 40
if (newHeight > 120) newHeight = 120
emit('update:height', newHeight)
}
document.onmouseup = () => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
}
}
</script>
<style lang="scss" scoped>

View File

@ -11,33 +11,22 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { Slide } from '@/types/slides'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'layout-pool',
emits: ['select'],
components: {
ThumbnailSlide,
},
setup(props, { emit }) {
const { layouts } = storeToRefs(useSlidesStore())
const emit = defineEmits<{
(event: 'select', payload: Slide): void
}>()
const selectSlideTemplate = (slide: Slide) => {
emit('select', slide)
}
const { layouts } = storeToRefs(useSlidesStore())
return {
layouts,
selectSlideTemplate,
}
},
})
const selectSlideTemplate = (slide: Slide) => {
emit('select', slide)
}
</script>
<style lang="scss" scoped>

View File

@ -43,8 +43,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
import { fillDigit } from '@/utils/common'
@ -57,209 +57,183 @@ import Draggable from 'vuedraggable'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
import LayoutPool from './LayoutPool.vue'
export default defineComponent({
name: 'thumbnails',
components: {
Draggable,
ThumbnailSlide,
LayoutPool,
},
setup() {
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const keyboardStore = useKeyboardStore()
const { selectedSlidesIndex: _selectedSlidesIndex, thumbnailsFocus } = storeToRefs(mainStore)
const { slides, slideIndex } = storeToRefs(slidesStore)
const { ctrlKeyState, shiftKeyState } = storeToRefs(keyboardStore)
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const keyboardStore = useKeyboardStore()
const { selectedSlidesIndex: _selectedSlidesIndex, thumbnailsFocus } = storeToRefs(mainStore)
const { slides, slideIndex } = storeToRefs(slidesStore)
const { ctrlKeyState, shiftKeyState } = storeToRefs(keyboardStore)
const { slidesLoadLimit } = useLoadSlides()
const { slidesLoadLimit } = useLoadSlides()
const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
const presetLayoutPopoverVisible = ref(false)
const presetLayoutPopoverVisible = ref(false)
const {
copySlide,
pasteSlide,
createSlide,
createSlideByTemplate,
copyAndPasteSlide,
deleteSlide,
cutSlide,
selectAllSlide,
} = useSlideHandler()
const {
copySlide,
pasteSlide,
createSlide,
createSlideByTemplate,
copyAndPasteSlide,
deleteSlide,
cutSlide,
selectAllSlide,
} = useSlideHandler()
//
const changSlideIndex = (index: number) => {
mainStore.setActiveElementIdList([])
//
const changSlideIndex = (index: number) => {
mainStore.setActiveElementIdList([])
if (slideIndex.value === index) return
slidesStore.updateSlideIndex(index)
if (slideIndex.value === index) return
slidesStore.updateSlideIndex(index)
}
//
const handleClickSlideThumbnail = (e: MouseEvent, index: number) => {
const isMultiSelected = selectedSlidesIndex.value.length > 1
if (isMultiSelected && selectedSlidesIndex.value.includes(index) && e.button !== 0) return
// Ctrl
if (ctrlKeyState.value) {
if (slideIndex.value === index) {
if (!isMultiSelected) return
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changSlideIndex(selectedSlidesIndex.value[0])
}
//
const handleClickSlideThumbnail = (e: MouseEvent, index: number) => {
const isMultiSelected = selectedSlidesIndex.value.length > 1
if (isMultiSelected && selectedSlidesIndex.value.includes(index) && e.button !== 0) return
// Ctrl
if (ctrlKeyState.value) {
if (slideIndex.value === index) {
if (!isMultiSelected) return
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changSlideIndex(selectedSlidesIndex.value[0])
}
else {
if (selectedSlidesIndex.value.includes(index)) {
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
}
else {
const newSelectedSlidesIndex = [...selectedSlidesIndex.value, index]
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changSlideIndex(index)
}
}
else {
if (selectedSlidesIndex.value.includes(index)) {
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
}
// Shift
else if (shiftKeyState.value) {
if (slideIndex.value === index && !isMultiSelected) return
let minIndex = Math.min(...selectedSlidesIndex.value)
let maxIndex = index
if (index < minIndex) {
maxIndex = Math.max(...selectedSlidesIndex.value)
minIndex = index
}
const newSelectedSlidesIndex = []
for (let i = minIndex; i <= maxIndex; i++) newSelectedSlidesIndex.push(i)
else {
const newSelectedSlidesIndex = [...selectedSlidesIndex.value, index]
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changSlideIndex(index)
}
//
else {
mainStore.updateSelectedSlidesIndex([])
changSlideIndex(index)
}
}
}
// Shift
else if (shiftKeyState.value) {
if (slideIndex.value === index && !isMultiSelected) return
let minIndex = Math.min(...selectedSlidesIndex.value)
let maxIndex = index
if (index < minIndex) {
maxIndex = Math.max(...selectedSlidesIndex.value)
minIndex = index
}
//
const setThumbnailsFocus = (focus: boolean) => {
if (thumbnailsFocus.value === focus) return
mainStore.setThumbnailsFocus(focus)
const newSelectedSlidesIndex = []
for (let i = minIndex; i <= maxIndex; i++) newSelectedSlidesIndex.push(i)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changSlideIndex(index)
}
//
else {
mainStore.updateSelectedSlidesIndex([])
changSlideIndex(index)
}
}
if (!focus) mainStore.updateSelectedSlidesIndex([])
}
//
const setThumbnailsFocus = (focus: boolean) => {
if (thumbnailsFocus.value === focus) return
mainStore.setThumbnailsFocus(focus)
//
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData
if (oldIndex === newIndex) return
if (!focus) mainStore.updateSelectedSlidesIndex([])
}
const _slides = JSON.parse(JSON.stringify(slides.value))
const _slide = _slides[oldIndex]
_slides.splice(oldIndex, 1)
_slides.splice(newIndex, 0, _slide)
slidesStore.setSlides(_slides)
slidesStore.updateSlideIndex(newIndex)
}
//
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData
if (oldIndex === newIndex) return
const { enterScreening, enterScreeningFromStart } = useScreening()
const _slides = JSON.parse(JSON.stringify(slides.value))
const _slide = _slides[oldIndex]
_slides.splice(oldIndex, 1)
_slides.splice(newIndex, 0, _slide)
slidesStore.setSlides(_slides)
slidesStore.updateSlideIndex(newIndex)
}
const contextmenusThumbnails = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
const { enterScreening, enterScreeningFromStart } = useScreening()
const contextmenusThumbnailItem = (): ContextmenuItem[] => {
return [
{
text: '剪切',
subText: 'Ctrl + X',
handler: cutSlide,
},
{
text: '复制',
subText: 'Ctrl + C',
handler: copySlide,
},
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{ divider: true },
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '复制页面',
subText: 'Ctrl + D',
handler: copyAndPasteSlide,
},
{
text: '删除页面',
subText: 'Delete',
handler: () => deleteSlide(),
},
{ divider: true },
{
text: '从当前放映',
subText: 'Shift + F5',
handler: enterScreening,
},
]
}
const contextmenusThumbnails = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
return {
slides,
slideIndex,
selectedSlidesIndex,
presetLayoutPopoverVisible,
slidesLoadLimit,
createSlide,
createSlideByTemplate,
setThumbnailsFocus,
handleClickSlideThumbnail,
contextmenusThumbnails,
contextmenusThumbnailItem,
fillDigit,
handleDragEnd,
}
},
})
const contextmenusThumbnailItem = (): ContextmenuItem[] => {
return [
{
text: '剪切',
subText: 'Ctrl + X',
handler: cutSlide,
},
{
text: '复制',
subText: 'Ctrl + C',
handler: copySlide,
},
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{ divider: true },
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '复制页面',
subText: 'Ctrl + D',
handler: copyAndPasteSlide,
},
{
text: '删除页面',
subText: 'Delete',
handler: () => deleteSlide(),
},
{ divider: true },
{
text: '从当前放映',
subText: 'Shift + F5',
handler: enterScreening,
},
]
}
</script>
<style lang="scss" scoped>

View File

@ -30,9 +30,9 @@
<div
class="animation-box"
:class="[
`${prefix}animated`,
`${prefix}fast`,
hoverPreviewAnimation === item.value && `${prefix}${item.value}`,
`${ANIMATION_CLASS_PREFIX}animated`,
`${ANIMATION_CLASS_PREFIX}fast`,
hoverPreviewAnimation === item.value && `${ANIMATION_CLASS_PREFIX}${item.value}`,
]"
>{{item.name}}</div>
</div>
@ -113,8 +113,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { nanoid } from 'nanoid'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
@ -157,204 +157,175 @@ interface TabItem {
const animationTypes: AnimationType[] = ['in', 'out', 'attention']
export default defineComponent({
name: 'element-animation-panel',
components: {
Draggable,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const { currentSlide, formatedAnimations, currentSlideAnimations } = storeToRefs(slidesStore)
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const { currentSlide, formatedAnimations, currentSlideAnimations } = storeToRefs(slidesStore)
const tabs: TabItem[] = [
{ key: 'in', label: '入场' },
{ key: 'out', label: '退场' },
{ key: 'attention', label: '强调' },
]
const activeTab = ref('in')
const tabs: TabItem[] = [
{ key: 'in', label: '入场' },
{ key: 'out', label: '退场' },
{ key: 'attention', label: '强调' },
]
const activeTab = ref('in')
watch(() => handleElementId.value, () => {
animationPoolVisible.value = false
})
const hoverPreviewAnimation = ref('')
const animationPoolVisible = ref(false)
const { addHistorySnapshot } = useHistorySnapshot()
//
const animationSequence = computed(() => {
const animationSequence = []
for (let i = 0; i < formatedAnimations.value.length; i++) {
const item = formatedAnimations.value[i]
for (let j = 0; j < item.animations.length; j++) {
const animation = item.animations[j]
const el = currentSlide.value.elements.find(el => el.id === animation.elId)
if (!el) continue
const elType = ELEMENT_TYPE_ZH[el.type]
const animationEffect = animationEffects[animation.effect]
animationSequence.push({
...animation,
index: j === 0 ? i + 1 : '',
elType,
animationEffect,
})
}
}
return animationSequence
})
//
const handleElementAnimation = computed(() => {
const animations = currentSlideAnimations.value
const animation = animations.filter(item => item.elId === handleElementId.value)
return animation || []
})
//
const deleteAnimation = (id: string) => {
const animations = currentSlideAnimations.value.filter(item => item.id !== id)
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData
if (oldIndex === newIndex) return
const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
const animation = animations[oldIndex]
animations.splice(oldIndex, 1)
animations.splice(newIndex, 0, animation)
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const runAnimation = (elId: string, effect: string, duration: number) => {
const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`)
if (elRef) {
const animationName = `${ANIMATION_CLASS_PREFIX}${effect}`
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
elRef.classList.add(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
const handleAnimationEnd = () => {
document.documentElement.style.removeProperty('--animate-duration')
elRef.classList.remove(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
}
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}
}
//
const updateElementAnimationDuration = (id: string, duration: number) => {
if (duration < 100 || duration > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === id) return { ...item, duration }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationTrigger = (id: string, trigger: 'click' | 'meantime' | 'auto') => {
const animations = currentSlideAnimations.value.map(item => {
if (item.id === id) return { ...item, trigger }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimation = (type: AnimationType, effect: string) => {
const animations = currentSlideAnimations.value.map(item => {
if (item.id === handleAnimationId.value) return { ...item, type, effect }
return item
})
slidesStore.updateSlide({ animations })
animationPoolVisible.value = false
addHistorySnapshot()
const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value)
const duration = animationItem?.duration || ANIMATION_DEFAULT_DURATION
runAnimation(handleElementId.value, effect, duration)
}
const handleAnimationId = ref('')
//
const addAnimation = (type: AnimationType, effect: string) => {
if (handleAnimationId.value) {
updateElementAnimation(type, effect)
return
}
const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
animations.push({
id: nanoid(10),
elId: handleElementId.value,
type,
effect,
duration: ANIMATION_DEFAULT_DURATION,
trigger: ANIMATION_DEFAULT_TRIGGER,
})
slidesStore.updateSlide({ animations })
animationPoolVisible.value = false
addHistorySnapshot()
runAnimation(handleElementId.value, effect, ANIMATION_DEFAULT_DURATION)
}
// 600ms
const popoverMaskHide = ref(false)
const handlePopoverVisibleChange = (visible: boolean) => {
if (visible) {
setTimeout(() => popoverMaskHide.value = true, 600)
}
else popoverMaskHide.value = false
}
const openAnimationPool = (elementId: string) => {
animationPoolVisible.value = true
handleAnimationId.value = elementId
handlePopoverVisibleChange(true)
}
return {
tabs,
activeTab,
handleAnimationId,
handleElement,
animationPoolVisible,
animationSequence,
hoverPreviewAnimation,
handleElementAnimation,
popoverMaskHide,
animations: {
in: ENTER_ANIMATIONS,
out: EXIT_ANIMATIONS,
attention: ATTENTION_ANIMATIONS,
},
prefix: ANIMATION_CLASS_PREFIX,
animationTypes,
addAnimation,
deleteAnimation,
handleDragEnd,
runAnimation,
updateElementAnimationDuration,
updateElementAnimationTrigger,
handlePopoverVisibleChange,
openAnimationPool,
}
},
watch(() => handleElementId.value, () => {
animationPoolVisible.value = false
})
const hoverPreviewAnimation = ref('')
const animationPoolVisible = ref(false)
const { addHistorySnapshot } = useHistorySnapshot()
//
const animationSequence = computed(() => {
const animationSequence = []
for (let i = 0; i < formatedAnimations.value.length; i++) {
const item = formatedAnimations.value[i]
for (let j = 0; j < item.animations.length; j++) {
const animation = item.animations[j]
const el = currentSlide.value.elements.find(el => el.id === animation.elId)
if (!el) continue
const elType = ELEMENT_TYPE_ZH[el.type]
const animationEffect = animationEffects[animation.effect]
animationSequence.push({
...animation,
index: j === 0 ? i + 1 : '',
elType,
animationEffect,
})
}
}
return animationSequence
})
//
const handleElementAnimation = computed(() => {
const animations = currentSlideAnimations.value
const animation = animations.filter(item => item.elId === handleElementId.value)
return animation || []
})
//
const deleteAnimation = (id: string) => {
const animations = currentSlideAnimations.value.filter(item => item.id !== id)
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData
if (oldIndex === newIndex) return
const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
const animation = animations[oldIndex]
animations.splice(oldIndex, 1)
animations.splice(newIndex, 0, animation)
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const runAnimation = (elId: string, effect: string, duration: number) => {
const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`)
if (elRef) {
const animationName = `${ANIMATION_CLASS_PREFIX}${effect}`
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
elRef.classList.add(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
const handleAnimationEnd = () => {
document.documentElement.style.removeProperty('--animate-duration')
elRef.classList.remove(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
}
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}
}
//
const updateElementAnimationDuration = (id: string, duration: number) => {
if (duration < 100 || duration > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === id) return { ...item, duration }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationTrigger = (id: string, trigger: 'click' | 'meantime' | 'auto') => {
const animations = currentSlideAnimations.value.map(item => {
if (item.id === id) return { ...item, trigger }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimation = (type: AnimationType, effect: string) => {
const animations = currentSlideAnimations.value.map(item => {
if (item.id === handleAnimationId.value) return { ...item, type, effect }
return item
})
slidesStore.updateSlide({ animations })
animationPoolVisible.value = false
addHistorySnapshot()
const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value)
const duration = animationItem?.duration || ANIMATION_DEFAULT_DURATION
runAnimation(handleElementId.value, effect, duration)
}
const handleAnimationId = ref('')
//
const addAnimation = (type: AnimationType, effect: string) => {
if (handleAnimationId.value) {
updateElementAnimation(type, effect)
return
}
const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
animations.push({
id: nanoid(10),
elId: handleElementId.value,
type,
effect,
duration: ANIMATION_DEFAULT_DURATION,
trigger: ANIMATION_DEFAULT_TRIGGER,
})
slidesStore.updateSlide({ animations })
animationPoolVisible.value = false
addHistorySnapshot()
runAnimation(handleElementId.value, effect, ANIMATION_DEFAULT_DURATION)
}
// 600ms
const popoverMaskHide = ref(false)
const handlePopoverVisibleChange = (visible: boolean) => {
if (visible) {
setTimeout(() => popoverMaskHide.value = true, 600)
}
else popoverMaskHide.value = false
}
const openAnimationPool = (elementId: string) => {
animationPoolVisible.value = true
handleAnimationId.value = elementId
handlePopoverVisibleChange(true)
}
const animations = {
in: ENTER_ANIMATIONS,
out: EXIT_ANIMATIONS,
attention: ATTENTION_ANIMATIONS,
}
</script>
<style lang="scss" scoped>

View File

@ -2,12 +2,12 @@
<div class="element-positopn-panel">
<div class="title">层级</div>
<ButtonGroup class="row">
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.TOP)"><IconSendToBack class="btn-icon" /> 置于顶层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.BOTTOM)"><IconBringToFrontOne class="btn-icon" /> 置于底层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.TOP)"><IconSendToBack class="btn-icon" /> 置于顶层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.BOTTOM)"><IconBringToFrontOne class="btn-icon" /> 置于底层</Button>
</ButtonGroup>
<ButtonGroup class="row">
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.UP)"><IconBringToFront class="btn-icon" /> 上移一层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.DOWN)"><IconSentToBack class="btn-icon" /> 下移一层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.UP)"><IconBringToFront class="btn-icon" /> 上移一层</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.DOWN)"><IconSentToBack class="btn-icon" /> 下移一层</Button>
</ButtonGroup>
<Divider />
@ -61,7 +61,7 @@
<div style="flex: 4;" class="label">Y</div>
</div>
<template v-if="handleElement.type !== 'line'">
<template v-if="handleElement!.type !== 'line'">
<div class="row">
<div style="flex: 3;">大小</div>
<InputNumber
@ -72,7 +72,7 @@
@change="value => updateWidth(value as number)"
style="flex: 4;"
/>
<template v-if="['image', 'shape', 'audio'].includes(handleElement.type)">
<template v-if="['image', 'shape', 'audio'].includes(handleElement!.type)">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="解除宽高比锁定" v-if="fixedRatio">
<IconLock style="flex: 1;" class="icon-btn" @click="updateFixedRatio(false)" />
</Tooltip>
@ -85,7 +85,7 @@
:min="minSize"
:max="800"
:step="5"
:disabled="handleElement.type === 'text'"
:disabled="handleElement!.type === 'text'"
:value="height"
@change="value => updateHeight(value as number)"
style="flex: 4;"
@ -99,7 +99,7 @@
</div>
</template>
<template v-if="!['line', 'video', 'audio'].includes(handleElement.type)">
<template v-if="!['line', 'video', 'audio'].includes(handleElement!.type)">
<Divider />
<div class="row">
@ -131,129 +131,101 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, Ref, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { round } from 'lodash'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElement } from '@/types/slides'
import { ElementAlignCommands, ElementOrderCommands } from '@/types/edit'
import { MIN_SIZE } from '@/configs/element'
import useOrderElement from '@/hooks/useOrderElement'
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default defineComponent({
name: 'element-positopn-panel',
setup() {
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const left = ref(0)
const top = ref(0)
const width = ref(0)
const height = ref(0)
const rotate = ref(0)
const fixedRatio = ref(false)
const left = ref(0)
const top = ref(0)
const width = ref(0)
const height = ref(0)
const rotate = ref(0)
const fixedRatio = ref(false)
const minSize = computed(() => {
if (!handleElement.value) return 20
return MIN_SIZE[handleElement.value.type] || 20
})
watch(handleElement, () => {
if (!handleElement.value) return
left.value = round(handleElement.value.left, 1)
top.value = round(handleElement.value.top, 1)
fixedRatio.value = 'fixedRatio' in handleElement.value && !!handleElement.value.fixedRatio
if (handleElement.value.type !== 'line') {
width.value = round(handleElement.value.width, 1)
height.value = round(handleElement.value.height, 1)
rotate.value = 'rotate' in handleElement.value && handleElement.value.rotate !== undefined ? round(handleElement.value.rotate, 1) : 0
}
}, { deep: true, immediate: true })
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateLeft = (value: number) => {
const props = { left: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateTop = (value: number) => {
const props = { top: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateWidth = (value: number) => {
const props = { width: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateHeight = (value: number) => {
const props = { height: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateRotate = (value: number) => {
const props = { rotate: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateFixedRatio = (value: boolean) => {
const props = { fixedRatio: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
// 45
const updateRotate45 = (command: '+' | '-') => {
let _rotate = Math.floor(rotate.value / 45) * 45
if (command === '+') _rotate = _rotate + 45
else if (command === '-') _rotate = _rotate - 45
if (_rotate < -180) _rotate = -180
if (_rotate > 180) _rotate = 180
const props = { rotate: _rotate }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
return {
handleElement: handleElement as Ref<PPTElement>,
orderElement,
alignElementToCanvas,
left,
top,
width,
height,
rotate,
fixedRatio,
minSize,
updateLeft,
updateTop,
updateWidth,
updateHeight,
updateRotate,
updateFixedRatio,
updateRotate45,
ElementOrderCommands,
ElementAlignCommands,
}
},
const minSize = computed(() => {
if (!handleElement.value) return 20
return MIN_SIZE[handleElement.value.type] || 20
})
watch(handleElement, () => {
if (!handleElement.value) return
left.value = round(handleElement.value.left, 1)
top.value = round(handleElement.value.top, 1)
fixedRatio.value = 'fixedRatio' in handleElement.value && !!handleElement.value.fixedRatio
if (handleElement.value.type !== 'line') {
width.value = round(handleElement.value.width, 1)
height.value = round(handleElement.value.height, 1)
rotate.value = 'rotate' in handleElement.value && handleElement.value.rotate !== undefined ? round(handleElement.value.rotate, 1) : 0
}
}, { deep: true, immediate: true })
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateLeft = (value: number) => {
const props = { left: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateTop = (value: number) => {
const props = { top: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateWidth = (value: number) => {
const props = { width: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateHeight = (value: number) => {
const props = { height: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateRotate = (value: number) => {
const props = { rotate: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateFixedRatio = (value: boolean) => {
const props = { fixedRatio: value }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
// 45
const updateRotate45 = (command: '+' | '-') => {
let _rotate = Math.floor(rotate.value / 45) * 45
if (command === '+') _rotate = _rotate + 45
else if (command === '-') _rotate = _rotate - 45
if (_rotate < -180) _rotate = -180
if (_rotate > 180) _rotate = 180
const props = { rotate: _rotate }
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -5,11 +5,11 @@
<Popover trigger="click">
<template #content>
<ColorPicker
:modelValue="handleElement.color"
:modelValue="handleAudioElement.color"
@update:modelValue="value => updateAudio({ color: value })"
/>
</template>
<ColorButton :color="handleElement.color" style="flex: 3;" />
<ColorButton :color="handleAudioElement.color" style="flex: 3;" />
</Popover>
</div>
@ -17,7 +17,7 @@
<div style="flex: 2;">自动播放</div>
<div class="switch-wrapper" style="flex: 3;">
<Switch
:checked="handleElement.autoplay"
:checked="handleAudioElement.autoplay"
@change="checked => updateAudio({ autoplay: checked as boolean })"
/>
</div>
@ -27,7 +27,7 @@
<div style="flex: 2;">循环播放</div>
<div class="switch-wrapper" style="flex: 3;">
<Switch
:checked="handleElement.loop"
:checked="handleAudioElement.loop"
@change="checked => updateAudio({ loop: checked as boolean })"
/>
</div>
@ -35,8 +35,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, Ref } from 'vue'
<script lang="ts" setup>
import { Ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTAudioElement } from '@/types/slides'
@ -44,29 +44,18 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from '../common/ColorButton.vue'
export default defineComponent({
name: 'audio-style-panel',
components: {
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const handleAudioElement = handleElement as Ref<PPTAudioElement>
const updateAudio = (props: Partial<PPTAudioElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const { addHistorySnapshot } = useHistorySnapshot()
return {
handleElement: handleElement as Ref<PPTAudioElement>,
updateAudio,
}
}
})
const updateAudio = (props: Partial<PPTAudioElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -54,246 +54,233 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, PropType, ref } from 'vue'
import { ChartData } from '@/types/slides'
import { KEYS } from '@/configs/hotkey'
import { pasteCustomClipboardString, pasteExcelClipboardString } from '@/utils/clipboard'
const props = defineProps({
data: {
type: Object as PropType<ChartData>,
required: true,
}
})
const emit = defineEmits<{
(event: 'save', payload: ChartData): void
(event: 'close'): void
}>()
const CELL_WIDTH = 100
const CELL_HEIGHT = 32
export default defineComponent({
name: 'chart-data-editor',
emits: ['save', 'close'],
props: {
data: {
type: Object as PropType<ChartData>,
required: true,
}
},
setup(props, { emit }) {
const selectedRange = ref([0, 0])
const tempRangeSize = ref({ width: 0, height: 0 })
const focusCell = ref<[number, number] | null>(null)
const selectedRange = ref([0, 0])
const tempRangeSize = ref({ width: 0, height: 0 })
const focusCell = ref<[number, number] | null>(null)
// 线
const rangeLines = computed(() => {
const width = selectedRange.value[0] * CELL_WIDTH
const height = selectedRange.value[1] * CELL_HEIGHT
return [
{ type: 't', style: {width: width + 'px'} },
{ type: 'b', style: {top: height + 'px', width: width + 'px'} },
{ type: 'l', style: {height: height + 'px'} },
{ type: 'r', style: {left: width + 'px', height: height + 'px'} },
]
})
//
const resizablePointStyle = computed(() => {
const width = selectedRange.value[0] * CELL_WIDTH
const height = selectedRange.value[1] * CELL_HEIGHT
return { left: width + 'px', top: height + 'px' }
})
// DOM
const initData = () => {
const _data: string[][] = []
const { labels, legends, series } = props.data
const rowCount = labels.length
const colCount = series.length
_data.push(['', ...legends])
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = [labels[rowIndex]]
for (let colIndex = 0; colIndex < colCount; colIndex++) {
row.push(series[colIndex][rowIndex] + '')
}
_data.push(row)
}
for (let rowIndex = 0; rowIndex < rowCount + 1; rowIndex++) {
for (let colIndex = 0; colIndex < colCount + 1; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = _data[rowIndex][colIndex] + ''
}
}
selectedRange.value = [colCount + 1, rowCount + 1]
}
onMounted(initData)
//
const moveNextRow = () => {
if (!focusCell.value) return
const [rowIndex, colIndex] = focusCell.value
const inputRef = document.querySelector(`#cell-${rowIndex + 1}-${colIndex}`) as HTMLInputElement
inputRef && inputRef.focus()
}
const keyboardListener = (e: KeyboardEvent) => {
const key = e.key.toUpperCase()
if (key === KEYS.ENTER) moveNextRow()
}
onMounted(() => {
document.addEventListener('keydown', keyboardListener)
})
onUnmounted(() => {
document.removeEventListener('keydown', keyboardListener)
})
// DOM
const getTableData = () => {
const [col, row] = selectedRange.value
const labels: string[] = []
const legends: string[] = []
const series: number[][] = []
//
for (let rowIndex = 1; rowIndex < row; rowIndex++) {
let labelsItem = `类别${rowIndex}`
const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
labels.push(labelsItem)
}
for (let colIndex = 1; colIndex < col; colIndex++) {
let legendsItem = `系列${colIndex}`
const labelInputRef = document.querySelector(`#cell-0-${colIndex}`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) legendsItem = labelInputRef.value
legends.push(legendsItem)
}
for (let colIndex = 1; colIndex < col; colIndex++) {
const seriesItem = []
for (let rowIndex = 1; rowIndex < row; rowIndex++) {
const valueInputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
let value = 0
if (valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) {
value = +valueInputRef.value
}
seriesItem.push(value)
}
series.push(seriesItem)
}
emit('save', { labels, legends, series })
}
//
const clear = () => {
for (let rowIndex = 1; rowIndex < 31; rowIndex++) {
for (let colIndex = 1; colIndex < 7; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = ''
}
}
}
//
const handlePaste = (e: ClipboardEvent, rowIndex: number, colIndex: number) => {
e.preventDefault()
if (!e.clipboardData) return
const clipboardDataFirstItem = e.clipboardData.items[0]
if (clipboardDataFirstItem && clipboardDataFirstItem.kind === 'string' && clipboardDataFirstItem.type === 'text/plain') {
clipboardDataFirstItem.getAsString(text => {
const clipboardData = pasteCustomClipboardString(text)
if (typeof clipboardData === 'object') return
const excelData = pasteExcelClipboardString(text)
if (excelData) {
const maxRow = rowIndex + excelData.length
const maxCol = colIndex + excelData[0].length
for (let i = rowIndex; i < maxRow; i++) {
for (let j = colIndex; j < maxCol; j++) {
const inputRef = document.querySelector(`#cell-${i}-${j}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = excelData[i - rowIndex][j - colIndex]
}
}
}
})
}
}
//
const closeEditor = () => emit('close')
//
const changeSelectRange = (e: MouseEvent) => {
let isMouseDown = true
const startPageX = e.pageX
const startPageY = e.pageY
const originWidth = selectedRange.value[0] * CELL_WIDTH
const originHeight = selectedRange.value[1] * CELL_HEIGHT
document.onmousemove = e => {
if (!isMouseDown) return
const currentPageX = e.pageX
const currentPageY = e.pageY
const x = currentPageX - startPageX
const y = currentPageY - startPageY
const width = originWidth + x
const height = originHeight + y
tempRangeSize.value = { width, height }
}
document.onmouseup = e => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
const endPageX = e.pageX
const endPageY = e.pageY
if (startPageX === endPageX && startPageY === endPageY) return
//
let width = tempRangeSize.value.width
let height = tempRangeSize.value.height
if (width % CELL_WIDTH > CELL_WIDTH * 0.5) width = width + (CELL_WIDTH - width % CELL_WIDTH)
if (height % CELL_HEIGHT > CELL_HEIGHT * 0.5) height = height + (CELL_HEIGHT - height % CELL_HEIGHT)
let row = Math.round(height / CELL_HEIGHT)
let col = Math.round(width / CELL_WIDTH)
if (row < 3) row = 3
if (col < 2) col = 2
selectedRange.value = [col, row]
tempRangeSize.value = { width: 0, height: 0 }
}
}
return {
tempRangeSize,
rangeLines,
resizablePointStyle,
selectedRange,
focusCell,
changeSelectRange,
getTableData,
closeEditor,
clear,
handlePaste,
}
},
// 线
const rangeLines = computed(() => {
const width = selectedRange.value[0] * CELL_WIDTH
const height = selectedRange.value[1] * CELL_HEIGHT
return [
{ type: 't', style: {width: width + 'px'} },
{ type: 'b', style: {top: height + 'px', width: width + 'px'} },
{ type: 'l', style: {height: height + 'px'} },
{ type: 'r', style: {left: width + 'px', height: height + 'px'} },
]
})
//
const resizablePointStyle = computed(() => {
const width = selectedRange.value[0] * CELL_WIDTH
const height = selectedRange.value[1] * CELL_HEIGHT
return { left: width + 'px', top: height + 'px' }
})
// DOM
const initData = () => {
const _data: string[][] = []
const { labels, legends, series } = props.data
const rowCount = labels.length
const colCount = series.length
_data.push(['', ...legends])
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = [labels[rowIndex]]
for (let colIndex = 0; colIndex < colCount; colIndex++) {
row.push(series[colIndex][rowIndex] + '')
}
_data.push(row)
}
for (let rowIndex = 0; rowIndex < rowCount + 1; rowIndex++) {
for (let colIndex = 0; colIndex < colCount + 1; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = _data[rowIndex][colIndex] + ''
}
}
selectedRange.value = [colCount + 1, rowCount + 1]
}
onMounted(initData)
//
const moveNextRow = () => {
if (!focusCell.value) return
const [rowIndex, colIndex] = focusCell.value
const inputRef = document.querySelector(`#cell-${rowIndex + 1}-${colIndex}`) as HTMLInputElement
inputRef && inputRef.focus()
}
const keyboardListener = (e: KeyboardEvent) => {
const key = e.key.toUpperCase()
if (key === KEYS.ENTER) moveNextRow()
}
onMounted(() => {
document.addEventListener('keydown', keyboardListener)
})
onUnmounted(() => {
document.removeEventListener('keydown', keyboardListener)
})
// DOM
const getTableData = () => {
const [col, row] = selectedRange.value
const labels: string[] = []
const legends: string[] = []
const series: number[][] = []
//
for (let rowIndex = 1; rowIndex < row; rowIndex++) {
let labelsItem = `类别${rowIndex}`
const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
labels.push(labelsItem)
}
for (let colIndex = 1; colIndex < col; colIndex++) {
let legendsItem = `系列${colIndex}`
const labelInputRef = document.querySelector(`#cell-0-${colIndex}`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) legendsItem = labelInputRef.value
legends.push(legendsItem)
}
for (let colIndex = 1; colIndex < col; colIndex++) {
const seriesItem = []
for (let rowIndex = 1; rowIndex < row; rowIndex++) {
const valueInputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
let value = 0
if (valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) {
value = +valueInputRef.value
}
seriesItem.push(value)
}
series.push(seriesItem)
}
emit('save', { labels, legends, series })
}
//
const clear = () => {
for (let rowIndex = 1; rowIndex < 31; rowIndex++) {
for (let colIndex = 1; colIndex < 7; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = ''
}
}
}
//
const handlePaste = (e: ClipboardEvent, rowIndex: number, colIndex: number) => {
e.preventDefault()
if (!e.clipboardData) return
const clipboardDataFirstItem = e.clipboardData.items[0]
if (clipboardDataFirstItem && clipboardDataFirstItem.kind === 'string' && clipboardDataFirstItem.type === 'text/plain') {
clipboardDataFirstItem.getAsString(text => {
const clipboardData = pasteCustomClipboardString(text)
if (typeof clipboardData === 'object') return
const excelData = pasteExcelClipboardString(text)
if (excelData) {
const maxRow = rowIndex + excelData.length
const maxCol = colIndex + excelData[0].length
for (let i = rowIndex; i < maxRow; i++) {
for (let j = colIndex; j < maxCol; j++) {
const inputRef = document.querySelector(`#cell-${i}-${j}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = excelData[i - rowIndex][j - colIndex]
}
}
}
})
}
}
//
const closeEditor = () => emit('close')
//
const changeSelectRange = (e: MouseEvent) => {
let isMouseDown = true
const startPageX = e.pageX
const startPageY = e.pageY
const originWidth = selectedRange.value[0] * CELL_WIDTH
const originHeight = selectedRange.value[1] * CELL_HEIGHT
document.onmousemove = e => {
if (!isMouseDown) return
const currentPageX = e.pageX
const currentPageY = e.pageY
const x = currentPageX - startPageX
const y = currentPageY - startPageY
const width = originWidth + x
const height = originHeight + y
tempRangeSize.value = { width, height }
}
document.onmouseup = e => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
const endPageX = e.pageX
const endPageY = e.pageY
if (startPageX === endPageX && startPageY === endPageY) return
//
let width = tempRangeSize.value.width
let height = tempRangeSize.value.height
if (width % CELL_WIDTH > CELL_WIDTH * 0.5) width = width + (CELL_WIDTH - width % CELL_WIDTH)
if (height % CELL_HEIGHT > CELL_HEIGHT * 0.5) height = height + (CELL_HEIGHT - height % CELL_HEIGHT)
let row = Math.round(height / CELL_HEIGHT)
let col = Math.round(width / CELL_WIDTH)
if (row < 3) row = 3
if (col < 2) col = 2
selectedRange.value = [col, row]
tempRangeSize.value = { width: 0, height: 0 }
}
}
</script>
<style lang="scss" scoped>

View File

@ -6,7 +6,7 @@
<Divider />
<template v-if="handleElement.chartType === 'line'">
<template v-if="handleChartElement.chartType === 'line'">
<div class="row">
<Checkbox
@change="e => updateOptions({ showArea: e.target.checked })"
@ -26,7 +26,7 @@
>使用平滑曲线</Checkbox>
</div>
</template>
<div class="row" v-if="handleElement.chartType === 'bar'">
<div class="row" v-if="handleChartElement.chartType === 'bar'">
<Checkbox
@change="e => updateOptions({ horizontalBars: e.target.checked })"
:checked="horizontalBars"
@ -36,7 +36,7 @@
:checked="stackBars"
>堆叠样式</Checkbox>
</div>
<div class="row" v-if="handleElement.chartType === 'pie'">
<div class="row" v-if="handleChartElement.chartType === 'pie'">
<Checkbox
@change="e => updateOptions({ donut: e.target.checked })"
:checked="donut"
@ -143,7 +143,7 @@
destroyOnClose
>
<ChartDataEditor
:data="handleElement.data"
:data="handleChartElement.data"
@close="chartDataEditorVisible = false"
@save="value => updateData(value)"
/>
@ -151,8 +151,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, Ref, ref, watch } from 'vue'
<script lang="ts" setup>
import { onUnmounted, Ref, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { ChartData, ChartOptions, PPTChartElement } from '@/types/slides'
@ -178,165 +178,132 @@ const presetChartThemes = [
['#8a7ca8', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'],
]
export default defineComponent({
name: 'chart-style-panel',
components: {
ElementOutline,
ChartDataEditor,
ColorButton,
},
setup() {
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(mainStore)
const { theme } = storeToRefs(slidesStore)
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(mainStore)
const { theme } = storeToRefs(slidesStore)
const chartDataEditorVisible = ref(false)
const presetThemesVisible = ref(false)
const presetThemeColorHoverIndex = ref<[number, number]>([-1, -1])
const handleChartElement = handleElement as Ref<PPTChartElement>
const { addHistorySnapshot } = useHistorySnapshot()
const chartDataEditorVisible = ref(false)
const presetThemesVisible = ref(false)
const presetThemeColorHoverIndex = ref<[number, number]>([-1, -1])
const fill = ref<string>('#000')
const { addHistorySnapshot } = useHistorySnapshot()
const themeColor = ref<string[]>([])
const gridColor = ref('')
const legend = ref('')
const fill = ref<string>('#000')
const lineSmooth = ref(true)
const showLine = ref(true)
const showArea = ref(false)
const horizontalBars = ref(false)
const donut = ref(false)
const stackBars = ref(false)
const themeColor = ref<string[]>([])
const gridColor = ref('')
const legend = ref('')
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'chart') return
fill.value = handleElement.value.fill || '#fff'
const lineSmooth = ref(true)
const showLine = ref(true)
const showArea = ref(false)
const horizontalBars = ref(false)
const donut = ref(false)
const stackBars = ref(false)
if (handleElement.value.options) {
const {
lineSmooth: _lineSmooth,
showLine: _showLine,
showArea: _showArea,
horizontalBars: _horizontalBars,
donut: _donut,
stackBars: _stackBars,
} = handleElement.value.options
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'chart') return
fill.value = handleElement.value.fill || '#fff'
if (_lineSmooth !== undefined) lineSmooth.value = _lineSmooth as boolean
if (_showLine !== undefined) showLine.value = _showLine
if (_showArea !== undefined) showArea.value = _showArea
if (_horizontalBars !== undefined) horizontalBars.value = _horizontalBars
if (_donut !== undefined) donut.value = _donut
if (_stackBars !== undefined) stackBars.value = _stackBars
}
if (handleElement.value.options) {
const {
lineSmooth: _lineSmooth,
showLine: _showLine,
showArea: _showArea,
horizontalBars: _horizontalBars,
donut: _donut,
stackBars: _stackBars,
} = handleElement.value.options
themeColor.value = handleElement.value.themeColor
gridColor.value = handleElement.value.gridColor || '#333'
legend.value = handleElement.value.legend || ''
}, { deep: true, immediate: true })
if (_lineSmooth !== undefined) lineSmooth.value = _lineSmooth as boolean
if (_showLine !== undefined) showLine.value = _showLine
if (_showArea !== undefined) showArea.value = _showArea
if (_horizontalBars !== undefined) horizontalBars.value = _horizontalBars
if (_donut !== undefined) donut.value = _donut
if (_stackBars !== undefined) stackBars.value = _stackBars
}
const updateElement = (props: Partial<PPTChartElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
themeColor.value = handleElement.value.themeColor
gridColor.value = handleElement.value.gridColor || '#333'
legend.value = handleElement.value.legend || ''
}, { deep: true, immediate: true })
//
const updateData = (data: ChartData) => {
chartDataEditorVisible.value = false
updateElement({ data })
}
const updateElement = (props: Partial<PPTChartElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
//
const updateData = (data: ChartData) => {
chartDataEditorVisible.value = false
updateElement({ data })
}
// 线线线线
const updateOptions = (optionProps: ChartOptions) => {
const _handleElement = handleElement.value as PPTChartElement
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
const newOptions = { ..._handleElement.options, ...optionProps }
updateElement({ options: newOptions })
}
// 线线线线
const updateOptions = (optionProps: ChartOptions) => {
const _handleElement = handleElement.value as PPTChartElement
//
const updateTheme = (color: string, index: number) => {
const props = {
themeColor: themeColor.value.map((c, i) => i === index ? color : c),
}
updateElement(props)
}
const newOptions = { ..._handleElement.options, ...optionProps }
updateElement({ options: newOptions })
}
//
const addThemeColor = () => {
const props = {
themeColor: [...themeColor.value, theme.value.themeColor],
}
updateElement(props)
}
//
const updateTheme = (color: string, index: number) => {
const props = {
themeColor: themeColor.value.map((c, i) => i === index ? color : c),
}
updateElement(props)
}
// 使
const applyPresetTheme = (colors: string[], index: number) => {
const themeColor = colors.slice(0, index + 1)
updateElement({ themeColor })
presetThemesVisible.value = false
}
//
const addThemeColor = () => {
const props = {
themeColor: [...themeColor.value, theme.value.themeColor],
}
updateElement(props)
}
//
const deleteThemeColor = (index: number) => {
const props = {
themeColor: themeColor.value.filter((c, i) => i !== index),
}
updateElement(props)
}
// 使
const applyPresetTheme = (colors: string[], index: number) => {
const themeColor = colors.slice(0, index + 1)
updateElement({ themeColor })
presetThemesVisible.value = false
}
//
const updateGridColor = (gridColor: string) => {
updateElement({ gridColor })
}
//
const deleteThemeColor = (index: number) => {
const props = {
themeColor: themeColor.value.filter((c, i) => i !== index),
}
updateElement(props)
}
// /
const updateLegend = (legend: '' | 'top' | 'bottom') => {
updateElement({ legend })
}
//
const updateGridColor = (gridColor: string) => {
updateElement({ gridColor })
}
const openDataEditor = () => chartDataEditorVisible.value = true
// /
const updateLegend = (legend: '' | 'top' | 'bottom') => {
updateElement({ legend })
}
emitter.on(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
onUnmounted(() => {
emitter.off(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
})
const openDataEditor = () => chartDataEditorVisible.value = true
return {
chartDataEditorVisible,
presetThemesVisible,
presetThemeColorHoverIndex,
handleElement: handleElement as Ref<PPTChartElement>,
updateData,
fill,
updateFill,
lineSmooth,
showLine,
showArea,
horizontalBars,
donut,
stackBars,
updateOptions,
themeColor,
gridColor,
legend,
updateTheme,
addThemeColor,
deleteThemeColor,
updateGridColor,
updateLegend,
presetChartThemes,
applyPresetTheme,
}
},
emitter.on(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
onUnmounted(() => {
emitter.off(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
})
// handleElement: handleElement as Ref<PPTChartElement>,
</script>
<style lang="scss" scoped>

View File

@ -2,7 +2,7 @@
<div class="image-style-panel">
<div
class="origin-image"
:style="{ backgroundImage: `url(${handleElement.src})` }"
:style="{ backgroundImage: `url(${handleImageElement.src})` }"
></div>
<ElementFlip />
@ -57,8 +57,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, Ref, ref } from 'vue'
<script lang="ts" setup>
import { Ref, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTImageElement, SlideBackground } from '@/types/slides'
@ -105,175 +105,154 @@ const ratioClipOptions = [
},
]
export default defineComponent({
name: 'image-style-panel',
components: {
ElementOutline,
ElementShadow,
ElementFlip,
ElementFilter,
},
setup() {
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(mainStore)
const { currentSlide } = storeToRefs(slidesStore)
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(mainStore)
const { currentSlide } = storeToRefs(slidesStore)
const clipPanelVisible = ref(false)
const handleImageElement = handleElement as Ref<PPTImageElement>
const { addHistorySnapshot } = useHistorySnapshot()
const clipPanelVisible = ref(false)
//
const clipImage = () => {
mainStore.setClipingImageElementId(handleElementId.value)
clipPanelVisible.value = false
const { addHistorySnapshot } = useHistorySnapshot()
//
const clipImage = () => {
mainStore.setClipingImageElementId(handleElementId.value)
clipPanelVisible.value = false
}
//
const getImageElementDataBeforeClip = () => {
const _handleElement = handleElement.value as PPTImageElement
//
const imgWidth = _handleElement.width
const imgHeight = _handleElement.height
const imgLeft = _handleElement.left
const imgTop = _handleElement.top
const originClipRange: [[number, number], [number, number]] = _handleElement.clip ? _handleElement.clip.range : [[0, 0], [100, 100]]
const originWidth = imgWidth / ((originClipRange[1][0] - originClipRange[0][0]) / 100)
const originHeight = imgHeight / ((originClipRange[1][1] - originClipRange[0][1]) / 100)
const originLeft = imgLeft - originWidth * (originClipRange[0][0] / 100)
const originTop = imgTop - originHeight * (originClipRange[0][1] / 100)
return {
originClipRange,
originWidth,
originHeight,
originLeft,
originTop,
}
}
//
const presetImageClip = (shape: string, ratio = 0) => {
const _handleElement = handleElement.value as PPTImageElement
const {
originClipRange,
originWidth,
originHeight,
originLeft,
originTop,
} = getImageElementDataBeforeClip()
//
if (ratio) {
const imageRatio = originHeight / originWidth
const min = 0
const max = 100
let range: [[number, number], [number, number]]
if (imageRatio > ratio) {
const distance = ((1 - ratio / imageRatio) / 2) * 100
range = [[min, distance], [max, max - distance]]
}
//
const getImageElementDataBeforeClip = () => {
const _handleElement = handleElement.value as PPTImageElement
//
const imgWidth = _handleElement.width
const imgHeight = _handleElement.height
const imgLeft = _handleElement.left
const imgTop = _handleElement.top
const originClipRange: [[number, number], [number, number]] = _handleElement.clip ? _handleElement.clip.range : [[0, 0], [100, 100]]
const originWidth = imgWidth / ((originClipRange[1][0] - originClipRange[0][0]) / 100)
const originHeight = imgHeight / ((originClipRange[1][1] - originClipRange[0][1]) / 100)
const originLeft = imgLeft - originWidth * (originClipRange[0][0] / 100)
const originTop = imgTop - originHeight * (originClipRange[0][1] / 100)
return {
originClipRange,
originWidth,
originHeight,
originLeft,
originTop,
}
else {
const distance = ((1 - imageRatio / ratio) / 2) * 100
range = [[distance, min], [max - distance, max]]
}
slidesStore.updateElement({
id: handleElementId.value,
props: {
clip: { ..._handleElement.clip, shape, range },
left: originLeft + originWidth * (range[0][0] / 100),
top: originTop + originHeight * (range[0][1] / 100),
width: originWidth * (range[1][0] - range[0][0]) / 100,
height: originHeight * (range[1][1] - range[0][1]) / 100,
},
})
}
//
else {
slidesStore.updateElement({
id: handleElementId.value,
props: {
clip: { ..._handleElement.clip, shape, range: originClipRange }
},
})
}
clipImage()
addHistorySnapshot()
}
//
const presetImageClip = (shape: string, ratio = 0) => {
const _handleElement = handleElement.value as PPTImageElement
//
const replaceImage = (files: FileList) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => {
const props = { src: dataURL }
slidesStore.updateElement({ id: handleElementId.value, props })
})
addHistorySnapshot()
}
const {
originClipRange,
originWidth,
originHeight,
originLeft,
originTop,
} = getImageElementDataBeforeClip()
//
if (ratio) {
const imageRatio = originHeight / originWidth
//
const resetImage = () => {
const _handleElement = handleElement.value as PPTImageElement
const min = 0
const max = 100
let range: [[number, number], [number, number]]
if (_handleElement.clip) {
const {
originWidth,
originHeight,
originLeft,
originTop,
} = getImageElementDataBeforeClip()
if (imageRatio > ratio) {
const distance = ((1 - ratio / imageRatio) / 2) * 100
range = [[min, distance], [max, max - distance]]
}
else {
const distance = ((1 - imageRatio / ratio) / 2) * 100
range = [[distance, min], [max - distance, max]]
}
slidesStore.updateElement({
id: handleElementId.value,
props: {
clip: { ..._handleElement.clip, shape, range },
left: originLeft + originWidth * (range[0][0] / 100),
top: originTop + originHeight * (range[0][1] / 100),
width: originWidth * (range[1][0] - range[0][0]) / 100,
height: originHeight * (range[1][1] - range[0][1]) / 100,
},
})
}
//
else {
slidesStore.updateElement({
id: handleElementId.value,
props: {
clip: { ..._handleElement.clip, shape, range: originClipRange }
},
})
}
clipImage()
addHistorySnapshot()
}
slidesStore.updateElement({
id: handleElementId.value,
props: {
left: originLeft,
top: originTop,
width: originWidth,
height: originHeight,
},
})
}
//
const replaceImage = (files: File[]) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => {
const props = { src: dataURL }
slidesStore.updateElement({ id: handleElementId.value, props })
})
addHistorySnapshot()
}
slidesStore.removeElementProps({
id: handleElementId.value,
propName: ['clip', 'outline', 'flip', 'shadow', 'filters'],
})
addHistorySnapshot()
}
//
const resetImage = () => {
const _handleElement = handleElement.value as PPTImageElement
//
const setBackgroundImage = () => {
const _handleElement = handleElement.value as PPTImageElement
if (_handleElement.clip) {
const {
originWidth,
originHeight,
originLeft,
originTop,
} = getImageElementDataBeforeClip()
slidesStore.updateElement({
id: handleElementId.value,
props: {
left: originLeft,
top: originTop,
width: originWidth,
height: originHeight,
},
})
}
slidesStore.removeElementProps({
id: handleElementId.value,
propName: ['clip', 'outline', 'flip', 'shadow', 'filters'],
})
addHistorySnapshot()
}
//
const setBackgroundImage = () => {
const _handleElement = handleElement.value as PPTImageElement
const background: SlideBackground = {
...currentSlide.value.background,
type: 'image',
image: _handleElement.src,
imageSize: 'cover',
}
slidesStore.updateSlide({ background })
addHistorySnapshot()
}
return {
clipPanelVisible,
shapeClipPathOptions,
ratioClipOptions,
handleElement: handleElement as Ref<PPTImageElement>,
clipImage,
presetImageClip,
replaceImage,
resetImage,
setBackgroundImage,
}
},
})
const background: SlideBackground = {
...currentSlide.value.background,
type: 'image',
image: _handleElement.src,
imageSize: 'cover',
}
slidesStore.updateSlide({ background })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -9,11 +9,11 @@
<Popover trigger="click">
<template #content>
<ColorPicker
:modelValue="handleElement.color"
:modelValue="handleLatexElement.color"
@update:modelValue="value => updateLatex({ color: value })"
/>
</template>
<ColorButton :color="handleElement.color" style="flex: 3;" />
<ColorButton :color="handleLatexElement.color" style="flex: 3;" />
</Popover>
</div>
<div class="row">
@ -21,7 +21,7 @@
<InputNumber
:min="1"
:max="3"
:value="handleElement.strokeWidth"
:value="handleLatexElement.strokeWidth"
@change="value => updateLatex({ strokeWidth: value as number })"
style="flex: 3;"
/>
@ -35,7 +35,7 @@
destroyOnClose
>
<LaTeXEditor
:value="handleElement.latex"
:value="handleLatexElement.latex"
@close="latexEditorVisible = false"
@update="data => { updateLatexData(data); latexEditorVisible = false }"
/>
@ -43,8 +43,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, Ref, ref } from 'vue'
<script lang="ts" setup>
import { onUnmounted, Ref, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTLatexElement } from '@/types/slides'
@ -54,50 +54,36 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from '../common/ColorButton.vue'
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
export default defineComponent({
name: 'latex-style-panel',
components: {
ColorButton,
LaTeXEditor,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const latexEditorVisible = ref(false)
const handleLatexElement = handleElement as Ref<PPTLatexElement>
const { addHistorySnapshot } = useHistorySnapshot()
const latexEditorVisible = ref(false)
const updateLatex = (props: Partial<PPTLatexElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const { addHistorySnapshot } = useHistorySnapshot()
const updateLatexData = (data: { path: string; latex: string; w: number; h: number; }) => {
updateLatex({
path: data.path,
latex: data.latex,
width: data.w,
height: data.h,
viewBox: [data.w, data.h],
})
}
const updateLatex = (props: Partial<PPTLatexElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const openLatexEditor = () => latexEditorVisible.value = true
const updateLatexData = (data: { path: string; latex: string; w: number; h: number; }) => {
updateLatex({
path: data.path,
latex: data.latex,
width: data.w,
height: data.h,
viewBox: [data.w, data.h],
})
}
emitter.on(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
onUnmounted(() => {
emitter.off(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
})
const openLatexEditor = () => latexEditorVisible.value = true
return {
handleElement: handleElement as Ref<PPTLatexElement>,
latexEditorVisible,
updateLatex,
updateLatexData,
}
}
emitter.on(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
onUnmounted(() => {
emitter.off(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
})
</script>

View File

@ -4,7 +4,7 @@
<div style="flex: 2;">线条样式</div>
<Select
style="flex: 3;"
:value="handleElement.style"
:value="handleLineElement.style"
@change="value => updateLine({ style: value as 'solid' | 'dashed' })"
>
<SelectOption value="solid">实线</SelectOption>
@ -16,17 +16,17 @@
<Popover trigger="click">
<template #content>
<ColorPicker
:modelValue="handleElement.color"
:modelValue="handleLineElement.color"
@update:modelValue="value => updateLine({ color: value })"
/>
</template>
<ColorButton :color="handleElement.color" style="flex: 3;" />
<ColorButton :color="handleLineElement.color" style="flex: 3;" />
</Popover>
</div>
<div class="row">
<div style="flex: 2;">线条宽度</div>
<InputNumber
:value="handleElement.width"
:value="handleLineElement.width"
@change="value => updateLine({ width: value as number })"
style="flex: 3;"
/>
@ -36,8 +36,8 @@
<div style="flex: 2;">起点样式</div>
<Select
style="flex: 3;"
:value="handleElement.points[0]"
@change="value => updateLine({ points: [value as 'arrow' | 'dot', handleElement.points[1]] })"
:value="handleLineElement.points[0]"
@change="value => updateLine({ points: [value as 'arrow' | 'dot', handleLineElement.points[1]] })"
>
<SelectOption value=""></SelectOption>
<SelectOption value="arrow">箭头</SelectOption>
@ -48,8 +48,8 @@
<div style="flex: 2;">终点样式</div>
<Select
style="flex: 3;"
:value="handleElement.points[1]"
@change="value => updateLine({ points: [handleElement.points[0], value as 'arrow' | 'dot'] })"
:value="handleLineElement.points[1]"
@change="value => updateLine({ points: [handleLineElement.points[0], value as 'arrow' | 'dot'] })"
>
<SelectOption value=""></SelectOption>
<SelectOption value="arrow">箭头</SelectOption>
@ -62,8 +62,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, Ref } from 'vue'
<script lang="ts" setup>
import { Ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTLineElement } from '@/types/slides'
@ -72,30 +72,18 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ElementShadow from '../common/ElementShadow.vue'
import ColorButton from '../common/ColorButton.vue'
export default defineComponent({
name: 'line-style-panel',
components: {
ElementShadow,
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const handleLineElement = handleElement as Ref<PPTLineElement>
const updateLine = (props: Partial<PPTLineElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const { addHistorySnapshot } = useHistorySnapshot()
return {
handleElement: handleElement as Ref<PPTLineElement>,
updateLine,
}
}
})
const updateLine = (props: Partial<PPTLineElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -62,7 +62,7 @@
</SelectOption>
</SelectOptGroup>
<SelectOptGroup label="在线字体">
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
<SelectOption v-for="font in WEB_FONTS" :key="font.value" :value="font.value">
<span>{{font.label}}</span>
</SelectOption>
</SelectOptGroup>
@ -141,8 +141,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElement, PPTElementOutline, TableCell } from '@/types/slides'
@ -152,118 +152,96 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from '../common/ColorButton.vue'
const webFonts = WEB_FONTS
const slidesStore = useSlidesStore()
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
export default defineComponent({
name: 'multi-style-panel',
components: {
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (id: string, props: Partial<PPTElement>) => {
slidesStore.updateElement({ id, props })
addHistorySnapshot()
}
const updateElement = (id: string, props: Partial<PPTElement>) => {
slidesStore.updateElement({ id, props })
addHistorySnapshot()
}
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const fill = ref('#fff')
const outline = ref<PPTElementOutline>({
width: 0,
color: '#fff',
style: 'solid',
})
const fill = ref('#fff')
const outline = ref<PPTElementOutline>({
width: 0,
color: '#fff',
style: 'solid',
})
//
const updateFill = (value: string) => {
for (const el of activeElementList.value) {
if (
el.type === 'text' ||
el.type === 'shape' ||
el.type === 'chart'
) updateElement(el.id, { fill: value })
//
const updateFill = (value: string) => {
for (const el of activeElementList.value) {
if (
el.type === 'text' ||
el.type === 'shape' ||
el.type === 'chart'
) updateElement(el.id, { fill: value })
if (el.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(el.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, backcolor: value }
}
}
updateElement(el.id, { data })
}
if (el.type === 'audio') updateElement(el.id, { color: value })
}
fill.value = value
}
// /线
const updateOutline = (outlineProps: Partial<PPTElementOutline>) => {
for (const el of activeElementList.value) {
if (
el.type === 'text' ||
el.type === 'image' ||
el.type === 'shape' ||
el.type === 'table' ||
el.type === 'chart'
) {
const outline = el.outline || { width: 2, color: '#000', style: 'solid' }
const props = { outline: { ...outline, ...outlineProps } }
updateElement(el.id, props)
}
if (el.type === 'line') updateElement(el.id, outlineProps)
}
outline.value = { ...outline.value, ...outlineProps }
}
//
const updateFontStyle = (command: string, value: string) => {
for (const el of activeElementList.value) {
if (el.type === 'text' || (el.type === 'shape' && el.text?.content)) {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { target: el.id, action: { command, value } })
}
if (el.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(el.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, [command]: value }
}
}
updateElement(el.id, { data })
}
if (el.type === 'latex' && command === 'color') {
updateElement(el.id, { color: value })
if (el.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(el.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, backcolor: value }
}
}
updateElement(el.id, { data })
}
return {
webFonts,
richTextAttrs,
availableFonts,
fontSizeOptions,
fill,
outline,
updateFill,
updateOutline,
updateFontStyle,
if (el.type === 'audio') updateElement(el.id, { color: value })
}
fill.value = value
}
// /线
const updateOutline = (outlineProps: Partial<PPTElementOutline>) => {
for (const el of activeElementList.value) {
if (
el.type === 'text' ||
el.type === 'image' ||
el.type === 'shape' ||
el.type === 'table' ||
el.type === 'chart'
) {
const outline = el.outline || { width: 2, color: '#000', style: 'solid' }
const props = { outline: { ...outline, ...outlineProps } }
updateElement(el.id, props)
}
if (el.type === 'line') updateElement(el.id, outlineProps)
}
outline.value = { ...outline.value, ...outlineProps }
}
//
const updateFontStyle = (command: string, value: string) => {
for (const el of activeElementList.value) {
if (el.type === 'text' || (el.type === 'shape' && el.text?.content)) {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { target: el.id, action: { command, value } })
}
if (el.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(el.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, [command]: value }
}
}
updateElement(el.id, { data })
}
if (el.type === 'latex' && command === 'color') {
updateElement(el.id, { color: value })
}
}
})
}
</script>
<style lang="scss" scoped>

View File

@ -71,7 +71,7 @@
<ElementFlip />
<Divider />
<template v-if="handleElement?.text?.content">
<template v-if="handleShapeElement.text?.content">
<InputGroup compact class="row">
<Select
style="flex: 3;"
@ -85,7 +85,7 @@
</SelectOption>
</SelectOptGroup>
<SelectOptGroup label="在线字体">
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
<SelectOption v-for="font in WEB_FONTS" :key="font.value" :value="font.value">
<span>{{font.label}}</span>
</SelectOption>
</SelectOptGroup>
@ -222,8 +222,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, Ref, ref, watch } from 'vue'
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
@ -237,109 +237,80 @@ import ElementShadow from '../common/ElementShadow.vue'
import ElementFlip from '../common/ElementFlip.vue'
import ColorButton from '../common/ColorButton.vue'
const webFonts = WEB_FONTS
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(mainStore)
export default defineComponent({
name: 'shape-style-panel',
components: {
ElementOpacity,
ElementOutline,
ElementShadow,
ElementFlip,
ColorButton,
},
setup() {
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(mainStore)
const handleShapeElement = handleElement as Ref<PPTShapeElement>
const fill = ref<string>('#000')
const gradient = ref<ShapeGradient>({
type: 'linear',
rotate: 0,
color: ['#fff', '#fff'],
})
const fillType = ref('fill')
const textAlign = ref('middle')
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'shape') return
fill.value = handleElement.value.fill || '#fff'
gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, color: [fill.value, '#fff'] }
fillType.value = handleElement.value.gradient ? 'gradient' : 'fill'
textAlign.value = handleElement.value?.text?.align || 'middle'
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (props: Partial<PPTShapeElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateFillType = (type: 'gradient' | 'fill') => {
if (type === 'fill') {
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
addHistorySnapshot()
}
else updateElement({ gradient: gradient.value })
}
//
const updateGradient = (gradientProps: Partial<ShapeGradient>) => {
if (!gradient.value) return
const _gradient: ShapeGradient = { ...gradient.value, ...gradientProps }
updateElement({ gradient: _gradient })
}
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
const updateTextAlign = (align: 'top' | 'middle' | 'bottom') => {
const _handleElement = handleElement.value as PPTShapeElement
const defaultText: ShapeText = {
content: '',
defaultFontName: '微软雅黑',
defaultColor: '#000',
align: 'middle',
}
const _text = _handleElement.text || defaultText
updateElement({ text: { ..._text, align } })
}
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
return {
fill,
gradient,
fillType,
textAlign,
richTextAttrs,
availableFonts,
fontSizeOptions,
webFonts,
handleElement: handleElement as Ref<PPTShapeElement>,
emitRichTextCommand,
updateFillType,
updateFill,
updateGradient,
updateTextAlign,
}
},
const fill = ref<string>('#000')
const gradient = ref<ShapeGradient>({
type: 'linear',
rotate: 0,
color: ['#fff', '#fff'],
})
const fillType = ref('fill')
const textAlign = ref('middle')
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'shape') return
fill.value = handleElement.value.fill || '#fff'
gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, color: [fill.value, '#fff'] }
fillType.value = handleElement.value.gradient ? 'gradient' : 'fill'
textAlign.value = handleElement.value?.text?.align || 'middle'
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (props: Partial<PPTShapeElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateFillType = (type: 'gradient' | 'fill') => {
if (type === 'fill') {
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
addHistorySnapshot()
}
else updateElement({ gradient: gradient.value })
}
//
const updateGradient = (gradientProps: Partial<ShapeGradient>) => {
if (!gradient.value) return
const _gradient: ShapeGradient = { ...gradient.value, ...gradientProps }
updateElement({ gradient: _gradient })
}
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
const updateTextAlign = (align: 'top' | 'middle' | 'bottom') => {
const _handleElement = handleElement.value as PPTShapeElement
const defaultText: ShapeText = {
content: '',
defaultFontName: '微软雅黑',
defaultColor: '#000',
align: 'middle',
}
const _text = _handleElement.text || defaultText
updateElement({ text: { ..._text, align } })
}
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
</script>
<style lang="scss" scoped>

View File

@ -13,7 +13,7 @@
</SelectOption>
</SelectOptGroup>
<SelectOptGroup label="在线字体">
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
<SelectOption v-for="font in WEB_FONTS" :key="font.value" :value="font.value">
<span>{{font.label}}</span>
</SelectOption>
</SelectOptGroup>
@ -185,8 +185,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { nanoid } from 'nanoid'
import { useMainStore, useSlidesStore } from '@/store'
@ -197,24 +197,63 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ElementOutline from '../common/ElementOutline.vue'
import ColorButton from '../common/ColorButton.vue'
const webFonts = WEB_FONTS
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, selectedTableCells: selectedCells, availableFonts } = storeToRefs(useMainStore())
const themeColor = computed(() => slidesStore.theme.themeColor)
export default defineComponent({
name: 'table-style-panel',
components: {
ElementOutline,
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, selectedTableCells: selectedCells, availableFonts } = storeToRefs(useMainStore())
const themeColor = computed(() => slidesStore.theme.themeColor)
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
]
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
]
const textAttrs = ref({
const textAttrs = ref({
bold: false,
em: false,
underline: false,
strikethrough: false,
color: '#000',
backcolor: '#000',
fontsize: '12px',
fontname: '微软雅黑',
align: 'left',
})
const theme = ref<TableTheme>()
const hasTheme = ref(false)
const rowCount = ref(0)
const colCount = ref(0)
const minRowCount = ref(0)
const minColCount = ref(0)
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'table') return
theme.value = handleElement.value.theme
hasTheme.value = !!theme.value
rowCount.value = handleElement.value.data.length
colCount.value = handleElement.value.data[0].length
minRowCount.value = handleElement.value.data.length
minColCount.value = handleElement.value.data[0].length
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateTextAttrState = () => {
if (!handleElement.value || handleElement.value.type !== 'table') return
let rowIndex = 0
let colIndex = 0
if (selectedCells.value.length) {
const selectedCell = selectedCells.value[0]
rowIndex = +selectedCell.split('_')[0]
colIndex = +selectedCell.split('_')[1]
}
const style = handleElement.value.data[rowIndex][colIndex].style
if (!style) {
textAttrs.value = {
bold: false,
em: false,
underline: false,
@ -224,200 +263,132 @@ export default defineComponent({
fontsize: '12px',
fontname: '微软雅黑',
align: 'left',
})
const theme = ref<TableTheme>()
const hasTheme = ref(false)
const rowCount = ref(0)
const colCount = ref(0)
const minRowCount = ref(0)
const minColCount = ref(0)
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'table') return
theme.value = handleElement.value.theme
hasTheme.value = !!theme.value
rowCount.value = handleElement.value.data.length
colCount.value = handleElement.value.data[0].length
minRowCount.value = handleElement.value.data.length
minColCount.value = handleElement.value.data[0].length
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateTextAttrState = () => {
if (!handleElement.value || handleElement.value.type !== 'table') return
let rowIndex = 0
let colIndex = 0
if (selectedCells.value.length) {
const selectedCell = selectedCells.value[0]
rowIndex = +selectedCell.split('_')[0]
colIndex = +selectedCell.split('_')[1]
}
const style = handleElement.value.data[rowIndex][colIndex].style
if (!style) {
textAttrs.value = {
bold: false,
em: false,
underline: false,
strikethrough: false,
color: '#000',
backcolor: '#000',
fontsize: '12px',
fontname: '微软雅黑',
align: 'left',
}
}
else {
textAttrs.value = {
bold: !!style.bold,
em: !!style.em,
underline: !!style.underline,
strikethrough: !!style.strikethrough,
color: style.color || '#000',
backcolor: style.backcolor || '#000',
fontsize: style.fontsize || '12px',
fontname: style.fontname || '微软雅黑',
align: style.align || 'left',
}
}
}
onMounted(() => {
if (selectedCells.value.length) updateTextAttrState()
})
watch(selectedCells, updateTextAttrState)
const updateElement = (props: Partial<PPTTableElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
else {
textAttrs.value = {
bold: !!style.bold,
em: !!style.em,
underline: !!style.underline,
strikethrough: !!style.strikethrough,
color: style.color || '#000',
backcolor: style.backcolor || '#000',
fontsize: style.fontsize || '12px',
fontname: style.fontname || '微软雅黑',
align: style.align || 'left',
}
}
}
//
const updateTextAttrs = (textAttrProp: Partial<TableCellStyle>) => {
const _handleElement = handleElement.value as PPTTableElement
const data: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
if (!selectedCells.value.length || selectedCells.value.includes(`${i}_${j}`)) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, ...textAttrProp }
}
}
}
updateElement({ data })
updateTextAttrState()
}
//
const updateTheme = (themeProp: Partial<TableTheme>) => {
if (!theme.value) return
const _theme = { ...theme.value, ...themeProp }
updateElement({ theme: _theme })
}
// /
const toggleTheme = (checked: boolean) => {
if (checked) {
const props = {
theme: {
color: themeColor.value,
rowHeader: true,
rowFooter: false,
colHeader: false,
colFooter: false,
}
}
updateElement(props)
}
else {
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'theme' })
addHistorySnapshot()
}
}
//
const setTableRow = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const rowCount = _handleElement.data.length
if (value > rowCount) {
const rowCells: TableCell[] = new Array(colCount.value).fill({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
const newTableCells: TableCell[][] = new Array(value - rowCount).fill(rowCells)
const tableCells: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
tableCells.push(...newTableCells)
updateElement({ data: tableCells })
}
else {
const tableCells: TableCell[][] = _handleElement.data.slice(0, value)
updateElement({ data: tableCells })
}
}
//
const setTableCol = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const colCount = _handleElement.data[0].length
let tableCells = _handleElement.data
let colSizeList = _handleElement.colWidths.map(item => item * _handleElement.width)
if (value > colCount) {
tableCells = tableCells.map(item => {
const cells: TableCell[] = new Array(value - colCount).fill({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
item.push(...cells)
return item
})
const newColSizeList: number[] = new Array(value - colCount).fill(100)
colSizeList.push(...newColSizeList)
}
else {
tableCells = tableCells.map(item => item.slice(0, value))
colSizeList = colSizeList.slice(0, value)
}
const width = colSizeList.reduce((a, b) => a + b)
const colWidths = colSizeList.map(item => item / width)
const props = {
width,
data: tableCells,
colWidths,
}
updateElement(props)
}
return {
availableFonts,
fontSizeOptions,
textAttrs,
updateTextAttrs,
theme,
rowCount,
colCount,
minRowCount,
minColCount,
hasTheme,
toggleTheme,
updateTheme,
setTableRow,
setTableCol,
webFonts,
}
},
onMounted(() => {
if (selectedCells.value.length) updateTextAttrState()
})
watch(selectedCells, updateTextAttrState)
const updateElement = (props: Partial<PPTTableElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
//
const updateTextAttrs = (textAttrProp: Partial<TableCellStyle>) => {
const _handleElement = handleElement.value as PPTTableElement
const data: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
if (!selectedCells.value.length || selectedCells.value.includes(`${i}_${j}`)) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, ...textAttrProp }
}
}
}
updateElement({ data })
updateTextAttrState()
}
//
const updateTheme = (themeProp: Partial<TableTheme>) => {
if (!theme.value) return
const _theme = { ...theme.value, ...themeProp }
updateElement({ theme: _theme })
}
// /
const toggleTheme = (checked: boolean) => {
if (checked) {
const props = {
theme: {
color: themeColor.value,
rowHeader: true,
rowFooter: false,
colHeader: false,
colFooter: false,
}
}
updateElement(props)
}
else {
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'theme' })
addHistorySnapshot()
}
}
//
const setTableRow = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const rowCount = _handleElement.data.length
if (value > rowCount) {
const rowCells: TableCell[] = new Array(colCount.value).fill({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
const newTableCells: TableCell[][] = new Array(value - rowCount).fill(rowCells)
const tableCells: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
tableCells.push(...newTableCells)
updateElement({ data: tableCells })
}
else {
const tableCells: TableCell[][] = _handleElement.data.slice(0, value)
updateElement({ data: tableCells })
}
}
//
const setTableCol = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const colCount = _handleElement.data[0].length
let tableCells = _handleElement.data
let colSizeList = _handleElement.colWidths.map(item => item * _handleElement.width)
if (value > colCount) {
tableCells = tableCells.map(item => {
const cells: TableCell[] = new Array(value - colCount).fill({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
item.push(...cells)
return item
})
const newColSizeList: number[] = new Array(value - colCount).fill(100)
colSizeList.push(...newColSizeList)
}
else {
tableCells = tableCells.map(item => item.slice(0, value))
colSizeList = colSizeList.slice(0, value)
}
const width = colSizeList.reduce((a, b) => a + b)
const colWidths = colSizeList.map(item => item / width)
const props = {
width,
data: tableCells,
colWidths,
}
updateElement(props)
}
</script>
<style lang="scss" scoped>

View File

@ -25,7 +25,7 @@
</SelectOption>
</SelectOptGroup>
<SelectOptGroup label="在线字体">
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
<SelectOption v-for="font in WEB_FONTS" :key="font.value" :value="font.value">
<span>{{font.label}}</span>
</SelectOption>
</SelectOptGroup>
@ -270,8 +270,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTTextElement } from '@/types/slides'
@ -358,136 +358,95 @@ const presetStyles = [
},
]
const webFonts = WEB_FONTS
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore())
export default defineComponent({
name: 'text-style-panel',
components: {
ElementOpacity,
ElementOutline,
ElementShadow,
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (props: Partial<PPTTextElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const updateElement = (props: Partial<PPTTextElement>) => {
slidesStore.updateElement({ id: handleElementId.value, props })
addHistorySnapshot()
}
const fill = ref<string>('#000')
const lineHeight = ref<number>()
const wordSpace = ref<number>()
const textIndent = ref<number>()
const paragraphSpace = ref<number>()
const fill = ref<string>('#000')
const lineHeight = ref<number>()
const wordSpace = ref<number>()
const textIndent = ref<number>()
const paragraphSpace = ref<number>()
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'text') return
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'text') return
fill.value = handleElement.value.fill || '#fff'
lineHeight.value = handleElement.value.lineHeight || 1.5
wordSpace.value = handleElement.value.wordSpace || 0
textIndent.value = handleElement.value.textIndent || 0
paragraphSpace.value = handleElement.value.paragraphSpace === undefined ? 5 : handleElement.value.paragraphSpace
}, { deep: true, immediate: true })
fill.value = handleElement.value.fill || '#fff'
lineHeight.value = handleElement.value.lineHeight || 1.5
wordSpace.value = handleElement.value.wordSpace || 0
textIndent.value = handleElement.value.textIndent || 0
paragraphSpace.value = handleElement.value.paragraphSpace === undefined ? 5 : handleElement.value.paragraphSpace
}, { deep: true, immediate: true })
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const lineHeightOptions = [0.9, 1.0, 1.15, 1.2, 1.4, 1.5, 1.8, 2.0, 2.5, 3.0]
const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 6, 8, 10]
const textIndentOptions = [0, 48, 96, 144, 192, 240, 288, 336]
const paragraphSpaceOptions = [0, 5, 10, 15, 20, 25, 30, 40, 50, 80]
const fontSizeOptions = [
'12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
'36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
'80px', '88px', '96px', '104px', '112px', '120px',
]
const lineHeightOptions = [0.9, 1.0, 1.15, 1.2, 1.4, 1.5, 1.8, 2.0, 2.5, 3.0]
const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 6, 8, 10]
const textIndentOptions = [0, 48, 96, 144, 192, 240, 288, 336]
const paragraphSpaceOptions = [0, 5, 10, 15, 20, 25, 30, 40, 50, 80]
//
const updateLineHeight = (value: number) => {
updateElement({ lineHeight: value })
}
//
const updateLineHeight = (value: number) => {
updateElement({ lineHeight: value })
}
//
const updateParagraphSpace = (value: number) => {
updateElement({ paragraphSpace: value })
}
//
const updateParagraphSpace = (value: number) => {
updateElement({ paragraphSpace: value })
}
//
const updateWordSpace = (value: number) => {
updateElement({ wordSpace: value })
}
//
const updateWordSpace = (value: number) => {
updateElement({ wordSpace: value })
}
//
const updateTextIndent = (value: number) => {
updateElement({ textIndent: value })
}
//
const updateTextIndent = (value: number) => {
updateElement({ textIndent: value })
}
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
//
const updateFill = (value: string) => {
updateElement({ fill: value })
}
//
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
//
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
//
const emitBatchRichTextCommand = (action: RichTextAction[]) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action })
}
//
const emitBatchRichTextCommand = (action: RichTextAction[]) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action })
}
//
const link = ref('')
const linkPopoverVisible = ref(false)
//
const link = ref('')
const linkPopoverVisible = ref(false)
watch(richTextAttrs, () => linkPopoverVisible.value = false)
watch(richTextAttrs, () => linkPopoverVisible.value = false)
const openLinkPopover = () => {
link.value = richTextAttrs.value.link
linkPopoverVisible.value = true
}
const updateLink = (link?: string) => {
if (link) {
const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
if (!linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
}
emitRichTextCommand('link', link)
linkPopoverVisible.value = false
}
return {
fill,
lineHeight,
wordSpace,
textIndent,
paragraphSpace,
richTextAttrs,
availableFonts,
webFonts,
fontSizeOptions,
lineHeightOptions,
wordSpaceOptions,
textIndentOptions,
paragraphSpaceOptions,
updateLineHeight,
updateParagraphSpace,
updateWordSpace,
updateTextIndent,
updateFill,
emitRichTextCommand,
emitBatchRichTextCommand,
presetStyles,
link,
linkPopoverVisible,
openLinkPopover,
updateLink,
}
},
})
const openLinkPopover = () => {
link.value = richTextAttrs.value.link
linkPopoverVisible.value = true
}
const updateLink = (link?: string) => {
if (link) {
const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
if (!linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
}
emitRichTextCommand('link', link)
linkPopoverVisible.value = false
}
</script>
<style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
<div class="background-image-wrapper">
<FileInput @change="files => setVideoPoster(files)">
<div class="background-image">
<div class="content" :style="{ backgroundImage: `url(${handleElement.poster})` }">
<div class="content" :style="{ backgroundImage: `url(${handleVideoElement.poster})` }">
<IconPlus />
</div>
</div>
@ -14,42 +14,33 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { Ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTVideoElement } from '@/types/slides'
import { getImageDataURL } from '@/utils/image'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default defineComponent({
name: 'video-style-panel',
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const { addHistorySnapshot } = useHistorySnapshot()
const handleVideoElement = handleElement as Ref<PPTVideoElement>
const updateVideo = (props: Partial<PPTVideoElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const { addHistorySnapshot } = useHistorySnapshot()
//
const setVideoPoster = (files: File[]) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => updateVideo({ poster: dataURL }))
}
const updateVideo = (props: Partial<PPTVideoElement>) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
return {
handleElement,
updateVideo,
setVideoPoster,
}
}
})
//
const setVideoPoster = (files: FileList) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => updateVideo({ poster: dataURL }))
}
</script>
<style lang="scss" scoped>

View File

@ -4,8 +4,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { ElementTypes } from '@/types/slides'
@ -33,26 +33,16 @@ const panelMap = {
[ElementTypes.AUDIO]: AudioStylePanel,
}
export default defineComponent({
name: 'element-style-panel',
setup() {
const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
const currentPanelComponent = computed(() => {
if (activeElementIdList.value.length > 1) {
if (!activeGroupElementId.value) return MultiStylePanel
const currentPanelComponent = computed(() => {
if (activeElementIdList.value.length > 1) {
if (!activeGroupElementId.value) return MultiStylePanel
const activeGroupElement = activeElementList.value.find(item => item.id === activeGroupElementId.value)
return activeGroupElement ? (panelMap[activeGroupElement.type] || null) : null
}
const activeGroupElement = activeElementList.value.find(item => item.id === activeGroupElementId.value)
return activeGroupElement ? (panelMap[activeGroupElement.type] || null) : null
}
return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
})
return {
handleElement,
currentPanelComponent,
}
},
return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
})
</script>

View File

@ -36,42 +36,25 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { ElementAlignCommands } from '@/types/edit'
import useCombineElement from '@/hooks/useCombineElement'
import useAlignActiveElement from '@/hooks/useAlignActiveElement'
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
import useUniformDisplayElement from '@/hooks/useUniformDisplayElement'
export default defineComponent({
name: 'multi-position-panel',
setup() {
const { canCombine, combineElements, uncombineElements } = useCombineElement()
const { alignActiveElement } = useAlignActiveElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { displayItemCount, uniformHorizontalDisplay, uniformVerticalDisplay } = useUniformDisplayElement()
const { canCombine, combineElements, uncombineElements } = useCombineElement()
const { alignActiveElement } = useAlignActiveElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { displayItemCount, uniformHorizontalDisplay, uniformVerticalDisplay } = useUniformDisplayElement()
//
//
//
const alignElement = (command: ElementAlignCommands) => {
if (canCombine.value) alignActiveElement(command)
else alignElementToCanvas(command)
}
return {
canCombine,
displayItemCount,
combineElements,
uncombineElements,
uniformHorizontalDisplay,
uniformVerticalDisplay,
alignElement,
ElementAlignCommands,
}
},
})
//
//
//
const alignElement = (command: ElementAlignCommands) => {
if (canCombine.value) alignActiveElement(command)
else alignElementToCanvas(command)
}
</script>
<style lang="scss" scoped>

View File

@ -16,8 +16,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { TurningMode } from '@/types/slides'
@ -28,50 +28,38 @@ interface Animations {
value: TurningMode
}
export default defineComponent({
name: 'slide-animation-panel',
setup() {
const slidesStore = useSlidesStore()
const { slides, currentSlide } = storeToRefs(slidesStore)
const slidesStore = useSlidesStore()
const { slides, currentSlide } = storeToRefs(slidesStore)
const currentTurningMode = computed(() => currentSlide.value.turningMode || 'slideY')
const currentTurningMode = computed(() => currentSlide.value.turningMode || 'slideY')
const animations: Animations[] = [
{ label: '无', value: 'no' },
{ label: '淡入淡出', value: 'fade' },
{ label: '左右推移', value: 'slideX' },
{ label: '上下推移', value: 'slideY' },
]
const animations: Animations[] = [
{ label: '无', value: 'no' },
{ label: '淡入淡出', value: 'fade' },
{ label: '左右推移', value: 'slideX' },
{ label: '上下推移', value: 'slideY' },
]
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateTurningMode = (mode: TurningMode) => {
if (mode === currentTurningMode.value) return
slidesStore.updateSlide({ turningMode: mode })
addHistorySnapshot()
}
//
const applyAllSlide = () => {
const newSlides = slides.value.map(slide => {
return {
...slide,
turningMode: currentSlide.value.turningMode,
}
})
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
//
const updateTurningMode = (mode: TurningMode) => {
if (mode === currentTurningMode.value) return
slidesStore.updateSlide({ turningMode: mode })
addHistorySnapshot()
}
//
const applyAllSlide = () => {
const newSlides = slides.value.map(slide => {
return {
currentTurningMode,
animations,
updateTurningMode,
applyAllSlide,
...slide,
turningMode: currentSlide.value.turningMode,
}
},
})
})
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -122,7 +122,7 @@
</SelectOption>
</SelectOptGroup>
<SelectOptGroup label="在线字体">
<SelectOption v-for="font in webFonts" :key="font.value" :value="font.value">
<SelectOption v-for="font in WEB_FONTS" :key="font.value" :value="font.value">
<span>{{font.label}}</span>
</SelectOption>
</SelectOptGroup>
@ -171,7 +171,7 @@
<div class="theme-list" v-if="showPresetThemes">
<div
class="theme-item"
v-for="(item, index) in themes"
v-for="(item, index) in PRESET_THEMES"
:key="index"
:style="{ backgroundColor: item.background }"
@click="updateTheme({
@ -191,8 +191,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { Slide, SlideBackground, SlideTheme } from '@/types/slides'
@ -203,169 +203,140 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from './common/ColorButton.vue'
import { getImageDataURL } from '@/utils/image'
const themes = PRESET_THEMES
const webFonts = WEB_FONTS
const slidesStore = useSlidesStore()
const { availableFonts } = storeToRefs(useMainStore())
const { slides, currentSlide, viewportRatio, theme } = storeToRefs(slidesStore)
export default defineComponent({
name: 'slide-design-panel',
components: {
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { availableFonts } = storeToRefs(useMainStore())
const { slides, currentSlide, viewportRatio, theme } = storeToRefs(slidesStore)
const background = computed(() => {
if (!currentSlide.value.background) {
return {
type: 'solid',
value: '#fff',
} as SlideBackground
}
return currentSlide.value.background
})
const background = computed(() => {
if (!currentSlide.value.background) {
return {
type: 'solid',
value: '#fff',
} as SlideBackground
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateBackgroundType = (type: 'solid' | 'image' | 'gradient') => {
if (type === 'solid') {
const newBackground: SlideBackground = {
...background.value,
type: 'solid',
color: background.value.color || '#fff',
}
slidesStore.updateSlide({ background: newBackground })
}
else if (type === 'image') {
const newBackground: SlideBackground = {
...background.value,
type: 'image',
image: background.value.image || '',
imageSize: background.value.imageSize || 'cover',
}
slidesStore.updateSlide({ background: newBackground })
}
else {
const newBackground: SlideBackground = {
...background.value,
type: 'gradient',
gradientType: background.value.gradientType || 'linear',
gradientColor: background.value.gradientColor || ['#fff', '#fff'],
gradientRotate: background.value.gradientRotate || 0,
}
slidesStore.updateSlide({ background: newBackground })
}
addHistorySnapshot()
}
//
const updateBackground = (props: Partial<SlideBackground>) => {
slidesStore.updateSlide({ background: { ...background.value, ...props } })
addHistorySnapshot()
}
//
const uploadBackgroundImage = (files: FileList) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => updateBackground({ image: dataURL }))
}
//
const applyBackgroundAllSlide = () => {
const newSlides = slides.value.map(slide => {
return {
...slide,
background: currentSlide.value.background,
}
})
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
//
const updateTheme = (themeProps: Partial<SlideTheme>) => {
slidesStore.setTheme(themeProps)
}
//
const applyThemeAllSlide = () => {
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value))
const { themeColor, backgroundColor, fontColor, fontName } = theme.value
for (const slide of newSlides) {
if (!slide.background || slide.background.type !== 'image') {
slide.background = {
...slide.background,
type: 'solid',
color: backgroundColor
}
return currentSlide.value.background
})
}
const { addHistorySnapshot } = useHistorySnapshot()
//
const updateBackgroundType = (type: 'solid' | 'image' | 'gradient') => {
if (type === 'solid') {
const newBackground: SlideBackground = {
...background.value,
type: 'solid',
color: background.value.color || '#fff',
}
slidesStore.updateSlide({ background: newBackground })
const elements = slide.elements
for (const el of elements) {
if (el.type === 'shape') el.fill = themeColor
else if (el.type === 'line') el.color = themeColor
else if (el.type === 'text') {
el.defaultColor = fontColor
el.defaultFontName = fontName
if (el.fill) el.fill = themeColor
}
else if (type === 'image') {
const newBackground: SlideBackground = {
...background.value,
type: 'image',
image: background.value.image || '',
imageSize: background.value.imageSize || 'cover',
}
slidesStore.updateSlide({ background: newBackground })
}
else {
const newBackground: SlideBackground = {
...background.value,
type: 'gradient',
gradientType: background.value.gradientType || 'linear',
gradientColor: background.value.gradientColor || ['#fff', '#fff'],
gradientRotate: background.value.gradientRotate || 0,
}
slidesStore.updateSlide({ background: newBackground })
}
addHistorySnapshot()
}
//
const updateBackground = (props: Partial<SlideBackground>) => {
slidesStore.updateSlide({ background: { ...background.value, ...props } })
addHistorySnapshot()
}
//
const uploadBackgroundImage = (files: File[]) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => updateBackground({ image: dataURL }))
}
//
const applyBackgroundAllSlide = () => {
const newSlides = slides.value.map(slide => {
return {
...slide,
background: currentSlide.value.background,
}
})
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
//
const updateTheme = (themeProps: Partial<SlideTheme>) => {
slidesStore.setTheme(themeProps)
}
//
const applyThemeAllSlide = () => {
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value))
const { themeColor, backgroundColor, fontColor, fontName } = theme.value
for (const slide of newSlides) {
if (!slide.background || slide.background.type !== 'image') {
slide.background = {
...slide.background,
type: 'solid',
color: backgroundColor
}
}
const elements = slide.elements
for (const el of elements) {
if (el.type === 'shape') el.fill = themeColor
else if (el.type === 'line') el.color = themeColor
else if (el.type === 'text') {
el.defaultColor = fontColor
el.defaultFontName = fontName
if (el.fill) el.fill = themeColor
}
else if (el.type === 'table') {
if (el.theme) el.theme.color = themeColor
for (const rowCells of el.data) {
for (const cell of rowCells) {
if (cell.style) {
cell.style.color = fontColor
cell.style.fontname = fontName
}
}
else if (el.type === 'table') {
if (el.theme) el.theme.color = themeColor
for (const rowCells of el.data) {
for (const cell of rowCells) {
if (cell.style) {
cell.style.color = fontColor
cell.style.fontname = fontName
}
}
else if (el.type === 'chart') {
el.themeColor = [themeColor]
el.gridColor = fontColor
}
else if (el.type === 'latex') el.color = fontColor
else if (el.type === 'audio') el.color = themeColor
}
}
slidesStore.setSlides(newSlides)
addHistorySnapshot()
else if (el.type === 'chart') {
el.themeColor = [themeColor]
el.gridColor = fontColor
}
else if (el.type === 'latex') el.color = fontColor
else if (el.type === 'audio') el.color = themeColor
}
}
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
//
const showPresetThemes = ref(true)
const togglePresetThemesVisible = () => {
showPresetThemes.value = !showPresetThemes.value
}
//
const showPresetThemes = ref(true)
const togglePresetThemesVisible = () => {
showPresetThemes.value = !showPresetThemes.value
}
//
const updateViewportRatio = (value: number) => {
slidesStore.setViewportRatio(value)
}
return {
availableFonts,
background,
updateBackgroundType,
updateBackground,
uploadBackgroundImage,
applyBackgroundAllSlide,
themes,
theme,
webFonts,
updateTheme,
applyThemeAllSlide,
viewportRatio,
updateViewportRatio,
showPresetThemes,
togglePresetThemesVisible,
}
},
})
//
const updateViewportRatio = (value: number) => {
slidesStore.setViewportRatio(value)
}
</script>
<style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
<div
class="tab"
:class="{ 'active': selectedSymbolKey === item.key }"
v-for="item in symbolPoolList"
v-for="item in SYMBOL_LIST"
:key="item.key"
@click="selectedSymbolKey = item.key"
>{{item.label}}</div>
@ -17,34 +17,20 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { SYMBOL_LIST } from '@/configs/symbol'
import emitter, { EmitterEvents } from '@/utils/emitter'
const symbolPoolList = SYMBOL_LIST
export default defineComponent({
name: 'symbol-panel',
setup() {
const selectedSymbolKey = ref(symbolPoolList[0].key)
const symbolPool = computed(() => {
const selectedSymbol = symbolPoolList.find(item => item.key === selectedSymbolKey.value)
return selectedSymbol?.children || []
})
const selectSymbol = (value: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command: 'insert', value } })
}
return {
symbolPoolList,
symbolPool,
selectedSymbolKey,
selectSymbol,
}
},
const selectedSymbolKey = ref(SYMBOL_LIST[0].key)
const symbolPool = computed(() => {
const selectedSymbol = SYMBOL_LIST.find(item => item.key === selectedSymbolKey.value)
return selectedSymbol?.children || []
})
const selectSymbol = (value: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command: 'insert', value } })
}
</script>
<style lang="scss" scoped>

View File

@ -7,16 +7,11 @@
</Button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'color-button',
props: {
color: {
type: String,
required: true,
},
<script lang="ts" setup>
defineProps({
color: {
type: String,
required: true,
},
})
</script>

View File

@ -25,8 +25,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTImageElement } from '@/types/slides'
@ -52,63 +52,51 @@ const defaultFilters: FilterOption[] = [
{ label: '不透明度', key: 'opacity', default: 100, value: 100, unit: '%', max: 100, step: 5 },
]
export default defineComponent({
name: 'element-filter',
setup() {
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore())
const filterOptions = ref<FilterOption[]>(JSON.parse(JSON.stringify(defaultFilters)))
const hasFilters = ref(false)
const filterOptions = ref<FilterOption[]>(JSON.parse(JSON.stringify(defaultFilters)))
const hasFilters = ref(false)
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'image') return
const filters = handleElement.value.filters
if (filters) {
filterOptions.value = defaultFilters.map(item => {
if (filters[item.key] !== undefined) return { ...item, value: parseInt(filters[item.key]) }
return item
})
hasFilters.value = true
}
else {
filterOptions.value = JSON.parse(JSON.stringify(defaultFilters))
hasFilters.value = false
}
}, { deep: true, immediate: true })
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'image') return
const filters = handleElement.value.filters
if (filters) {
filterOptions.value = defaultFilters.map(item => {
if (filters[item.key] !== undefined) return { ...item, value: parseInt(filters[item.key]) }
return item
})
hasFilters.value = true
}
else {
filterOptions.value = JSON.parse(JSON.stringify(defaultFilters))
hasFilters.value = false
}
}, { deep: true, immediate: true })
//
const updateFilter = (filter: FilterOption, value: number) => {
const _handleElement = handleElement.value as PPTImageElement
const originFilters = _handleElement.filters || {}
const filters = { ...originFilters, [filter.key]: `${value}${filter.unit}` }
slidesStore.updateElement({ id: handleElementId.value, props: { filters } })
addHistorySnapshot()
}
//
const updateFilter = (filter: FilterOption, value: number) => {
const _handleElement = handleElement.value as PPTImageElement
const originFilters = _handleElement.filters || {}
const filters = { ...originFilters, [filter.key]: `${value}${filter.unit}` }
slidesStore.updateElement({ id: handleElementId.value, props: { filters } })
addHistorySnapshot()
}
const toggleFilters = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
slidesStore.updateElement({ id: handleElement.value.id, props: { filters: {} } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'filters' })
}
addHistorySnapshot()
}
return {
filterOptions,
hasFilters,
toggleFilters,
updateFilter,
}
},
})
const toggleFilters = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
slidesStore.updateElement({ id: handleElement.value.id, props: { filters: {} } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'filters' })
}
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -15,44 +15,33 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { ImageOrShapeFlip } from '@/types/slides'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default defineComponent({
name: 'element-flip',
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const flipH = ref(false)
const flipV = ref(false)
const flipH = ref(false)
const flipV = ref(false)
watch(handleElement, () => {
if (handleElement.value && (handleElement.value.type === 'image' || handleElement.value.type === 'shape')) {
flipH.value = !!handleElement.value.flipH
flipV.value = !!handleElement.value.flipV
}
}, { deep: true, immediate: true })
watch(handleElement, () => {
if (handleElement.value && (handleElement.value.type === 'image' || handleElement.value.type === 'shape')) {
flipH.value = !!handleElement.value.flipH
flipV.value = !!handleElement.value.flipV
}
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateFlip = (flipProps: ImageOrShapeFlip) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props: flipProps })
addHistorySnapshot()
}
return {
flipH,
flipV,
updateFlip,
}
},
})
const updateFlip = (flipProps: ImageOrShapeFlip) => {
if (!handleElement.value) return
slidesStore.updateElement({ id: handleElement.value.id, props: flipProps })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -14,40 +14,30 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default defineComponent({
name: 'element-opacity',
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const opacity = ref<number>(1)
const opacity = ref<number>(1)
watch(handleElement, () => {
if (!handleElement.value) return
opacity.value = 'opacity' in handleElement.value && handleElement.value.opacity !== undefined ? handleElement.value.opacity : 1
}, { deep: true, immediate: true })
watch(handleElement, () => {
if (!handleElement.value) return
opacity.value = 'opacity' in handleElement.value && handleElement.value.opacity !== undefined ? handleElement.value.opacity : 1
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateOpacity = (value: number) => {
if (!handleElement.value) return
const props = { opacity: value }
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
return {
opacity,
updateOpacity,
}
},
})
const updateOpacity = (value: number) => {
if (!handleElement.value) return
const props = { opacity: value }
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -45,8 +45,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElementOutline } from '@/types/slides'
@ -54,59 +54,45 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from './ColorButton.vue'
export default defineComponent({
name: 'element-outline',
components: {
ColorButton,
},
props: {
fixed: {
type: Boolean,
default: false,
},
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const outline = ref<PPTElementOutline>()
const hasOutline = ref(false)
watch(handleElement, () => {
if (!handleElement.value) return
outline.value = 'outline' in handleElement.value ? handleElement.value.outline : undefined
hasOutline.value = !!outline.value
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const updateOutline = (outlineProps: Partial<PPTElementOutline>) => {
if (!handleElement.value) return
const props = { outline: { ...outline.value, ...outlineProps } }
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const toggleOutline = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
const _outline: PPTElementOutline = { width: 2, color: '#000', style: 'solid' }
slidesStore.updateElement({ id: handleElement.value.id, props: { outline: _outline } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'outline' })
}
addHistorySnapshot()
}
return {
outline,
hasOutline,
toggleOutline,
updateOutline,
}
defineProps({
fixed: {
type: Boolean,
default: false,
},
})
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const outline = ref<PPTElementOutline>()
const hasOutline = ref(false)
watch(handleElement, () => {
if (!handleElement.value) return
outline.value = 'outline' in handleElement.value ? handleElement.value.outline : undefined
hasOutline.value = !!outline.value
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const updateOutline = (outlineProps: Partial<PPTElementOutline>) => {
if (!handleElement.value) return
const props = { outline: { ...outline.value, ...outlineProps } }
slidesStore.updateElement({ id: handleElement.value.id, props })
addHistorySnapshot()
}
const toggleOutline = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
const _outline: PPTElementOutline = { width: 2, color: '#000', style: 'solid' }
slidesStore.updateElement({ id: handleElement.value.id, props: { outline: _outline } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'outline' })
}
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -56,8 +56,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElementShadow } from '@/types/slides'
@ -65,53 +65,38 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ColorButton from './ColorButton.vue'
export default defineComponent({
name: 'element-shadow',
components: {
ColorButton,
},
setup() {
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())
const shadow = ref<PPTElementShadow>()
const hasShadow = ref(false)
const shadow = ref<PPTElementShadow>()
const hasShadow = ref(false)
watch(handleElement, () => {
if (!handleElement.value) return
shadow.value = 'shadow' in handleElement.value ? handleElement.value.shadow : undefined
hasShadow.value = !!shadow.value
}, { deep: true, immediate: true })
watch(handleElement, () => {
if (!handleElement.value) return
shadow.value = 'shadow' in handleElement.value ? handleElement.value.shadow : undefined
hasShadow.value = !!shadow.value
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateShadow = (shadowProps: Partial<PPTElementShadow>) => {
if (!handleElement.value || !shadow.value) return
const _shadow = { ...shadow.value, ...shadowProps }
slidesStore.updateElement({ id: handleElement.value.id, props: { shadow: _shadow } })
addHistorySnapshot()
}
const updateShadow = (shadowProps: Partial<PPTElementShadow>) => {
if (!handleElement.value || !shadow.value) return
const _shadow = { ...shadow.value, ...shadowProps }
slidesStore.updateElement({ id: handleElement.value.id, props: { shadow: _shadow } })
addHistorySnapshot()
}
const toggleShadow = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
const _shadow: PPTElementShadow = { h: 1, v: 1, blur: 2, color: '#000' }
slidesStore.updateElement({ id: handleElement.value.id, props: { shadow: _shadow } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'shadow' })
}
addHistorySnapshot()
}
return {
shadow,
hasShadow,
toggleShadow,
updateShadow,
}
},
})
const toggleShadow = (checked: boolean) => {
if (!handleElement.value) return
if (checked) {
const _shadow: PPTElementShadow = { h: 1, v: 1, blur: 2, color: '#000' }
slidesStore.updateElement({ id: handleElement.value.id, props: { shadow: _shadow } })
}
else {
slidesStore.removeElementProps({ id: handleElement.value.id, propName: 'shadow' })
}
addHistorySnapshot()
}
</script>
<style lang="scss" scoped>

View File

@ -15,8 +15,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue'
<script lang="ts" setup>
import { computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import { ToolbarStates } from '@/types/toolbar'
@ -34,74 +34,62 @@ interface ElementTabs {
value: ToolbarStates
}
export default defineComponent({
name: 'toolbar',
setup() {
const mainStore = useMainStore()
const { activeElementIdList, handleElement, toolbarState } = storeToRefs(mainStore)
const mainStore = useMainStore()
const { activeElementIdList, handleElement, toolbarState } = storeToRefs(mainStore)
const elementTabs = computed<ElementTabs[]>(() => {
if (handleElement.value?.type === 'text') {
return [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '符号', value: ToolbarStates.SYMBOL },
{ label: '位置', value: ToolbarStates.EL_POSITION },
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
]
}
return [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '位置', value: ToolbarStates.EL_POSITION },
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
]
})
const slideTabs = [
{ label: '设计', value: ToolbarStates.SLIDE_DESIGN },
{ label: '切换', value: ToolbarStates.SLIDE_ANIMATION },
const elementTabs = computed<ElementTabs[]>(() => {
if (handleElement.value?.type === 'text') {
return [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '符号', value: ToolbarStates.SYMBOL },
{ label: '位置', value: ToolbarStates.EL_POSITION },
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
]
const multiSelectTabs = [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '位置', value: ToolbarStates.MULTI_POSITION },
]
}
return [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '位置', value: ToolbarStates.EL_POSITION },
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
]
})
const slideTabs = [
{ label: '设计', value: ToolbarStates.SLIDE_DESIGN },
{ label: '切换', value: ToolbarStates.SLIDE_ANIMATION },
{ label: '动画', value: ToolbarStates.EL_ANIMATION },
]
const multiSelectTabs = [
{ label: '样式', value: ToolbarStates.EL_STYLE },
{ label: '位置', value: ToolbarStates.MULTI_POSITION },
]
const setToolbarState = (value: ToolbarStates) => {
mainStore.setToolbarState(value)
}
const setToolbarState = (value: ToolbarStates) => {
mainStore.setToolbarState(value)
}
const currentTabs = computed(() => {
if (!activeElementIdList.value.length) return slideTabs
else if (activeElementIdList.value.length > 1) return multiSelectTabs
return elementTabs.value
})
const currentTabs = computed(() => {
if (!activeElementIdList.value.length) return slideTabs
else if (activeElementIdList.value.length > 1) return multiSelectTabs
return elementTabs.value
})
watch(currentTabs, () => {
const currentTabsValue: ToolbarStates[] = currentTabs.value.map(tab => tab.value)
if (!currentTabsValue.includes(toolbarState.value)) {
mainStore.setToolbarState(currentTabsValue[0])
}
})
watch(currentTabs, () => {
const currentTabsValue: ToolbarStates[] = currentTabs.value.map(tab => tab.value)
if (!currentTabsValue.includes(toolbarState.value)) {
mainStore.setToolbarState(currentTabsValue[0])
}
})
const currentPanelComponent = computed(() => {
const panelMap = {
[ToolbarStates.EL_STYLE]: ElementStylePanel,
[ToolbarStates.EL_POSITION]: ElementPositionPanel,
[ToolbarStates.EL_ANIMATION]: ElementAnimationPanel,
[ToolbarStates.SLIDE_DESIGN]: SlideDesignPanel,
[ToolbarStates.SLIDE_ANIMATION]: SlideAnimationPanel,
[ToolbarStates.MULTI_POSITION]: MultiPositionPanel,
[ToolbarStates.SYMBOL]: SymbolPanel,
}
return panelMap[toolbarState.value] || null
})
return {
toolbarState,
currentTabs,
setToolbarState,
currentPanelComponent,
}
},
const currentPanelComponent = computed(() => {
const panelMap = {
[ToolbarStates.EL_STYLE]: ElementStylePanel,
[ToolbarStates.EL_POSITION]: ElementPositionPanel,
[ToolbarStates.EL_ANIMATION]: ElementAnimationPanel,
[ToolbarStates.SLIDE_DESIGN]: SlideDesignPanel,
[ToolbarStates.SLIDE_ANIMATION]: SlideAnimationPanel,
[ToolbarStates.MULTI_POSITION]: MultiPositionPanel,
[ToolbarStates.SYMBOL]: SymbolPanel,
}
return panelMap[toolbarState.value] || null
})
</script>

View File

@ -29,8 +29,8 @@
</Modal>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/store'
import useGlobalHotkey from '@/hooks/useGlobalHotkey'
@ -44,34 +44,14 @@ import Toolbar from './Toolbar/index.vue'
import Remark from './Remark/index.vue'
import ExportDialog from './ExportDialog/index.vue'
export default defineComponent({
name: 'editor',
components: {
EditorHeader,
Canvas,
CanvasTool,
Thumbnails,
Toolbar,
Remark,
ExportDialog,
},
setup() {
const mainStore = useMainStore()
const { dialogForExport } = storeToRefs(mainStore)
const closeExportDialog = () => mainStore.setDialogForExport('')
const mainStore = useMainStore()
const { dialogForExport } = storeToRefs(mainStore)
const closeExportDialog = () => mainStore.setDialogForExport('')
const remarkHeight = ref(40)
const remarkHeight = ref(40)
useGlobalHotkey()
usePasteEvent()
return {
remarkHeight,
dialogForExport,
closeExportDialog,
}
},
})
useGlobalHotkey()
usePasteEvent()
</script>
<style lang="scss" scoped>

View File

@ -96,10 +96,10 @@
<Divider style="margin: 20px 0;" />
<ButtonGroup class="row">
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.TOP)"><IconSendToBack class="icon" /> 置顶</Button>
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.BOTTOM)"><IconBringToFrontOne class="icon" /> 置底</Button>
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.UP)"><IconBringToFront class="icon" /> 上移</Button>
<Button style="flex: 1;" @click="orderElement(handleElement, ElementOrderCommands.DOWN)"><IconSentToBack class="icon" /> 下移</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.TOP)"><IconSendToBack class="icon" /> 置顶</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.BOTTOM)"><IconBringToFrontOne class="icon" /> 置底</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.UP)"><IconBringToFront class="icon" /> 上移</Button>
<Button style="flex: 1;" @click="orderElement(handleElement!, ElementOrderCommands.DOWN)"><IconSentToBack class="icon" /> 下移</Button>
</ButtonGroup>
<Divider style="margin: 20px 0;" />
@ -119,8 +119,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, Ref, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElement, TableCell } from '@/types/slides'
@ -139,100 +139,78 @@ interface TabItem {
const colors = ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c', '#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57']
export default defineComponent({
name: 'element-toolbar',
setup() {
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs } = storeToRefs(mainStore)
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, richTextAttrs } = storeToRefs(mainStore)
const { addHistorySnapshot } = useHistorySnapshot()
const { addHistorySnapshot } = useHistorySnapshot()
const updateElement = (id: string, props: Partial<PPTElement>) => {
slidesStore.updateElement({ id, props })
addHistorySnapshot()
}
const updateElement = (id: string, props: Partial<PPTElement>) => {
slidesStore.updateElement({ id, props })
addHistorySnapshot()
}
const tabs: TabItem[] = [
{ key: 'style', label: '样式' },
{ key: 'common', label: '布局' },
]
const activeTab = ref('common')
const tabs: TabItem[] = [
{ key: 'style', label: '样式' },
{ key: 'common', label: '布局' },
]
const activeTab = ref('common')
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { addElementsFromData } = useAddSlidesOrElements()
const { deleteElement } = useDeleteElement()
const { orderElement } = useOrderElement()
const { alignElementToCanvas } = useAlignElementToCanvas()
const { addElementsFromData } = useAddSlidesOrElements()
const { deleteElement } = useDeleteElement()
const copyElement = () => {
const element: PPTElement = JSON.parse(JSON.stringify(handleElement.value))
addElementsFromData([element])
}
const copyElement = () => {
const element: PPTElement = JSON.parse(JSON.stringify(handleElement.value))
addElementsFromData([element])
}
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
const emitRichTextCommand = (command: string, value?: string) => {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
}
const updateFontColor = (color: string) => {
if (!handleElement.value) return
if (handleElement.value.type === 'text' || (handleElement.value.type === 'shape' && handleElement.value.text?.content)) {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command: 'color', value: color } })
}
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, color }
}
}
updateElement(handleElementId.value, { data })
}
if (handleElement.value.type === 'latex') {
updateElement(handleElementId.value, { color })
const updateFontColor = (color: string) => {
if (!handleElement.value) return
if (handleElement.value.type === 'text' || (handleElement.value.type === 'shape' && handleElement.value.text?.content)) {
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command: 'color', value: color } })
}
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, color }
}
}
updateElement(handleElementId.value, { data })
}
if (handleElement.value.type === 'latex') {
updateElement(handleElementId.value, { color })
}
}
const updateFill = (color: string) => {
if (!handleElement.value) return
if (
handleElement.value.type === 'text' ||
handleElement.value.type === 'shape' ||
handleElement.value.type === 'chart'
) updateElement(handleElementId.value, { fill: color })
const updateFill = (color: string) => {
if (!handleElement.value) return
if (
handleElement.value.type === 'text' ||
handleElement.value.type === 'shape' ||
handleElement.value.type === 'chart'
) updateElement(handleElementId.value, { fill: color })
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, backcolor: color }
}
}
updateElement(handleElementId.value, { data })
if (handleElement.value.type === 'table') {
const data: TableCell[][] = JSON.parse(JSON.stringify(handleElement.value.data))
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const style = data[i][j].style || {}
data[i][j].style = { ...style, backcolor: color }
}
if (handleElement.value.type === 'audio') updateElement(handleElementId.value, { color })
}
updateElement(handleElementId.value, { data })
}
return {
handleElement: handleElement as Ref<PPTElement>,
tabs,
activeTab,
richTextAttrs,
colors,
orderElement,
alignElementToCanvas,
copyElement,
deleteElement,
emitRichTextCommand,
updateFontColor,
updateFill,
ElementOrderCommands,
ElementAlignCommands,
}
},
})
if (handleElement.value.type === 'audio') updateElement(handleElementId.value, { color })
}
</script>
<style lang="scss" scoped>

View File

@ -8,33 +8,22 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useSnapshotStore } from '@/store'
import { Mode } from '@/types/mobile'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default defineComponent({
name: 'mobile-editor-header',
props: {
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
},
setup() {
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
const { redo, undo } = useHistorySnapshot()
return {
redo,
undo,
canUndo,
canRedo,
}
defineProps({
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
})
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
const { redo, undo } = useHistorySnapshot()
</script>
<style lang="scss" scoped>

View File

@ -14,8 +14,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { ElementTypes, PPTElement } from '@/types/slides'
import ImageElement from '@/views/components/element/ImageElement/index.vue'
@ -28,41 +28,33 @@ import LatexElement from '@/views/components/element/LatexElement/index.vue'
import VideoElement from '@/views/components/element/VideoElement/index.vue'
import AudioElement from '@/views/components/element/AudioElement/index.vue'
export default defineComponent({
name: 'mobile-editable-element',
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
elementIndex: {
type: Number,
required: true,
},
selectElement: {
type: Function as PropType<(e: TouchEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,
},
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
setup(props) {
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
}
return elementTypeMap[props.elementInfo.type] || null
})
return {
currentElementComponent,
}
elementIndex: {
type: Number,
required: true,
},
selectElement: {
type: Function as PropType<(e: TouchEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,
},
})
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
}
return elementTypeMap[props.elementInfo.type] || null
})
</script>

View File

@ -29,8 +29,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue'
<script lang="ts" setup>
import { PropType, computed } from 'vue'
import { PPTElement, PPTLineElement } from '@/types/slides'
import useCommonOperate from '@/views/Editor/Canvas/hooks/useCommonOperate'
import { OperateResizeHandlers } from '@/types/edit'
@ -38,48 +38,36 @@ import { OperateResizeHandlers } from '@/types/edit'
import BorderLine from '@/views/Editor/Canvas/Operate/BorderLine.vue'
import ResizeHandler from '@/views/Editor/Canvas/Operate/ResizeHandler.vue'
export default defineComponent({
name: 'mobile-operate',
components: {
BorderLine,
ResizeHandler,
const props = defineProps({
elementInfo: {
type: Object as PropType<Exclude<PPTElement, PPTLineElement>>,
required: true,
},
props: {
elementInfo: {
type: Object as PropType<Exclude<PPTElement, PPTLineElement>>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
setup(props) {
const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
const {
borderLines,
resizeHandlers,
textElementResizeHandlers,
} = useCommonOperate(scaleWidth, scaleHeight)
return {
rotate,
borderLines,
resizeHandlers: props.elementInfo.type === 'text' || props.elementInfo.type === 'table' ? textElementResizeHandlers : resizeHandlers,
}
canvasScale: {
type: Number,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => void>,
required: true,
},
})
const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
const {
borderLines,
resizeHandlers: _resizeHandlers,
textElementResizeHandlers,
} = useCommonOperate(scaleWidth, scaleHeight)
const resizeHandlers = props.elementInfo.type === 'text' || props.elementInfo.type === 'table' ? textElementResizeHandlers : _resizeHandlers
</script>
<style lang="scss" scoped>

View File

@ -29,8 +29,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useSlideHandler from '@/hooks/useSlideHandler'
@ -41,75 +41,56 @@ import { VIEWPORT_SIZE } from '@/configs/canvas'
import MobileThumbnails from '../MobileThumbnails.vue'
export default defineComponent({
name: 'slide-toolbar',
components: {
MobileThumbnails,
},
setup() {
const slidesStore = useSlidesStore()
const { viewportRatio, currentSlide } = storeToRefs(slidesStore)
const slidesStore = useSlidesStore()
const { viewportRatio, currentSlide } = storeToRefs(slidesStore)
const { createSlide, copyAndPasteSlide, deleteSlide, } = useSlideHandler()
const { createTextElement, createImageElement, createShapeElement } = useCreateElement()
const { createSlide, copyAndPasteSlide, deleteSlide, } = useSlideHandler()
const { createTextElement, createImageElement, createShapeElement } = useCreateElement()
const insertTextElement = () => {
const width = 400
const height = 56
const insertTextElement = () => {
const width = 400
const height = 56
createTextElement({
left: (VIEWPORT_SIZE - width) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
width,
height,
}, '<p><span style=\"font-size: 24px\">新添加文本</span></p>')
}
createTextElement({
left: (VIEWPORT_SIZE - width) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
width,
height,
}, '<p><span style=\"font-size: 24px\">新添加文本</span></p>')
}
const insertImageElement = (files: File[]) => {
if (!files || !files[0]) return
getImageDataURL(files[0]).then(dataURL => createImageElement(dataURL))
}
const insertImageElement = (files: FileList) => {
if (!files || !files[0]) return
getImageDataURL(files[0]).then(dataURL => createImageElement(dataURL))
}
const insertShapeElement = (type: 'square' | 'round') => {
const square: ShapePoolItem = {
viewBox: [200, 200],
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
}
const round: ShapePoolItem = {
viewBox: [200, 200],
path: 'M 100 0 A 50 50 0 1 1 100 200 A 50 50 0 1 1 100 0 Z',
}
const shape = { square, round }
const insertShapeElement = (type: 'square' | 'round') => {
const square: ShapePoolItem = {
viewBox: [200, 200],
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
}
const round: ShapePoolItem = {
viewBox: [200, 200],
path: 'M 100 0 A 50 50 0 1 1 100 200 A 50 50 0 1 1 100 0 Z',
}
const shape = { square, round }
const size = 200
const size = 200
createShapeElement({
left: (VIEWPORT_SIZE - size) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - size) / 2,
width: size,
height: size,
}, shape[type])
}
createShapeElement({
left: (VIEWPORT_SIZE - size) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - size) / 2,
width: size,
height: size,
}, shape[type])
}
const remark = computed(() => currentSlide.value?.remark || '')
const remark = computed(() => currentSlide.value?.remark || '')
const handleInputMark = (e: Event) => {
const value = (e.target as HTMLTextAreaElement).value
slidesStore.updateSlide({ remark: value })
}
return {
remark,
createSlide,
copyAndPasteSlide,
deleteSlide,
insertTextElement,
insertImageElement,
insertShapeElement,
handleInputMark,
}
},
})
const handleInputMark = (e: Event) => {
const value = (e.target as HTMLTextAreaElement).value
slidesStore.updateSlide({ remark: value })
}
</script>
<style lang="scss" scoped>

View File

@ -39,8 +39,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, PropType, ref, watchEffect } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, PropType, ref, watchEffect } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { PPTElement } from '@/types/slides'
@ -58,92 +58,65 @@ import SlideToolbar from './SlideToolbar.vue'
import ElementToolbar from './ElementToolbar.vue'
import Header from './Header.vue'
export default defineComponent({
name: 'mobile-editor',
components: {
AlignmentLine,
MobileEditableElement,
MobileOperate,
SlideToolbar,
ElementToolbar,
Header,
},
props: {
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
},
setup() {
const slidesStore = useSlidesStore()
const mainStore = useMainStore()
const { slideIndex, currentSlide, viewportRatio } = storeToRefs(slidesStore)
const { activeElementIdList, handleElement } = storeToRefs(mainStore)
const contentRef = ref<HTMLElement>()
const alignmentLines = ref<AlignmentLineProps[]>([])
const background = computed(() => currentSlide.value.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)
const canvasScale = computed(() => {
if (!contentRef.value) return 1
const contentWidth = contentRef.value.clientWidth
const contentheight = contentRef.value.clientHeight
const contentRatio = contentheight / contentWidth
if (contentRatio >= viewportRatio.value) return (contentWidth - 20) / VIEWPORT_SIZE
return (contentheight - 20) / viewportRatio.value / VIEWPORT_SIZE
})
onMounted(() => {
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
if (slideIndex.value !== 0) slidesStore.updateSlideIndex(0)
})
const viewportStyles = computed(() => ({
width: VIEWPORT_SIZE * canvasScale.value + 'px',
height: VIEWPORT_SIZE * viewportRatio.value * canvasScale.value + 'px',
}))
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
watchEffect(setLocalElementList)
const { dragElement } = useDragElement(elementList, alignmentLines, canvasScale)
const { scaleElement } = useScaleElement(elementList, alignmentLines, canvasScale)
const selectElement = (e: TouchEvent, element: PPTElement, startMove = true) => {
if (!activeElementIdList.value.includes(element.id)) {
mainStore.setActiveElementIdList([element.id])
mainStore.setHandleElementId(element.id)
}
if (startMove) dragElement(e, element)
}
const handleClickBlankArea = () => {
mainStore.setActiveElementIdList([])
}
return {
contentRef,
slideIndex,
elementList,
canvasScale,
viewportStyles,
backgroundStyle,
activeElementIdList,
alignmentLines,
selectElement,
handleClickBlankArea,
scaleElement,
handleElement,
}
defineProps({
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
})
const slidesStore = useSlidesStore()
const mainStore = useMainStore()
const { slideIndex, currentSlide, viewportRatio } = storeToRefs(slidesStore)
const { activeElementIdList, handleElement } = storeToRefs(mainStore)
const contentRef = ref<HTMLElement>()
const alignmentLines = ref<AlignmentLineProps[]>([])
const background = computed(() => currentSlide.value.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)
const canvasScale = computed(() => {
if (!contentRef.value) return 1
const contentWidth = contentRef.value.clientWidth
const contentheight = contentRef.value.clientHeight
const contentRatio = contentheight / contentWidth
if (contentRatio >= viewportRatio.value) return (contentWidth - 20) / VIEWPORT_SIZE
return (contentheight - 20) / viewportRatio.value / VIEWPORT_SIZE
})
onMounted(() => {
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
if (slideIndex.value !== 0) slidesStore.updateSlideIndex(0)
})
const viewportStyles = computed(() => ({
width: VIEWPORT_SIZE * canvasScale.value + 'px',
height: VIEWPORT_SIZE * viewportRatio.value * canvasScale.value + 'px',
}))
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
watchEffect(setLocalElementList)
const { dragElement } = useDragElement(elementList, alignmentLines, canvasScale)
const { scaleElement } = useScaleElement(elementList, alignmentLines, canvasScale)
const selectElement = (e: TouchEvent, element: PPTElement, startMove = true) => {
if (!activeElementIdList.value.includes(element.id)) {
mainStore.setActiveElementIdList([element.id])
mainStore.setHandleElementId(element.id)
}
if (startMove) dragElement(e, element)
}
const handleClickBlankArea = () => {
mainStore.setActiveElementIdList([])
}
</script>
<style lang="scss" scoped>

View File

@ -51,8 +51,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, PropType, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, PropType, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { Mode } from '@/types/mobile'
@ -60,89 +60,70 @@ import { Mode } from '@/types/mobile'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
import MobileThumbnails from './MobileThumbnails.vue'
export default defineComponent({
name: 'mobile-player',
components: {
ThumbnailSlide,
MobileThumbnails,
},
props: {
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
},
setup() {
const slidesStore = useSlidesStore()
const { slides, slideIndex, currentSlide, viewportRatio } = storeToRefs(slidesStore)
const toolVisible = ref(false)
const playerSize = ref({ width: 0, height: 0 })
onMounted(() => {
if (slideIndex.value !== 0) slidesStore.updateSlideIndex(0)
playerSize.value = {
width: document.body.clientHeight,
height: document.body.clientWidth,
}
})
const slideSize = computed(() => {
const playerRatio = playerSize.value.height / playerSize.value.width
let slideWidth = 0
let slideHeight = 0
if (playerRatio >= viewportRatio.value) {
slideWidth = playerSize.value.width
slideHeight = slideWidth * viewportRatio.value
}
else {
slideHeight = playerSize.value.height
slideWidth = slideHeight / viewportRatio.value
}
return {
width: slideWidth,
height: slideHeight,
}
})
const touchInfo = ref<{ x: number; y: number; } | null>(null)
const touchStartListener = (e: TouchEvent) => {
touchInfo.value = {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY,
}
}
const touchEndListener = (e: TouchEvent) => {
if (!touchInfo.value) return
const offsetY = Math.abs(touchInfo.value.y - e.changedTouches[0].pageY)
const offsetX = e.changedTouches[0].pageX - touchInfo.value.x
if ( Math.abs(offsetX) > offsetY && Math.abs(offsetX) > 50 ) {
touchInfo.value = null
if (offsetX < 0 && slideIndex.value > 0) slidesStore.updateSlideIndex(slideIndex.value - 1)
if (offsetX > 0 && slideIndex.value < slides.value.length - 1) slidesStore.updateSlideIndex(slideIndex.value + 1)
}
}
return {
slides,
slideIndex,
currentSlide,
playerSize,
slideSize,
toolVisible,
touchStartListener,
touchEndListener,
}
defineProps({
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
})
const slidesStore = useSlidesStore()
const { slides, slideIndex, currentSlide, viewportRatio } = storeToRefs(slidesStore)
const toolVisible = ref(false)
const playerSize = ref({ width: 0, height: 0 })
onMounted(() => {
if (slideIndex.value !== 0) slidesStore.updateSlideIndex(0)
playerSize.value = {
width: document.body.clientHeight,
height: document.body.clientWidth,
}
})
const slideSize = computed(() => {
const playerRatio = playerSize.value.height / playerSize.value.width
let slideWidth = 0
let slideHeight = 0
if (playerRatio >= viewportRatio.value) {
slideWidth = playerSize.value.width
slideHeight = slideWidth * viewportRatio.value
}
else {
slideHeight = playerSize.value.height
slideWidth = slideHeight / viewportRatio.value
}
return {
width: slideWidth,
height: slideHeight,
}
})
const touchInfo = ref<{ x: number; y: number; } | null>(null)
const touchStartListener = (e: TouchEvent) => {
touchInfo.value = {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY,
}
}
const touchEndListener = (e: TouchEvent) => {
if (!touchInfo.value) return
const offsetY = Math.abs(touchInfo.value.y - e.changedTouches[0].pageY)
const offsetX = e.changedTouches[0].pageX - touchInfo.value.x
if ( Math.abs(offsetX) > offsetY && Math.abs(offsetX) > 50 ) {
touchInfo.value = null
if (offsetX < 0 && slideIndex.value > 0) slidesStore.updateSlideIndex(slideIndex.value - 1)
if (offsetX > 0 && slideIndex.value < slides.value.length - 1) slidesStore.updateSlideIndex(slideIndex.value + 1)
}
}
</script>
<style lang="scss" scoped>

View File

@ -17,8 +17,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, onMounted, ref } from 'vue'
<script lang="ts" setup>
import { PropType, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useLoadSlides from '@/hooks/useLoadSlides'
@ -26,36 +26,22 @@ import { Mode } from '@/types/mobile'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'mobile-preview',
components: {
ThumbnailSlide,
const props = defineProps({
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
props: {
changeMode: {
type: Function as PropType<(mode: Mode) => void>,
required: true,
},
},
setup() {
const { slides } = storeToRefs(useSlidesStore())
const { slidesLoadLimit } = useLoadSlides()
})
const mobileRef = ref<HTMLElement>()
const screenWidth = ref(0)
const { slides } = storeToRefs(useSlidesStore())
const { slidesLoadLimit } = useLoadSlides()
onMounted(() => {
if (!mobileRef.value) return
screenWidth.value = mobileRef.value.clientWidth
})
const mobileRef = ref<HTMLElement>()
const screenWidth = ref(0)
return {
slides,
slidesLoadLimit,
mobileRef,
screenWidth,
}
},
onMounted(() => {
if (!mobileRef.value) return
screenWidth.value = mobileRef.value.clientWidth
})
</script>

View File

@ -13,36 +13,20 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import useLoadSlides from '@/hooks/useLoadSlides'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'mobile-thumbnails',
components: {
ThumbnailSlide,
},
setup() {
const slidesStore = useSlidesStore()
const { slides, slideIndex } = storeToRefs(slidesStore)
const slidesStore = useSlidesStore()
const { slides, slideIndex } = storeToRefs(slidesStore)
const { slidesLoadLimit } = useLoadSlides()
const changeSlideIndex = (index: number) => {
slidesStore.updateSlideIndex(index)
}
return {
slides,
slideIndex,
slidesLoadLimit,
changeSlideIndex,
}
},
})
const { slidesLoadLimit } = useLoadSlides()
const changeSlideIndex = (index: number) => {
slidesStore.updateSlideIndex(index)
}
</script>
<style lang="scss" scoped>

View File

@ -7,35 +7,25 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { Mode } from '@/types/mobile'
import MobileEditor from './MobileEditor/index.vue'
import MobilePlayer from './MobilePlayer.vue'
import MobilePreview from './MobilePreview.vue'
export default defineComponent({
name: 'mobile',
setup() {
const mode = ref<Mode>('preview')
const mode = ref<Mode>('preview')
const changeMode = (_mode: Mode) => mode.value = _mode
const currentComponent = computed(() => {
const componentMap = {
'editor': MobileEditor,
'player': MobilePlayer,
'preview': MobilePreview,
}
return componentMap[mode.value] || null
})
const changeMode = (_mode: Mode) => mode.value = _mode
return {
currentComponent,
changeMode,
}
},
const currentComponent = computed(() => {
const componentMap = {
'editor': MobileEditor,
'player': MobilePlayer,
'preview': MobilePreview,
}
return componentMap[mode.value] || null
})
</script>

View File

@ -58,8 +58,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue'
<script lang="ts" setup>
import { PropType, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { ContextmenuItem } from '@/components/Contextmenu/types'
@ -73,126 +73,93 @@ import ScreenSlideList from './ScreenSlideList.vue'
import SlideThumbnails from './SlideThumbnails.vue'
import WritingBoardTool from './WritingBoardTool.vue'
export default defineComponent({
name: 'screen',
components: {
ScreenSlideList,
SlideThumbnails,
WritingBoardTool,
},
props: {
changeViewMode: {
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
required: true,
},
},
setup(props) {
const { slides, slideIndex } = storeToRefs(useSlidesStore())
const {
autoPlayTimer,
autoPlay,
closeAutoPlay,
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
execPrev,
execNext,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize()
const { exitScreening } = useScreening()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const rightToolsVisible = ref(false)
const writingBoardToolVisible = ref(false)
const slideThumbnailModelVisible = ref(false)
const laserPen = ref(false)
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '上一页',
subText: '↑ ←',
disable: slideIndex.value <= 0,
handler: () => turnPrevSlide(),
},
{
text: '下一页',
subText: '↓ →',
disable: slideIndex.value >= slides.value.length - 1,
handler: () => turnNextSlide(),
},
{
text: '第一页',
disable: slideIndex.value === 0,
handler: () => turnSlideToIndex(0),
},
{
text: '最后一页',
disable: slideIndex.value === slides.value.length - 1,
handler: () => turnSlideToIndex(slides.value.length - 1),
},
{ divider: true },
{
text: '显示工具栏',
handler: () => rightToolsVisible.value = true,
},
{
text: '查看所有幻灯片',
handler: () => slideThumbnailModelVisible.value = true,
},
{
text: '画笔工具',
handler: () => writingBoardToolVisible.value = true,
},
{
text: '演讲者视图',
handler: () => props.changeViewMode('presenter'),
},
{ divider: true },
{
text: autoPlayTimer.value ? '取消自动放映' : '自动放映',
handler: autoPlayTimer.value ? closeAutoPlay : autoPlay,
},
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
},
]
}
return {
slides,
slideIndex,
slideWidth,
slideHeight,
mousewheelListener,
touchStartListener,
touchEndListener,
animationIndex,
contextmenus,
execPrev,
execNext,
turnSlideToIndex,
turnSlideToId,
slideThumbnailModelVisible,
writingBoardToolVisible,
rightToolsVisible,
fullscreenState,
exitScreening,
enterFullscreen,
manualExitFullscreen,
laserPen,
}
const props = defineProps({
changeViewMode: {
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
required: true,
},
})
const { slides, slideIndex } = storeToRefs(useSlidesStore())
const {
autoPlayTimer,
autoPlay,
closeAutoPlay,
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
execPrev,
execNext,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize()
const { exitScreening } = useScreening()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const rightToolsVisible = ref(false)
const writingBoardToolVisible = ref(false)
const slideThumbnailModelVisible = ref(false)
const laserPen = ref(false)
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '上一页',
subText: '↑ ←',
disable: slideIndex.value <= 0,
handler: () => turnPrevSlide(),
},
{
text: '下一页',
subText: '↓ →',
disable: slideIndex.value >= slides.value.length - 1,
handler: () => turnNextSlide(),
},
{
text: '第一页',
disable: slideIndex.value === 0,
handler: () => turnSlideToIndex(0),
},
{
text: '最后一页',
disable: slideIndex.value === slides.value.length - 1,
handler: () => turnSlideToIndex(slides.value.length - 1),
},
{ divider: true },
{
text: '显示工具栏',
handler: () => rightToolsVisible.value = true,
},
{
text: '查看所有幻灯片',
handler: () => slideThumbnailModelVisible.value = true,
},
{
text: '画笔工具',
handler: () => writingBoardToolVisible.value = true,
},
{
text: '演讲者视图',
handler: () => props.changeViewMode('presenter'),
},
{ divider: true },
{
text: autoPlayTimer.value ? '取消自动放映' : '自动放映',
handler: autoPlayTimer.value ? closeAutoPlay : autoPlay,
},
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
},
]
}
</script>
<style lang="scss" scoped>

View File

@ -71,8 +71,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, nextTick, ref, watch, PropType } from 'vue'
<script lang="ts" setup>
import { computed, nextTick, ref, watch, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { ContextmenuItem } from '@/components/Contextmenu/types'
@ -88,142 +88,105 @@ import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
import ScreenSlideList from './ScreenSlideList.vue'
import WritingBoardTool from './WritingBoardTool.vue'
export default defineComponent({
name: 'presenter-view',
components: {
ScreenSlideList,
ThumbnailSlide,
WritingBoardTool,
},
props: {
changeViewMode: {
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
required: true,
},
},
setup(props) {
const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
const slideListWrapRef = ref<HTMLElement>()
const thumbnailsRef = ref<HTMLElement>()
const writingBoardToolVisible = ref(false)
const laserPen = ref(false)
const {
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
const { exitScreening } = useScreening()
const { slidesLoadLimit } = useLoadSlides()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const remarkFontSize = ref(16)
const currentSlideRemark = computed(() => {
return parseText2Paragraphs(currentSlide.value.remark || '无备注')
})
const handleMousewheelThumbnails = (e: WheelEvent) => {
if (!thumbnailsRef.value) return
thumbnailsRef.value.scrollBy(e.deltaY, 0)
}
const setRemarkFontSize = (fontSize: number) => {
if (fontSize < 12 || fontSize > 40) return
remarkFontSize.value = fontSize
}
watch(slideIndex, () => {
nextTick(() => {
if (!thumbnailsRef.value) return
const activeThumbnailRef: HTMLElement | null = thumbnailsRef.value.querySelector('.thumbnail.active')
if (!activeThumbnailRef) return
const width = thumbnailsRef.value.offsetWidth
const offsetLeft = activeThumbnailRef.offsetLeft
thumbnailsRef.value.scrollTo({ left: offsetLeft - width / 2, behavior: 'smooth' })
})
})
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '上一页',
subText: '↑ ←',
disable: slideIndex.value <= 0,
handler: () => turnPrevSlide(),
},
{
text: '下一页',
subText: '↓ →',
disable: slideIndex.value >= slides.value.length - 1,
handler: () => turnNextSlide(),
},
{
text: '第一页',
disable: slideIndex.value === 0,
handler: () => turnSlideToIndex(0),
},
{
text: '最后一页',
disable: slideIndex.value === slides.value.length - 1,
handler: () => turnSlideToIndex(slides.value.length - 1),
},
{ divider: true },
{
text: '画笔工具',
handler: () => writingBoardToolVisible.value = true,
},
{
text: '普通视图',
handler: () => props.changeViewMode('base'),
},
{ divider: true },
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
},
]
}
return {
slides,
slideIndex,
viewportRatio,
remarkFontSize,
currentSlideRemark,
setRemarkFontSize,
slideListWrapRef,
thumbnailsRef,
slideWidth,
slideHeight,
animationIndex,
turnSlideToId,
mousewheelListener,
touchStartListener,
touchEndListener,
turnSlideToIndex,
contextmenus,
slidesLoadLimit,
handleMousewheelThumbnails,
exitScreening,
fullscreenState,
enterFullscreen,
manualExitFullscreen,
writingBoardToolVisible,
laserPen,
}
const props = defineProps({
changeViewMode: {
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
required: true,
},
})
const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
const slideListWrapRef = ref<HTMLElement>()
const thumbnailsRef = ref<HTMLElement>()
const writingBoardToolVisible = ref(false)
const laserPen = ref(false)
const {
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
const { exitScreening } = useScreening()
const { slidesLoadLimit } = useLoadSlides()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const remarkFontSize = ref(16)
const currentSlideRemark = computed(() => {
return parseText2Paragraphs(currentSlide.value.remark || '无备注')
})
const handleMousewheelThumbnails = (e: WheelEvent) => {
if (!thumbnailsRef.value) return
thumbnailsRef.value.scrollBy(e.deltaY, 0)
}
const setRemarkFontSize = (fontSize: number) => {
if (fontSize < 12 || fontSize > 40) return
remarkFontSize.value = fontSize
}
watch(slideIndex, () => {
nextTick(() => {
if (!thumbnailsRef.value) return
const activeThumbnailRef: HTMLElement | null = thumbnailsRef.value.querySelector('.thumbnail.active')
if (!activeThumbnailRef) return
const width = thumbnailsRef.value.offsetWidth
const offsetLeft = activeThumbnailRef.offsetLeft
thumbnailsRef.value.scrollTo({ left: offsetLeft - width / 2, behavior: 'smooth' })
})
})
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '上一页',
subText: '↑ ←',
disable: slideIndex.value <= 0,
handler: () => turnPrevSlide(),
},
{
text: '下一页',
subText: '↓ →',
disable: slideIndex.value >= slides.value.length - 1,
handler: () => turnNextSlide(),
},
{
text: '第一页',
disable: slideIndex.value === 0,
handler: () => turnSlideToIndex(0),
},
{
text: '最后一页',
disable: slideIndex.value === slides.value.length - 1,
handler: () => turnSlideToIndex(slides.value.length - 1),
},
{ divider: true },
{
text: '画笔工具',
handler: () => writingBoardToolVisible.value = true,
},
{
text: '普通视图',
handler: () => props.changeViewMode('base'),
},
{ divider: true },
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
},
]
}
</script>
<style lang="scss" scoped>

View File

@ -19,8 +19,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { ElementTypes, PPTElement } from '@/types/slides'
@ -35,92 +35,81 @@ import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexE
import ScreenVideoElement from '@/views/components/element/VideoElement/ScreenVideoElement.vue'
import ScreenAudioElement from '@/views/components/element/AudioElement/ScreenAudioElement.vue'
export default defineComponent({
name: 'screen-element',
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
elementIndex: {
type: Number,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
turnSlideToId: {
type: Function as PropType<(id: string) => void>,
required: true,
},
manualExitFullscreen: {
type: Function as PropType<() => void>,
required: true,
},
const props = defineProps({
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
setup(props) {
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: BaseImageElement,
[ElementTypes.TEXT]: BaseTextElement,
[ElementTypes.SHAPE]: BaseShapeElement,
[ElementTypes.LINE]: BaseLineElement,
[ElementTypes.CHART]: ScreenChartElement,
[ElementTypes.TABLE]: BaseTableElement,
[ElementTypes.LATEX]: BaseLatexElement,
[ElementTypes.VIDEO]: ScreenVideoElement,
[ElementTypes.AUDIO]: ScreenAudioElement,
}
return elementTypeMap[props.elementInfo.type] || null
})
const { formatedAnimations, theme } = storeToRefs(useSlidesStore())
//
const needWaitAnimation = computed(() => {
//
const elementIndexInAnimation = formatedAnimations.value.findIndex(item => {
const elIds = item.animations.map(item => item.elId)
return elIds.includes(props.elementInfo.id)
})
//
if (elementIndexInAnimation === -1) return false
//
// 退退
if (elementIndexInAnimation < props.animationIndex) return false
//
//
const firstAnimation = formatedAnimations.value[elementIndexInAnimation].animations.find(item => item.elId === props.elementInfo.id)
if (firstAnimation?.type === 'in') return true
return false
})
//
const openLink = () => {
const link = props.elementInfo.link
if (!link) return
if (link.type === 'web') {
props.manualExitFullscreen()
window.open(link.target)
}
else if (link.type === 'slide') {
props.turnSlideToId(link.target)
}
}
return {
currentElementComponent,
needWaitAnimation,
theme,
openLink,
}
elementIndex: {
type: Number,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
turnSlideToId: {
type: Function as PropType<(id: string) => void>,
required: true,
},
manualExitFullscreen: {
type: Function as PropType<() => void>,
required: true,
},
})
const currentElementComponent = computed(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: BaseImageElement,
[ElementTypes.TEXT]: BaseTextElement,
[ElementTypes.SHAPE]: BaseShapeElement,
[ElementTypes.LINE]: BaseLineElement,
[ElementTypes.CHART]: ScreenChartElement,
[ElementTypes.TABLE]: BaseTableElement,
[ElementTypes.LATEX]: BaseLatexElement,
[ElementTypes.VIDEO]: ScreenVideoElement,
[ElementTypes.AUDIO]: ScreenAudioElement,
}
return elementTypeMap[props.elementInfo.type] || null
})
const { formatedAnimations, theme } = storeToRefs(useSlidesStore())
//
const needWaitAnimation = computed(() => {
//
const elementIndexInAnimation = formatedAnimations.value.findIndex(item => {
const elIds = item.animations.map(item => item.elId)
return elIds.includes(props.elementInfo.id)
})
//
if (elementIndexInAnimation === -1) return false
//
// 退退
if (elementIndexInAnimation < props.animationIndex) return false
//
//
const firstAnimation = formatedAnimations.value[elementIndexInAnimation].animations.find(item => item.elId === props.elementInfo.id)
if (firstAnimation?.type === 'in') return true
return false
})
//
const openLink = () => {
const link = props.elementInfo.link
if (!link) return
if (link.type === 'web') {
props.manualExitFullscreen()
window.open(link.target)
}
else if (link.type === 'slide') {
props.turnSlideToId(link.target)
}
}
</script>
<style lang="scss" scoped>

View File

@ -20,8 +20,8 @@
</div>
</template>
<script lang="ts">
import { computed, PropType, defineComponent, provide } from 'vue'
<script lang="ts" setup>
import { computed, PropType, provide } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { Slide } from '@/types/slides'
@ -31,49 +31,36 @@ import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
import ScreenElement from './ScreenElement.vue'
export default defineComponent({
name: 'screen-slide',
components: {
ScreenElement,
const props = defineProps({
slide: {
type: Object as PropType<Slide>,
required: true,
},
props: {
slide: {
type: Object as PropType<Slide>,
required: true,
},
scale: {
type: Number,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
turnSlideToId: {
type: Function as PropType<(id: string) => void>,
required: true,
},
manualExitFullscreen: {
type: Function as PropType<() => void>,
required: true,
},
scale: {
type: Number,
required: true,
},
setup(props) {
const { viewportRatio } = storeToRefs(useSlidesStore())
const background = computed(() => props.slide.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)
const slideId = computed(() => props.slide.id)
provide(injectKeySlideId, slideId)
return {
backgroundStyle,
VIEWPORT_SIZE,
viewportRatio,
}
animationIndex: {
type: Number,
required: true,
},
turnSlideToId: {
type: Function as PropType<(id: string) => void>,
required: true,
},
manualExitFullscreen: {
type: Function as PropType<() => void>,
required: true,
},
})
const { viewportRatio } = storeToRefs(useSlidesStore())
const background = computed(() => props.slide.background)
const { backgroundStyle } = useSlideBackgroundStyle(background)
const slideId = computed(() => props.slide.id)
provide(injectKeySlideId, slideId)
</script>
<style lang="scss" scoped>

Some files were not shown because too many files have changed in this diff Show More