diff --git a/src/types/slides.ts b/src/types/slides.ts index 0542b16f..09a300e7 100644 --- a/src/types/slides.ts +++ b/src/types/slides.ts @@ -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; } diff --git a/src/views/components/ThumbnailSlide/index.vue b/src/views/components/ThumbnailSlide/index.vue index 081361be..daff3590 100644 --- a/src/views/components/ThumbnailSlide/index.vue +++ b/src/views/components/ThumbnailSlide/index.vue @@ -13,7 +13,7 @@ transform: `scale(${scale})`, }" > -
+
{ 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, diff --git a/src/views/components/element/ElementOutline.vue b/src/views/components/element/ElementOutline.vue index 76415ac7..3bfd1ee7 100644 --- a/src/views/components/element/ElementOutline.vue +++ b/src/views/components/element/ElementOutline.vue @@ -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'" > diff --git a/src/views/components/element/ImageElement/BaseImageElement.vue b/src/views/components/element/ImageElement/BaseImageElement.vue index 70b0b108..c6b807a8 100644 --- a/src/views/components/element/ImageElement/BaseImageElement.vue +++ b/src/views/components/element/ImageElement/BaseImageElement.vue @@ -16,26 +16,7 @@ transform: flipStyle, }" > - - - +
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, } }, }) diff --git a/src/views/components/element/ImageElement/ImageClipHandler.vue b/src/views/components/element/ImageElement/ImageClipHandler.vue index 5a12b764..f2ffaeed 100644 --- a/src/views/components/element/ImageElement/ImageClipHandler.vue +++ b/src/views/components/element/ImageElement/ImageClipHandler.vue @@ -95,12 +95,6 @@ export default defineComponent({ const canvasScale = computed(() => store.state.canvasScale) const ctrlOrShiftKeyActive = computed(() => 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(null) + // 获取裁剪区域信息(裁剪区域占原图的宽高比例,处在原图中的位置) const getClipDataTransformInfo = () => { const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]] @@ -118,7 +113,8 @@ 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) } diff --git a/src/views/components/element/ImageElement/ImageEllipseOutline.vue b/src/views/components/element/ImageElement/ImageOutline/ImageEllipseOutline.vue similarity index 94% rename from src/views/components/element/ImageElement/ImageEllipseOutline.vue rename to src/views/components/element/ImageElement/ImageOutline/ImageEllipseOutline.vue index 6f99897d..d6dfa36a 100644 --- a/src/views/components/element/ImageElement/ImageEllipseOutline.vue +++ b/src/views/components/element/ImageElement/ImageOutline/ImageEllipseOutline.vue @@ -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'" > diff --git a/src/views/components/element/ImageElement/ImagePolygonOutline.vue b/src/views/components/element/ImageElement/ImageOutline/ImagePolygonOutline.vue similarity index 94% rename from src/views/components/element/ImageElement/ImagePolygonOutline.vue rename to src/views/components/element/ImageElement/ImageOutline/ImagePolygonOutline.vue index e67d3fdb..d57a567d 100644 --- a/src/views/components/element/ImageElement/ImagePolygonOutline.vue +++ b/src/views/components/element/ImageElement/ImageOutline/ImagePolygonOutline.vue @@ -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'" > diff --git a/src/views/components/element/ImageElement/ImageRectOutline.vue b/src/views/components/element/ImageElement/ImageOutline/ImageRectOutline.vue similarity index 95% rename from src/views/components/element/ImageElement/ImageRectOutline.vue rename to src/views/components/element/ImageElement/ImageOutline/ImageRectOutline.vue index 113211d3..df17ec78 100644 --- a/src/views/components/element/ImageElement/ImageRectOutline.vue +++ b/src/views/components/element/ImageElement/ImageOutline/ImageRectOutline.vue @@ -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'" > diff --git a/src/views/components/element/ImageElement/ImageOutline/index.vue b/src/views/components/element/ImageElement/ImageOutline/index.vue new file mode 100644 index 00000000..bb668b63 --- /dev/null +++ b/src/views/components/element/ImageElement/ImageOutline/index.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/src/views/components/element/ImageElement/index.vue b/src/views/components/element/ImageElement/index.vue index fb8eb984..b1548960 100644 --- a/src/views/components/element/ImageElement/index.vue +++ b/src/views/components/element/ImageElement/index.vue @@ -20,7 +20,7 @@ :top="elementInfo.top" :left="elementInfo.left" :clipPath="clipShape.style" - @clip="range => clip(range)" + @clip="range => handleClip(range)" />
- - - + -
+
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, diff --git a/src/views/components/element/ImageElement/useClipImage.ts b/src/views/components/element/ImageElement/useClipImage.ts new file mode 100644 index 00000000..d6ced87a --- /dev/null +++ b/src/views/components/element/ImageElement/useClipImage.ts @@ -0,0 +1,42 @@ +import { computed, Ref } from 'vue' +import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip' +import { ImageElementClip } from '@/types/slides' + +export default (clip: Ref) => { + 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, + } +} \ No newline at end of file diff --git a/src/views/components/element/ImageElement/useFilter.ts b/src/views/components/element/ImageElement/useFilter.ts new file mode 100644 index 00000000..76596424 --- /dev/null +++ b/src/views/components/element/ImageElement/useFilter.ts @@ -0,0 +1,17 @@ +import { computed, Ref } from 'vue' +import { ImageElementFilters } from '@/types/slides' + +export default (filters: Ref) => { + 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, + } +} \ No newline at end of file diff --git a/src/views/components/element/LineElement/BaseLineElement.vue b/src/views/components/element/LineElement/BaseLineElement.vue index 7fb4b41a..b8814045 100644 --- a/src/views/components/element/LineElement/BaseLineElement.vue +++ b/src/views/components/element/LineElement/BaseLineElement.vue @@ -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(',') diff --git a/src/views/components/element/LineElement/index.vue b/src/views/components/element/LineElement/index.vue index b8d0189a..08e46608 100644 --- a/src/views/components/element/LineElement/index.vue +++ b/src/views/components/element/LineElement/index.vue @@ -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(',') diff --git a/src/views/components/element/ShapeElement/index.vue b/src/views/components/element/ShapeElement/index.vue index 72c64a8f..7bf47581 100644 --- a/src/views/components/element/ShapeElement/index.vue +++ b/src/views/components/element/ShapeElement/index.vue @@ -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'" > diff --git a/src/views/components/element/TableElement/CustomTextarea.vue b/src/views/components/element/TableElement/CustomTextarea.vue index fd47b31a..5f216897 100644 --- a/src/views/components/element/TableElement/CustomTextarea.vue +++ b/src/views/components/element/TableElement/CustomTextarea.vue @@ -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 }) diff --git a/src/views/components/element/TableElement/EditableTable.vue b/src/views/components/element/TableElement/EditableTable.vue index 66ce4945..04f993dc 100644 --- a/src/views/components/element/TableElement/EditableTable.vue +++ b/src/views/components/element/TableElement/EditableTable.vue @@ -113,7 +113,21 @@ export default defineComponent({ setup(props, { emit }) { const store = useStore() const canvasScale = computed(() => store.state.canvasScale) + + const isStartSelect = ref(false) + const startCell = ref([]) + const endCell = ref([]) + const tableCells = computed({ + 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({ - get() { - return props.data - }, - set(newData) { - emit('change', newData) - }, - }) - + // 计算表格每一列的列宽和总宽度 const colSizeList = ref([]) 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([]) - const endCell = ref([]) - + // 清除全部单元格的选中状态 + // 表格处于不可编辑状态时也需要清除 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); } } diff --git a/src/views/components/element/TableElement/index.vue b/src/views/components/element/TableElement/index.vue index 7c138155..f2fac11f 100644 --- a/src/views/components/element/TableElement/index.vue +++ b/src/views/components/element/TableElement/index.vue @@ -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() 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 @@ -88,9 +92,13 @@ export default defineComponent({ watch(editable, () => { store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, editable.value) }) - - const elementRef = ref() + 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, diff --git a/src/views/components/element/TextElement/index.vue b/src/views/components/element/TextElement/index.vue index 15b0e855..0c191739 100644 --- a/src/views/components/element/TextElement/index.vue +++ b/src/views/components/element/TextElement/index.vue @@ -86,6 +86,23 @@ export default defineComponent({ const isScaling = ref(false) const realHeightCache = ref(-1) + const editorViewRef = ref() + 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 @@ -127,10 +144,11 @@ export default defineComponent({ onUnmounted(() => { if (elementRef.value) resizeObserver.unobserve(elementRef.value) }) - - const editorViewRef = ref() - 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: { @@ -181,19 +202,9 @@ export default defineComponent({ onUnmounted(() => { 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 diff --git a/src/views/components/element/hooks/useElementFlip.ts b/src/views/components/element/hooks/useElementFlip.ts index 0e9826bc..66616330 100644 --- a/src/views/components/element/hooks/useElementFlip.ts +++ b/src/views/components/element/hooks/useElementFlip.ts @@ -1,18 +1,20 @@ -import { ref, Ref, watchEffect } from 'vue' +import { computed, Ref } from 'vue' import { ImageOrShapeFlip } from '@/types/slides' +// 计算元素的翻转样式 export default (flip: Ref) => { - 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 { diff --git a/src/views/components/element/hooks/useElementOutline.ts b/src/views/components/element/hooks/useElementOutline.ts index 4ee9f240..2e9d8281 100644 --- a/src/views/components/element/hooks/useElementOutline.ts +++ b/src/views/components/element/hooks/useElementOutline.ts @@ -1,10 +1,11 @@ import { computed, Ref } from 'vue' import { PPTElementOutline } from '@/types/slides' +// 计算边框相关属性值,主要是对默认值的处理 export default (outline: Ref) => { - 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, diff --git a/src/views/components/element/hooks/useElementShadow.ts b/src/views/components/element/hooks/useElementShadow.ts index 20abbbd9..6729d2c9 100644 --- a/src/views/components/element/hooks/useElementShadow.ts +++ b/src/views/components/element/hooks/useElementShadow.ts @@ -1,15 +1,14 @@ -import { ref, Ref, watchEffect } from 'vue' +import { computed, Ref } from 'vue' import { PPTElementShadow } from '@/types/slides' +// 计算元素的阴影样式 export default (shadow: Ref) => { - 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 {