![]()
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 {