mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
perf: 代码优化&注释
This commit is contained in:
parent
1d25d85621
commit
d4a8d41394
@ -54,6 +54,10 @@ export interface ImageElementFilters {
|
||||
'hue-rotate'?: string;
|
||||
'opacity'?: string;
|
||||
}
|
||||
export interface ImageElementClip {
|
||||
range: [[number, number], [number, number]];
|
||||
shape: string;
|
||||
}
|
||||
export interface PPTImageElement {
|
||||
type: 'image';
|
||||
id: string;
|
||||
@ -68,10 +72,7 @@ export interface PPTImageElement {
|
||||
rotate?: number;
|
||||
outline?: PPTElementOutline;
|
||||
filters?: ImageElementFilters;
|
||||
clip?: {
|
||||
range: [[number, number], [number, number]];
|
||||
shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
|
||||
};
|
||||
clip?: ImageElementClip;
|
||||
flip?: ImageOrShapeFlip;
|
||||
shadow?: PPTElementShadow;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
transform: `scale(${scale})`,
|
||||
}"
|
||||
>
|
||||
<div class="background" :style="{ ...backgroundStyle }"></div>
|
||||
<div class="background" :style="backgroundStyle"></div>
|
||||
<ThumbnailElement
|
||||
v-for="(element, index) in slide.elements"
|
||||
:key="element.id"
|
||||
|
@ -101,6 +101,7 @@ export default defineComponent({
|
||||
|
||||
onMounted(renderChart)
|
||||
|
||||
// 更新主题配色:通过主题色的色相旋转,计算出一系列的颜色作为主题配色
|
||||
const updateTheme = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
@ -116,15 +117,19 @@ export default defineComponent({
|
||||
}
|
||||
chartRef.value.style.setProperty(`--theme-color-${i + 1}`, tinycolor(_hsla).toRgbString())
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.themeColor, updateTheme)
|
||||
onMounted(updateTheme)
|
||||
|
||||
// 更新网格颜色,包括坐标的文字部分
|
||||
const updateGridColor = () => {
|
||||
if (!chartRef.value) return
|
||||
if (props.gridColor) chartRef.value.style.setProperty(`--grid-color`, props.gridColor)
|
||||
}
|
||||
|
||||
watch([
|
||||
() => props.themeColor,
|
||||
() => props.gridColor,
|
||||
], updateTheme)
|
||||
onMounted(updateTheme)
|
||||
watch(() => props.gridColor, updateGridColor)
|
||||
onMounted(updateGridColor)
|
||||
|
||||
return {
|
||||
slideScale,
|
||||
|
@ -15,7 +15,7 @@
|
||||
:d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'"
|
||||
></path>
|
||||
</SvgWrapper>
|
||||
</template>
|
||||
|
@ -16,26 +16,7 @@
|
||||
transform: flipStyle,
|
||||
}"
|
||||
>
|
||||
<ImageRectOutline
|
||||
v-if="clipShape.type === 'rect'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:radius="clipShape.radius"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImageEllipseOutline
|
||||
v-else-if="clipShape.type === 'ellipse'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImagePolygonOutline
|
||||
v-else-if="clipShape.type === 'polygon'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:createPath="clipShape.createPath"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImageOutline :elementInfo="elementInfo" />
|
||||
|
||||
<div class="image-content" :style="{ clipPath: clipShape.style }">
|
||||
<img
|
||||
@ -57,23 +38,18 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
|
||||
import { PPTImageElement } from '@/types/slides'
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
|
||||
import ImageRectOutline from './ImageRectOutline.vue'
|
||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||
|
||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import useElementFlip from '@/views/components/element/hooks/useElementFlip'
|
||||
import useClipImage from './useClipImage'
|
||||
import useFilter from './useFilter'
|
||||
|
||||
import ImageOutline from './ImageOutline/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'base-element-image',
|
||||
components: {
|
||||
ImageRectOutline,
|
||||
ImageEllipseOutline,
|
||||
ImagePolygonOutline,
|
||||
ImageOutline,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
@ -82,59 +58,24 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const imgPosition = computed(() => {
|
||||
if (!props.elementInfo || !props.elementInfo.clip) {
|
||||
return {
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
}
|
||||
|
||||
const [start, end] = props.elementInfo.clip.range
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return {
|
||||
left: -left + '%',
|
||||
top: -top + '%',
|
||||
width: 100 / widthScale + '%',
|
||||
height: 100 / heightScale + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const clipShape = computed(() => {
|
||||
if (!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
||||
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
||||
|
||||
return CLIPPATHS[shape]
|
||||
})
|
||||
|
||||
const filter = computed(() => {
|
||||
if (!props.elementInfo.filters) return ''
|
||||
let filter = ''
|
||||
for (const key of Object.keys(props.elementInfo.filters)) {
|
||||
filter += `${key}(${props.elementInfo.filters[key]}) `
|
||||
}
|
||||
return filter
|
||||
})
|
||||
|
||||
const shadow = computed(() => props.elementInfo.shadow)
|
||||
const { shadowStyle } = useElementShadow(shadow)
|
||||
|
||||
const flip = computed(() => props.elementInfo.flip)
|
||||
const { flipStyle } = useElementFlip(flip)
|
||||
|
||||
const clip = computed(() => props.elementInfo.clip)
|
||||
const { clipShape, imgPosition } = useClipImage(clip)
|
||||
|
||||
const filters = computed(() => props.elementInfo.filters)
|
||||
const { filter } = useFilter(filters)
|
||||
|
||||
return {
|
||||
imgPosition,
|
||||
clipShape,
|
||||
filter,
|
||||
flipStyle,
|
||||
shadowStyle,
|
||||
clipShape,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -95,12 +95,6 @@ export default defineComponent({
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
const ctrlOrShiftKeyActive = computed<boolean>(() => store.getters.ctrlOrShiftKeyActive)
|
||||
|
||||
const topImgWrapperPosition = reactive({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
const clipWrapperPositionStyle = reactive({
|
||||
top: '0',
|
||||
left: '0',
|
||||
@ -108,6 +102,7 @@ export default defineComponent({
|
||||
const isSettingClipRange = ref(false)
|
||||
const currentRange = ref<ImageClipDataRange | null>(null)
|
||||
|
||||
// 获取裁剪区域信息(裁剪区域占原图的宽高比例,处在原图中的位置)
|
||||
const getClipDataTransformInfo = () => {
|
||||
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
|
||||
|
||||
@ -119,6 +114,7 @@ export default defineComponent({
|
||||
return { widthScale, heightScale, left, top }
|
||||
}
|
||||
|
||||
// 底层图片位置大小(遮罩区域图片)
|
||||
const imgPosition = computed(() => {
|
||||
const { widthScale, heightScale, left, top } = getClipDataTransformInfo()
|
||||
return {
|
||||
@ -129,6 +125,7 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
// 底层图片位置大小样式(遮罩区域图片)
|
||||
const bottomImgPositionStyle = computed(() => {
|
||||
return {
|
||||
top: imgPosition.value.top + '%',
|
||||
@ -138,6 +135,15 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
// 顶层图片容器位置大小(裁剪高亮区域)
|
||||
const topImgWrapperPosition = reactive({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
// 顶层图片容器位置大小样式(裁剪高亮区域)
|
||||
const topImgWrapperPositionStyle = computed(() => {
|
||||
return {
|
||||
top: topImgWrapperPosition.top + '%',
|
||||
@ -147,6 +153,7 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
// 顶层图片位置大小样式(裁剪区域图片)
|
||||
const topImgPositionStyle = computed(() => {
|
||||
const bottomWidth = imgPosition.value.width
|
||||
const bottomHeight = imgPosition.value.height
|
||||
@ -164,6 +171,7 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化裁剪位置信息
|
||||
const initClipPosition = () => {
|
||||
const { left, top } = getClipDataTransformInfo()
|
||||
topImgWrapperPosition.left = left
|
||||
@ -175,6 +183,7 @@ export default defineComponent({
|
||||
clipWrapperPositionStyle.left = -left + '%'
|
||||
}
|
||||
|
||||
// 执行裁剪:计算裁剪后的图片位置大小和裁剪信息,并将数据同步出去
|
||||
const handleClip = () => {
|
||||
if (isSettingClipRange.value) return
|
||||
|
||||
@ -199,6 +208,7 @@ export default defineComponent({
|
||||
emit('clip', clipedEmitData)
|
||||
}
|
||||
|
||||
// 快捷键监听:回车确认裁剪
|
||||
const keyboardListener = (e: KeyboardEvent) => {
|
||||
const key = e.key.toUpperCase()
|
||||
if (key === KEYS.ENTER) handleClip()
|
||||
@ -212,7 +222,8 @@ export default defineComponent({
|
||||
document.removeEventListener('keydown', keyboardListener)
|
||||
})
|
||||
|
||||
const getRange = () => {
|
||||
// 计算并更新裁剪区域范围数据
|
||||
const updateRange = () => {
|
||||
const retPosition = {
|
||||
left: parseInt(topImgPositionStyle.value.left),
|
||||
top: parseInt(topImgPositionStyle.value.top),
|
||||
@ -235,6 +246,7 @@ export default defineComponent({
|
||||
currentRange.value = [start, end]
|
||||
}
|
||||
|
||||
// 移动裁剪区域
|
||||
const moveClipRange = (e: MouseEvent) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
@ -261,7 +273,6 @@ export default defineComponent({
|
||||
let targetLeft = originPositopn.left + moveX
|
||||
let targetTop = originPositopn.top + moveY
|
||||
|
||||
// 范围限制
|
||||
if (targetLeft < 0) targetLeft = 0
|
||||
else if (targetLeft + originPositopn.width > bottomPosition.width) {
|
||||
targetLeft = bottomPosition.width - originPositopn.width
|
||||
@ -280,7 +291,7 @@ export default defineComponent({
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
updateRange()
|
||||
|
||||
setTimeout(() => {
|
||||
isSettingClipRange.value = false
|
||||
@ -288,6 +299,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// 缩放裁剪区域
|
||||
const scaleClipRange = (e: MouseEvent, type: ScaleClipRangeType) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
@ -323,7 +335,6 @@ export default defineComponent({
|
||||
|
||||
let targetLeft, targetTop, targetWidth, targetHeight
|
||||
|
||||
// 根据不同缩放点,计算目标大小和位置,同时做大小和范围的限制
|
||||
if (type === 't-l') {
|
||||
if (originPositopn.left + moveX < 0) {
|
||||
moveX = -originPositopn.left
|
||||
@ -408,7 +419,7 @@ export default defineComponent({
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
updateRange()
|
||||
|
||||
setTimeout(() => isSettingClipRange.value = false, 0)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
:ry="height / 2"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'"
|
||||
></ellipse>
|
||||
</SvgWrapper>
|
||||
</template>
|
@ -15,7 +15,7 @@
|
||||
:d="createPath(width, height)"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'"
|
||||
></path>
|
||||
</SvgWrapper>
|
||||
</template>
|
@ -18,7 +18,7 @@
|
||||
:height="height"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'"
|
||||
></rect>
|
||||
</SvgWrapper>
|
||||
</template>
|
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="image-outline">
|
||||
<ImageRectOutline
|
||||
v-if="clipShape.type === 'rect'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:radius="clipShape.radius"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImageEllipseOutline
|
||||
v-else-if="clipShape.type === 'ellipse'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImagePolygonOutline
|
||||
v-else-if="clipShape.type === 'polygon'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
:createPath="clipShape.createPath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import { PPTImageElement } from '@/types/slides'
|
||||
import useClipImage from '../useClipImage'
|
||||
|
||||
import ImageRectOutline from './ImageRectOutline.vue'
|
||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'image-outline',
|
||||
components: {
|
||||
ImageRectOutline,
|
||||
ImageEllipseOutline,
|
||||
ImagePolygonOutline,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTImageElement>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const clip = computed(() => props.elementInfo.clip)
|
||||
const { clipShape } = useClipImage(clip)
|
||||
|
||||
return {
|
||||
clipShape,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -20,7 +20,7 @@
|
||||
:top="elementInfo.top"
|
||||
:left="elementInfo.left"
|
||||
:clipPath="clipShape.style"
|
||||
@clip="range => clip(range)"
|
||||
@clip="range => handleClip(range)"
|
||||
/>
|
||||
<div
|
||||
class="element-content"
|
||||
@ -31,26 +31,7 @@
|
||||
transform: flipStyle,
|
||||
}"
|
||||
>
|
||||
<ImageRectOutline
|
||||
v-if="clipShape.type === 'rect'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:radius="clipShape.radius"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImageEllipseOutline
|
||||
v-else-if="clipShape.type === 'ellipse'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<ImagePolygonOutline
|
||||
v-else-if="clipShape.type === 'polygon'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
:createPath="clipShape.createPath"
|
||||
/>
|
||||
<ImageOutline :elementInfo="elementInfo" />
|
||||
|
||||
<div class="image-content" :style="{ clipPath: clipShape.style }">
|
||||
<img
|
||||
@ -74,23 +55,20 @@
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import { MutationTypes, useStore } from '@/store'
|
||||
import { PPTImageElement } from '@/types/slides'
|
||||
import { ImageClipedEmitData } from '@/types/edit'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import useElementFlip from '@/views/components/element/hooks/useElementFlip'
|
||||
import useClipImage from './useClipImage'
|
||||
import useFilter from './useFilter'
|
||||
|
||||
import ImageRectOutline from './ImageRectOutline.vue'
|
||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||
import ImageOutline from './ImageOutline/index.vue'
|
||||
import ImageClipHandler from './ImageClipHandler.vue'
|
||||
import { ImageClipedEmitData } from '@/types/edit'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element-image',
|
||||
components: {
|
||||
ImageRectOutline,
|
||||
ImageEllipseOutline,
|
||||
ImagePolygonOutline,
|
||||
ImageOutline,
|
||||
ImageClipHandler,
|
||||
},
|
||||
props: {
|
||||
@ -117,53 +95,19 @@ export default defineComponent({
|
||||
const flip = computed(() => props.elementInfo.flip)
|
||||
const { flipStyle } = useElementFlip(flip)
|
||||
|
||||
const clip = computed(() => props.elementInfo.clip)
|
||||
const { clipShape, imgPosition } = useClipImage(clip)
|
||||
|
||||
const filters = computed(() => props.elementInfo.filters)
|
||||
const { filter } = useFilter(filters)
|
||||
|
||||
const handleSelectElement = (e: MouseEvent) => {
|
||||
if (props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
props.selectElement(e, props.elementInfo)
|
||||
}
|
||||
const clipShape = computed(() => {
|
||||
if (!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
||||
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
||||
|
||||
return CLIPPATHS[shape]
|
||||
})
|
||||
|
||||
const imgPosition = computed(() => {
|
||||
if (!props.elementInfo || !props.elementInfo.clip) {
|
||||
return {
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
}
|
||||
|
||||
const [start, end] = props.elementInfo.clip.range
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return {
|
||||
left: -left + '%',
|
||||
top: -top + '%',
|
||||
width: 100 / widthScale + '%',
|
||||
height: 100 / heightScale + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const filter = computed(() => {
|
||||
if (!props.elementInfo.filters) return ''
|
||||
let filter = ''
|
||||
for (const key of Object.keys(props.elementInfo.filters)) {
|
||||
filter += `${key}(${props.elementInfo.filters[key]}) `
|
||||
}
|
||||
return filter
|
||||
})
|
||||
|
||||
const clip = (data: ImageClipedEmitData) => {
|
||||
const handleClip = (data: ImageClipedEmitData) => {
|
||||
store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, '')
|
||||
|
||||
if (!data) return
|
||||
@ -183,7 +127,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
isCliping,
|
||||
clip,
|
||||
handleClip,
|
||||
clipingImageElementId,
|
||||
shadowStyle,
|
||||
handleSelectElement,
|
||||
|
42
src/views/components/element/ImageElement/useClipImage.ts
Normal file
42
src/views/components/element/ImageElement/useClipImage.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { computed, Ref } from 'vue'
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
import { ImageElementClip } from '@/types/slides'
|
||||
|
||||
export default (clip: Ref<ImageElementClip | undefined>) => {
|
||||
const clipShape = computed(() => {
|
||||
if (!clip.value) return CLIPPATHS.rect
|
||||
const shape = clip.value.shape || ClipPathTypes.RECT
|
||||
|
||||
return CLIPPATHS[shape]
|
||||
})
|
||||
|
||||
const imgPosition = computed(() => {
|
||||
if (!clip.value) {
|
||||
return {
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
}
|
||||
|
||||
const [start, end] = clip.value.range
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return {
|
||||
left: -left + '%',
|
||||
top: -top + '%',
|
||||
width: 100 / widthScale + '%',
|
||||
height: 100 / heightScale + '%',
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
clipShape,
|
||||
imgPosition,
|
||||
}
|
||||
}
|
17
src/views/components/element/ImageElement/useFilter.ts
Normal file
17
src/views/components/element/ImageElement/useFilter.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { computed, Ref } from 'vue'
|
||||
import { ImageElementFilters } from '@/types/slides'
|
||||
|
||||
export default (filters: Ref<ImageElementFilters | undefined>) => {
|
||||
const filter = computed(() => {
|
||||
if (!filters.value) return ''
|
||||
let filter = ''
|
||||
for (const key of Object.keys(filters.value)) {
|
||||
filter += `${key}(${filters.value[key]}) `
|
||||
}
|
||||
return filter
|
||||
})
|
||||
|
||||
return {
|
||||
filter,
|
||||
}
|
||||
}
|
@ -82,6 +82,7 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
|
||||
|
||||
const path = computed(() => {
|
||||
const start = props.elementInfo.start.join(',')
|
||||
const end = props.elementInfo.end.join(',')
|
||||
|
@ -106,7 +106,8 @@ export default defineComponent({
|
||||
return height < 24 ? 24 : height
|
||||
})
|
||||
|
||||
const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
|
||||
const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10 6' : '0 0')
|
||||
|
||||
const path = computed(() => {
|
||||
const start = props.elementInfo.start.join(',')
|
||||
const end = props.elementInfo.end.join(',')
|
||||
|
@ -46,7 +46,7 @@
|
||||
:fill="elementInfo.gradient ? `url(#editabel-gradient-${elementInfo.id})` : elementInfo.fill"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 5' : '0 0'"
|
||||
:stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'"
|
||||
></path>
|
||||
</g>
|
||||
</SvgWrapper>
|
||||
|
@ -30,6 +30,8 @@ export default defineComponent({
|
||||
const text = ref('')
|
||||
const isFocus = ref(false)
|
||||
|
||||
// 自定义v-modal,同步数据
|
||||
// 当文本框聚焦时,不执行数据同步
|
||||
watch(() => props.modelValue, () => {
|
||||
if (isFocus.value) return
|
||||
text.value = props.modelValue
|
||||
@ -42,6 +44,7 @@ export default defineComponent({
|
||||
emit('update:modelValue', text)
|
||||
}
|
||||
|
||||
// 聚焦时更新焦点标记,并监听粘贴事件
|
||||
const handleFocus = () => {
|
||||
isFocus.value = true
|
||||
|
||||
@ -58,11 +61,13 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// 失焦时更新焦点标记,清除粘贴事件监听
|
||||
const handleBlur = () => {
|
||||
isFocus.value = false
|
||||
if (textareaRef.value) textareaRef.value.onpaste = null
|
||||
}
|
||||
|
||||
// 清除粘贴事件监听
|
||||
onUnmounted(() => {
|
||||
if (textareaRef.value) textareaRef.value.onpaste = null
|
||||
})
|
||||
|
@ -114,6 +114,20 @@ export default defineComponent({
|
||||
const store = useStore()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const isStartSelect = ref(false)
|
||||
const startCell = ref<number[]>([])
|
||||
const endCell = ref<number[]>([])
|
||||
|
||||
const tableCells = computed<TableCell[][]>({
|
||||
get() {
|
||||
return props.data
|
||||
},
|
||||
set(newData) {
|
||||
emit('change', newData)
|
||||
},
|
||||
})
|
||||
|
||||
// 通过表格的主题色计算辅助颜色
|
||||
const subThemeColor = ref(['', ''])
|
||||
watch(() => props.theme, () => {
|
||||
if (props.theme) {
|
||||
@ -127,15 +141,7 @@ export default defineComponent({
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const tableCells = computed<TableCell[][]>({
|
||||
get() {
|
||||
return props.data
|
||||
},
|
||||
set(newData) {
|
||||
emit('change', newData)
|
||||
},
|
||||
})
|
||||
|
||||
// 计算表格每一列的列宽和总宽度
|
||||
const colSizeList = ref<number[]>([])
|
||||
const totalWidth = computed(() => colSizeList.value.reduce((a, b) => a + b))
|
||||
watch([
|
||||
@ -145,10 +151,8 @@ export default defineComponent({
|
||||
colSizeList.value = props.colWidths.map(item => item * props.width)
|
||||
}, { immediate: true })
|
||||
|
||||
const isStartSelect = ref(false)
|
||||
const startCell = ref<number[]>([])
|
||||
const endCell = ref<number[]>([])
|
||||
|
||||
// 清除全部单元格的选中状态
|
||||
// 表格处于不可编辑状态时也需要清除
|
||||
const removeSelectedCells = () => {
|
||||
startCell.value = []
|
||||
endCell.value = []
|
||||
@ -158,6 +162,7 @@ export default defineComponent({
|
||||
if (!props.editable) removeSelectedCells()
|
||||
})
|
||||
|
||||
// 用于拖拽列宽的操作节点位置
|
||||
const dragLinePosition = computed(() => {
|
||||
const dragLinePosition: number[] = []
|
||||
for (let i = 1; i < colSizeList.value.length + 1; i++) {
|
||||
@ -167,6 +172,7 @@ export default defineComponent({
|
||||
return dragLinePosition
|
||||
})
|
||||
|
||||
// 无效的单元格位置(被合并的单元格位置)集合
|
||||
const hideCells = computed(() => {
|
||||
const hideCells = []
|
||||
|
||||
@ -188,6 +194,7 @@ export default defineComponent({
|
||||
return hideCells
|
||||
})
|
||||
|
||||
// 当前选中的单元格集合
|
||||
const selectedCells = computed(() => {
|
||||
if (!startCell.value.length) return []
|
||||
const [startX, startY] = startCell.value
|
||||
@ -217,11 +224,13 @@ export default defineComponent({
|
||||
emit('changeSelectedCells', selectedCells.value)
|
||||
})
|
||||
|
||||
// 当前激活的单元格:当且仅当只有一个选中单元格时,该单元格为激活的单元格
|
||||
const activedCell = computed(() => {
|
||||
if (selectedCells.value.length > 1) return null
|
||||
return selectedCells.value[0]
|
||||
})
|
||||
|
||||
// 当前选中的单元格位置范围
|
||||
const selectedRange = computed(() => {
|
||||
if (!startCell.value.length) return null
|
||||
const [startX, startY] = startCell.value
|
||||
@ -242,6 +251,7 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
// 设置选中单元格状态(鼠标点击或拖选)
|
||||
const handleMouseup = () => isStartSelect.value = false
|
||||
|
||||
const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
|
||||
@ -264,20 +274,24 @@ export default defineComponent({
|
||||
document.removeEventListener('mouseup', handleMouseup)
|
||||
})
|
||||
|
||||
// 判断某位置是否为无效单元格(被合并掉的位置)
|
||||
const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
|
||||
|
||||
// 选中指定的列
|
||||
const selectCol = (index: number) => {
|
||||
const maxRow = tableCells.value.length - 1
|
||||
startCell.value = [0, index]
|
||||
endCell.value = [maxRow, index]
|
||||
}
|
||||
|
||||
// 选中指定的行
|
||||
const selectRow = (index: number) => {
|
||||
const maxCol = tableCells.value[index].length - 1
|
||||
startCell.value = [index, 0]
|
||||
endCell.value = [index, maxCol]
|
||||
}
|
||||
|
||||
// 选中全部单元格
|
||||
const selectAll = () => {
|
||||
const maxRow = tableCells.value.length - 1
|
||||
const maxCol = tableCells.value[maxRow].length - 1
|
||||
@ -285,6 +299,7 @@ export default defineComponent({
|
||||
endCell.value = [maxRow, maxCol]
|
||||
}
|
||||
|
||||
// 删除一行
|
||||
const deleteRow = (rowIndex: number) => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
|
||||
@ -307,6 +322,7 @@ export default defineComponent({
|
||||
tableCells.value = _tableCells
|
||||
}
|
||||
|
||||
// 删除一列
|
||||
const deleteCol = (colIndex: number) => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
|
||||
@ -332,6 +348,7 @@ export default defineComponent({
|
||||
emit('changeColWidths', colSizeList.value)
|
||||
}
|
||||
|
||||
// 插入一行
|
||||
const insertRow = (rowIndex: number) => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
|
||||
@ -349,6 +366,7 @@ export default defineComponent({
|
||||
tableCells.value = _tableCells
|
||||
}
|
||||
|
||||
// 插入一列
|
||||
const insertCol = (colIndex: number) => {
|
||||
tableCells.value = tableCells.value.map(item => {
|
||||
const cell = {
|
||||
@ -364,6 +382,7 @@ export default defineComponent({
|
||||
emit('changeColWidths', colSizeList.value)
|
||||
}
|
||||
|
||||
// 合并单元格
|
||||
const mergeCells = () => {
|
||||
const [startX, startY] = startCell.value
|
||||
const [endX, endY] = endCell.value
|
||||
@ -382,6 +401,7 @@ export default defineComponent({
|
||||
removeSelectedCells()
|
||||
}
|
||||
|
||||
// 拆分单元格
|
||||
const splitCells = (rowIndex: number, colIndex: number) => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
_tableCells[rowIndex][colIndex].rowspan = 1
|
||||
@ -391,6 +411,7 @@ export default defineComponent({
|
||||
removeSelectedCells()
|
||||
}
|
||||
|
||||
// 鼠标拖拽调整列宽
|
||||
const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => {
|
||||
removeSelectedCells()
|
||||
let isMouseDown = true
|
||||
@ -417,6 +438,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// 清空选中单元格内的文字
|
||||
const clearSelectedCellText = () => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
|
||||
@ -430,6 +452,10 @@ export default defineComponent({
|
||||
tableCells.value = _tableCells
|
||||
}
|
||||
|
||||
// 将焦点移动到下一个单元格
|
||||
// 当前行右边有单元格时,焦点右移
|
||||
// 当前行右边无单元格(已处在行末),且存在下一行时,焦点移动下下一行行首
|
||||
// 当前行右边无单元格(已处在行末),且不存在下一行(已处在最后一行)时,新建一行并将焦点移动下下一行行首
|
||||
const tabActiveCell = () => {
|
||||
const getNextCell = (i: number, j: number): [number, number] | null => {
|
||||
if (!tableCells.value[i]) return null
|
||||
@ -450,12 +476,14 @@ export default defineComponent({
|
||||
}
|
||||
else startCell.value = nextCell
|
||||
|
||||
// 移动焦点后自动聚焦文本
|
||||
nextTick(() => {
|
||||
const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
|
||||
if (textRef) textRef.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 表格快捷键监听
|
||||
const keydownListener = (e: KeyboardEvent) => {
|
||||
if (!props.editable || !selectedCells.value.length) return
|
||||
|
||||
@ -498,6 +526,7 @@ export default defineComponent({
|
||||
document.removeEventListener('keydown', keydownListener)
|
||||
})
|
||||
|
||||
// 计算单元格文本样式
|
||||
const getTextStyle = (style?: TableCellStyle) => {
|
||||
if (!style) return {}
|
||||
const {
|
||||
@ -524,10 +553,12 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// 单元格文字输入时更新表格数据
|
||||
const handleInput = debounce(function() {
|
||||
emit('change', tableCells.value)
|
||||
}, 300, { trailing: true })
|
||||
|
||||
// 获取有效的单元格(排除掉被合并的单元格)
|
||||
const getEffectiveTableCells = () => {
|
||||
const effectiveTableCells = []
|
||||
|
||||
@ -543,6 +574,7 @@ export default defineComponent({
|
||||
return effectiveTableCells
|
||||
}
|
||||
|
||||
// 检查是否可以删除行和列:有效的行/列数大于1
|
||||
const checkCanDeleteRowOrCol = () => {
|
||||
const effectiveTableCells = getEffectiveTableCells()
|
||||
const canDeleteRow = effectiveTableCells.length > 1
|
||||
@ -551,6 +583,9 @@ export default defineComponent({
|
||||
return { canDeleteRow, canDeleteCol }
|
||||
}
|
||||
|
||||
// 检查是否可以合并或拆分
|
||||
// 必须多选才可以合并
|
||||
// 必须单选且所选单元格为合并单元格才可以拆分
|
||||
const checkCanMergeOrSplit = (rowIndex: number, colIndex: number) => {
|
||||
const isMultiSelected = selectedCells.value.length > 1
|
||||
const targetCell = tableCells.value[rowIndex][colIndex]
|
||||
@ -717,7 +752,7 @@ table {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba($color: $themeColor, $alpha: .3);
|
||||
background-color: rgba($color: #666, $alpha: .4);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,6 +69,9 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const store = useStore()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
|
||||
const elementRef = ref<HTMLElement>()
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
@ -78,8 +81,9 @@ export default defineComponent({
|
||||
|
||||
props.selectElement(e, props.elementInfo)
|
||||
}
|
||||
|
||||
// 更新表格的可编辑状态,表格处于编辑状态时需要禁用全局快捷键
|
||||
const editable = ref(false)
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
|
||||
watch(handleElementId, () => {
|
||||
if (handleElementId.value !== props.elementInfo.id) editable.value = false
|
||||
@ -89,8 +93,12 @@ export default defineComponent({
|
||||
store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, editable.value)
|
||||
})
|
||||
|
||||
const elementRef = ref<HTMLElement>()
|
||||
const startEdit = () => {
|
||||
if (!props.elementInfo.lock) editable.value = true
|
||||
}
|
||||
|
||||
// 监听表格元素的尺寸变化,当高度变化时,更新高度到vuex
|
||||
// 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
|
||||
const isScaling = ref(false)
|
||||
const realHeightCache = ref(-1)
|
||||
|
||||
@ -139,6 +147,7 @@ export default defineComponent({
|
||||
if (elementRef.value) resizeObserver.unobserve(elementRef.value)
|
||||
})
|
||||
|
||||
// 更新表格内容数据
|
||||
const updateTableCells = (data: TableCell[][]) => {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
@ -146,6 +155,8 @@ export default defineComponent({
|
||||
})
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
// 更新表格的列宽数据
|
||||
const updateColWidths = (widths: number[]) => {
|
||||
const width = widths.reduce((a, b) => a + b)
|
||||
const colWidths = widths.map(item => item / width)
|
||||
@ -157,14 +168,11 @@ export default defineComponent({
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
// 更新表格当前选中的单元格
|
||||
const updateSelectedCells = (cells: string[]) => {
|
||||
nextTick(() => emitter.emit(EmitterEvents.UPDATE_TABLE_SELECTED_CELL, cells))
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
if (!props.elementInfo.lock) editable.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
canvasScale,
|
||||
|
@ -86,6 +86,23 @@ export default defineComponent({
|
||||
const isScaling = ref(false)
|
||||
const realHeightCache = ref(-1)
|
||||
|
||||
const editorViewRef = ref<HTMLElement>()
|
||||
let editorView: EditorView
|
||||
|
||||
const shadow = computed(() => props.elementInfo.shadow)
|
||||
const { shadowStyle } = useElementShadow(shadow)
|
||||
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
|
||||
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||
if (props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
|
||||
props.selectElement(e, props.elementInfo, canMove)
|
||||
}
|
||||
|
||||
// 监听文本元素的尺寸变化,当高度变化时,更新高度到vuex
|
||||
// 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
|
||||
const scaleElementStateListener = (state: boolean) => {
|
||||
isScaling.value = state
|
||||
|
||||
@ -128,9 +145,10 @@ export default defineComponent({
|
||||
if (elementRef.value) resizeObserver.unobserve(elementRef.value)
|
||||
})
|
||||
|
||||
const editorViewRef = ref<HTMLElement>()
|
||||
let editorView: EditorView
|
||||
|
||||
// 富文本的各种交互事件监听:
|
||||
// 聚焦时取消全局快捷键事件
|
||||
// 输入文字时同步数据到vuex
|
||||
// 点击鼠标和键盘时同步富文本状态到工具栏
|
||||
const handleFocus = () => {
|
||||
store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, true)
|
||||
}
|
||||
@ -155,6 +173,7 @@ export default defineComponent({
|
||||
handleClick()
|
||||
}
|
||||
|
||||
// 将富文本内容同步到DOM
|
||||
const textContent = computed(() => props.elementInfo.content)
|
||||
watch(textContent, () => {
|
||||
if (!editorView) return
|
||||
@ -162,11 +181,13 @@ export default defineComponent({
|
||||
editorView.dom.innerHTML = textContent.value
|
||||
})
|
||||
|
||||
// 打开/关闭编辑器的编辑模式
|
||||
const editable = computed(() => !props.elementInfo.lock)
|
||||
watch(editable, () => {
|
||||
editorView.setProps({ editable: () => editable.value })
|
||||
})
|
||||
|
||||
// Prosemirror编辑器的初始化和卸载
|
||||
onMounted(() => {
|
||||
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
||||
handleDOMEvents: {
|
||||
@ -182,18 +203,8 @@ export default defineComponent({
|
||||
editorView && editorView.destroy()
|
||||
})
|
||||
|
||||
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||
if (props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
|
||||
props.selectElement(e, props.elementInfo, canMove)
|
||||
}
|
||||
|
||||
const shadow = computed(() => props.elementInfo.shadow)
|
||||
const { shadowStyle } = useElementShadow(shadow)
|
||||
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
|
||||
// 执行富文本命令(可以是一个或多个)
|
||||
// 部分命令在执行前先判断当前选区是否为空,如果选区为空先进行全选操作
|
||||
const execCommand = (payload: CommandPayload | CommandPayload[]) => {
|
||||
if (handleElementId.value !== props.elementInfo.id) return
|
||||
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { ref, Ref, watchEffect } from 'vue'
|
||||
import { computed, Ref } from 'vue'
|
||||
import { ImageOrShapeFlip } from '@/types/slides'
|
||||
|
||||
// 计算元素的翻转样式
|
||||
export default (flip: Ref<ImageOrShapeFlip | undefined>) => {
|
||||
const flipStyle = ref('')
|
||||
|
||||
watchEffect(() => {
|
||||
const flipStyle = computed(() => {
|
||||
if (flip.value) {
|
||||
let style = ''
|
||||
|
||||
const { x, y } = flip.value
|
||||
if (x && y) flipStyle.value = `rotateX(${x}deg) rotateY(${y}deg)`
|
||||
else if (x) flipStyle.value = `rotateX(${x}deg)`
|
||||
else if (y) flipStyle.value = `rotateY(${y}deg)`
|
||||
else flipStyle.value = ''
|
||||
if (x && y) style = `rotateX(${x}deg) rotateY(${y}deg)`
|
||||
else if (x) style = `rotateX(${x}deg)`
|
||||
else if (y) style = `rotateY(${y}deg)`
|
||||
|
||||
return style
|
||||
}
|
||||
else flipStyle.value = ''
|
||||
return ''
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { computed, Ref } from 'vue'
|
||||
import { PPTElementOutline } from '@/types/slides'
|
||||
|
||||
// 计算边框相关属性值,主要是对默认值的处理
|
||||
export default (outline: Ref<PPTElementOutline | undefined>) => {
|
||||
const outlineWidth = computed(() => (outline.value && outline.value.width !== undefined) ? outline.value.width : 0)
|
||||
const outlineStyle = computed(() => (outline.value && outline.value.style !== undefined) ? outline.value.style : 'solid')
|
||||
const outlineColor = computed(() => (outline.value && outline.value.color !== undefined) ? outline.value.color : '#d14424')
|
||||
const outlineWidth = computed(() => outline.value?.width ?? 0)
|
||||
const outlineStyle = computed(() => outline.value?.style || 'solid')
|
||||
const outlineColor = computed(() => outline.value?.color || '#d14424')
|
||||
|
||||
return {
|
||||
outlineWidth,
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { ref, Ref, watchEffect } from 'vue'
|
||||
import { computed, Ref } from 'vue'
|
||||
import { PPTElementShadow } from '@/types/slides'
|
||||
|
||||
// 计算元素的阴影样式
|
||||
export default (shadow: Ref<PPTElementShadow | undefined>) => {
|
||||
const shadowStyle = ref('')
|
||||
|
||||
watchEffect(() => {
|
||||
const shadowStyle = computed(() => {
|
||||
if (shadow.value) {
|
||||
const { h, v, blur, color } = shadow.value
|
||||
shadowStyle.value = `${h}px ${v}px ${blur}px ${color}`
|
||||
return `${h}px ${v}px ${blur}px ${color}`
|
||||
}
|
||||
else shadowStyle.value = ''
|
||||
return ''
|
||||
})
|
||||
|
||||
return {
|
||||
|
Loading…
x
Reference in New Issue
Block a user