From 4b25579b4565685c2e135e923b58e47112c23729 Mon Sep 17 00:00:00 2001 From: pipipi-pikachu Date: Tue, 9 Feb 2021 19:49:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ColorPicker/Alpha.vue | 1 + src/components/ColorPicker/Checkboard.vue | 1 + src/components/WritingBoard.vue | 113 +++++++------- src/hooks/useAlignElementToCanvas.ts | 16 ++ src/hooks/useCombineElement.ts | 28 +++- src/hooks/useCopyAndPasteElement.ts | 4 + src/hooks/useCreateElement.ts | 29 ++++ src/hooks/useDeleteElement.ts | 2 + src/hooks/useHistorySnapshot.ts | 3 + src/hooks/useLockElement.ts | 5 + src/hooks/useMoveElement.ts | 15 +- src/hooks/useOrderElement.ts | 173 +++++++++++++--------- src/hooks/usePasteTextClipboardData.ts | 21 ++- src/hooks/useScaleCanvas.ts | 8 + src/hooks/useScreening.ts | 3 + src/hooks/useSelectAllElement.ts | 1 + src/hooks/useSlideBackgroundStyle.ts | 7 + src/hooks/useSlideHandler.ts | 10 ++ src/plugins/iconPark.ts | 4 +- 19 files changed, 299 insertions(+), 145 deletions(-) diff --git a/src/components/ColorPicker/Alpha.vue b/src/components/ColorPicker/Alpha.vue index a1a42b53..7289fe0b 100644 --- a/src/components/ColorPicker/Alpha.vue +++ b/src/components/ColorPicker/Alpha.vue @@ -35,6 +35,7 @@ export default defineComponent({ }, setup(props, { emit }) { const color = computed(() => props.value) + const gradientColor = computed(() => { const rgbaStr = [color.value.r, color.value.g, color.value.b].join(',') return `linear-gradient(to right, rgba(${rgbaStr}, 0) 0%, rgba(${rgbaStr}, 1) 100%)` diff --git a/src/components/ColorPicker/Checkboard.vue b/src/components/ColorPicker/Checkboard.vue index 33ea1e04..a420abd5 100644 --- a/src/components/ColorPicker/Checkboard.vue +++ b/src/components/ColorPicker/Checkboard.vue @@ -6,6 +6,7 @@ import { computed, defineComponent } from 'vue' const checkboardCache = {} + const renderCheckboard = (white: string, grey: string, size: number) => { const canvas = document.createElement('canvas') canvas.width = canvas.height = size * 2 diff --git a/src/components/WritingBoard.vue b/src/components/WritingBoard.vue index ff5aa5a0..4a6f5188 100644 --- a/src/components/WritingBoard.vue +++ b/src/components/WritingBoard.vue @@ -27,7 +27,7 @@ height: rubberSize + 'px', }" v-if="mouseInCanvas && model === 'eraser'" - > + > @@ -53,6 +53,7 @@ export default defineComponent({ let ctx: CanvasRenderingContext2D | null = null const writingBoardRef = ref() const canvasRef = ref() + let lastPos = { x: 0, y: 0, @@ -61,12 +62,22 @@ export default defineComponent({ let lastTime = 0 let lastLineWidth = -1 + // 鼠标位置坐标:用于画笔或橡皮位置跟随 const mouse = reactive({ x: 0, y: 0, }) + // 更新鼠标位置坐标 + const updateMousePosition = (e: MouseEvent) => { + mouse.x = e.pageX + mouse.y = e.pageY + } + + // 鼠标是否处在画布范围内:处在范围内才会显示画笔或橡皮 const mouseInCanvas = ref(false) + + // 初始化画布 const initCanvas = () => { if (!canvasRef.value || !writingBoardRef.value) return @@ -82,30 +93,9 @@ export default defineComponent({ ctx.lineCap = 'round' ctx.lineJoin = 'round' } + onMounted(initCanvas) - const getDistance = (posX: number, posY: number) => { - const lastPosX = lastPos.x - const lastPosY = lastPos.y - return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY)) - } - - const getLineWidth = (s: number, t: number) => { - const maxV = 10 - const minV = 0.1 - const maxWidth = penSize - const minWidth = 3 - const v = s / t - let lineWidth - - if (v <= minV) lineWidth = maxWidth - else if (v >= maxV) lineWidth = minWidth - else lineWidth = maxWidth - v / maxV * maxWidth - - if (lastLineWidth === -1) return lineWidth - return lineWidth * 1 / 3 + lastLineWidth * 2 / 3 - } - - // 画笔绘制方法 + // 绘制画笔墨迹方法 const draw = (posX: number, posY: number, lineWidth: number) => { if (!ctx) return @@ -121,7 +111,7 @@ export default defineComponent({ ctx.closePath() } - // 橡皮擦除方法 + // 擦除墨迹方法 const erase = (posX: number, posY: number) => { if (!ctx || !canvasRef.value) return const lastPosX = lastPos.x @@ -155,47 +145,60 @@ export default defineComponent({ ctx.restore() } - const startDraw = (posX: number, posY: number) => { - lastPos = { x: posX, y: posY } - lastTime = new Date().getTime() - } - - const startMove = (posX: number, posY: number) => { - const time = new Date().getTime() - - // 画笔模式(这里通过绘制速度调节画笔的粗细) - if (props.model === 'pen') { - const s = getDistance(posX, posY) - const t = time - lastTime - const lineWidth = getLineWidth(s, t) - - draw(posX, posY, lineWidth) - lastLineWidth = lineWidth - } - // 橡皮模式 - else erase(posX, posY) - - lastPos = { x: posX, y: posY } - lastTime = new Date().getTime() - } - + // 准备开始绘制/擦除墨迹(落笔) const handleMousedown = (e: MouseEvent) => { isMouseDown = true - startDraw(e.offsetX, e.offsetY) + lastPos = { x: e.offsetX, y: e.offsetY } + lastTime = new Date().getTime() } - const updateMousePosition = (e: MouseEvent) => { - mouse.x = e.pageX - mouse.y = e.pageY + // 计算鼠标两次移动之间的距离 + const getDistance = (posX: number, posY: number) => { + const lastPosX = lastPos.x + const lastPosY = lastPos.y + return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY)) } + // 根据鼠标两次移动之间的距离s和时间t计算绘制速度,速度越快,墨迹越细 + const getLineWidth = (s: number, t: number) => { + const maxV = 10 + const minV = 0.1 + const maxWidth = penSize + const minWidth = 3 + const v = s / t + let lineWidth + + if (v <= minV) lineWidth = maxWidth + else if (v >= maxV) lineWidth = minWidth + else lineWidth = maxWidth - v / maxV * maxWidth + + if (lastLineWidth === -1) return lineWidth + return lineWidth * 1 / 3 + lastLineWidth * 2 / 3 + } + + // 开始绘制/擦除墨迹(移动) const handleMousemove = (e: MouseEvent) => { updateMousePosition(e) if (!isMouseDown) return - startMove(e.offsetX, e.offsetY) + + const time = new Date().getTime() + + if (props.model === 'pen') { + const s = getDistance(e.offsetX, e.offsetY) + const t = time - lastTime + const lineWidth = getLineWidth(s, t) + + draw(e.offsetX, e.offsetY, lineWidth) + lastLineWidth = lineWidth + } + else erase(e.offsetX, e.offsetY) + + lastPos = { x: e.offsetX, y: e.offsetY } + lastTime = new Date().getTime() } + // 结束绘制/擦除墨迹(停笔) const handleMouseup = () => { if (!isMouseDown) return isMouseDown = false @@ -207,8 +210,6 @@ export default defineComponent({ ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height) } - onMounted(initCanvas) - return { mouse, mouseInCanvas, diff --git a/src/hooks/useAlignElementToCanvas.ts b/src/hooks/useAlignElementToCanvas.ts index d73476f4..38515524 100644 --- a/src/hooks/useAlignElementToCanvas.ts +++ b/src/hooks/useAlignElementToCanvas.ts @@ -15,6 +15,10 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() + /** + * 将所有选中的元素对齐到画布 + * @param command 对齐方向 + */ const alignElementToCanvas = (command: ElementAlignCommand) => { const viewportWidth = VIEWPORT_SIZE const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO @@ -24,33 +28,45 @@ export default () => { for (const element of newElementList) { if (!activeElementIdList.value.includes(element.id)) continue + // 水平垂直居中 if (command === ElementAlignCommands.CENTER) { const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2 const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2 element.top = element.top - offsetY element.left = element.left - offsetX } + + // 顶部对齐 if (command === ElementAlignCommands.TOP) { const offsetY = minY - 0 element.top = element.top - offsetY } + + // 垂直居中 else if (command === ElementAlignCommands.VERTICAL) { const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2 element.top = element.top - offsetY } + + // 底部对齐 else if (command === ElementAlignCommands.BOTTOM) { const offsetY = maxY - viewportHeight element.top = element.top - offsetY } + // 左侧对齐 else if (command === ElementAlignCommands.LEFT) { const offsetX = minX - 0 element.left = element.left - offsetX } + + // 水平居中 else if (command === ElementAlignCommands.HORIZONTAL) { const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2 element.left = element.left - offsetX } + + // 右侧对齐 else if (command === ElementAlignCommands.RIGHT) { const offsetX = maxX - viewportWidth element.left = element.left - offsetX diff --git a/src/hooks/useCombineElement.ts b/src/hooks/useCombineElement.ts index 12cf3c84..857fe8e9 100644 --- a/src/hooks/useCombineElement.ts +++ b/src/hooks/useCombineElement.ts @@ -9,16 +9,23 @@ export default () => { const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementList = computed(() => store.getters.activeElementList) const currentSlide = computed(() => store.getters.currentSlide) + const handleElementId = computed(() => store.state.handleElementId) const { addHistorySnapshot } = useHistorySnapshot() - // 组合元素(为当前所有激活元素添加一个相同的groupId) + /** + * 组合当前选中的元素:给当前选中的元素赋予一个相同的分组ID + */ const combineElements = () => { if (!activeElementList.value.length) return + // 生成一个新元素列表进行后续操作 let newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements)) + + // 生成分组ID const groupId = createRandomCode() + // 收集需要组合的元素列表,并赋上唯一分组ID const combineElementList: PPTElement[] = [] for (const element of newElementList) { if (activeElementIdList.value.includes(element.id)) { @@ -27,19 +34,23 @@ export default () => { } } - // 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面 - const combineElementMaxIndex = newElementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id) + // 确保该组合内所有元素成员的层级是连续的,具体操作方法为: + // 先获取到该组合内最上层元素的层级,将本次需要组合的元素从新元素列表中移除, + // 再根据最上层元素的层级位置,将上面收集到的需要组合的元素列表一起插入到新元素列表中合适的位置 + const combineElementMaxLevel = newElementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id) const combineElementIdList = combineElementList.map(_element => _element.id) newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.id)) - const insertIndex = combineElementMaxIndex - combineElementList.length + 1 - newElementList.splice(insertIndex, 0, ...combineElementList) + const insertLevel = combineElementMaxLevel - combineElementList.length + 1 + newElementList.splice(insertLevel, 0, ...combineElementList) store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList }) addHistorySnapshot() } - // 取消组合元素(移除所有被激活元素的groupId) + /** + * 取消组合元素:移除选中元素的分组ID + */ const uncombineElements = () => { if (!activeElementList.value.length) return const hasElementInGroup = activeElementList.value.some(item => item.groupId) @@ -50,6 +61,11 @@ export default () => { if (activeElementIdList.value.includes(element.id) && element.groupId) delete element.groupId } store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList }) + + // 取消组合后,需要重置激活元素状态 + // 默认重置为当前正在操作的元素,如果不存在则重置为空 + const handleElementIdList = handleElementId.value ? [handleElementId.value] : [] + store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, handleElementIdList) addHistorySnapshot() } diff --git a/src/hooks/useCopyAndPasteElement.ts b/src/hooks/useCopyAndPasteElement.ts index 78ffaa1d..e2108128 100644 --- a/src/hooks/useCopyAndPasteElement.ts +++ b/src/hooks/useCopyAndPasteElement.ts @@ -15,6 +15,7 @@ export default () => { const { pasteTextClipboardData } = usePasteTextClipboardData() const { deleteElement } = useDeleteElement() + // 将选中元素数据加密后复制到剪贴板 const copyElement = () => { if (!activeElementIdList.value.length) return @@ -28,17 +29,20 @@ export default () => { }) } + // 将选中元素复制后删除(剪切) const cutElement = () => { copyElement() deleteElement() } + // 尝试将剪贴板元素数据解密后进行粘贴 const pasteElement = () => { readClipboard().then(text => { pasteTextClipboardData(text) }).catch(err => message.warning(err)) } + // 将选中元素复制后立刻粘贴 const quickCopyElement = () => { copyElement() pasteElement() diff --git a/src/hooks/useCreateElement.ts b/src/hooks/useCreateElement.ts index 06da120c..428abd8a 100644 --- a/src/hooks/useCreateElement.ts +++ b/src/hooks/useCreateElement.ts @@ -29,12 +29,17 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() + // 创建(插入)一个元素并将其设置为被选中元素 const createElement = (element: PPTElement) => { store.commit(MutationTypes.ADD_ELEMENT, element) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.id]) addHistorySnapshot() } + /** + * 创建图片元素 + * @param src 图片地址 + */ const createImageElement = (src: string) => { getImageSize(src).then(({ width, height }) => { const scale = height / width @@ -61,6 +66,10 @@ export default () => { }) } + /** + * 创建图表元素 + * @param chartType 图表类型 + */ const createChartElement = (chartType: ChartType) => { createElement({ type: 'chart', @@ -81,6 +90,11 @@ export default () => { }) } + /** + * 创建表格元素 + * @param row 行数 + * @param col 列数 + */ const createTableElement = (row: number, col: number) => { const rowCells: TableCell[] = new Array(col).fill({ id: createRandomCode(), colspan: 1, rowspan: 1, text: '' }) const data: TableCell[][] = new Array(row).fill(rowCells) @@ -117,6 +131,11 @@ export default () => { }) } + /** + * 创建文本元素 + * @param position 位置大小信息 + * @param content 文本内容 + */ const createTextElement = (position: CommonElementPosition, content = '请输入内容') => { const { left, top, width, height } = position createElement({ @@ -130,6 +149,11 @@ export default () => { }) } + /** + * 创建形状元素 + * @param position 位置大小信息 + * @param data 形状路径信息 + */ const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => { const { left, top, width, height } = position createElement({ @@ -146,6 +170,11 @@ export default () => { }) } + /** + * 创建线条元素 + * @param position 位置大小信息 + * @param data 线条的路径和样式 + */ const createLineElement = (position: LineElementPosition, data: LinePoolItem) => { const { left, top, start, end } = position createElement({ diff --git a/src/hooks/useDeleteElement.ts b/src/hooks/useDeleteElement.ts index d557342e..e405ea02 100644 --- a/src/hooks/useDeleteElement.ts +++ b/src/hooks/useDeleteElement.ts @@ -10,6 +10,7 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() + // 删除全部选中元素 const deleteElement = () => { if (!activeElementIdList.value.length) return const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.id)) @@ -18,6 +19,7 @@ export default () => { addHistorySnapshot() } + // 删除内面内全部元素(无论是否选中) const deleteAllElements = () => { if (!currentSlide.value.elements.length) return store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) diff --git a/src/hooks/useHistorySnapshot.ts b/src/hooks/useHistorySnapshot.ts index 6f851e2d..8797c7c2 100644 --- a/src/hooks/useHistorySnapshot.ts +++ b/src/hooks/useHistorySnapshot.ts @@ -5,14 +5,17 @@ import { ActionTypes, useStore } from '@/store' export default () => { const store = useStore() + // 添加历史快照(历史记录) const addHistorySnapshot = debounce(function() { store.dispatch(ActionTypes.ADD_SNAPSHOT) }, 300, { trailing: true }) + // 重做 const redo = throttle(function() { store.dispatch(ActionTypes.RE_DO) }, 100, { leading: true, trailing: false }) + // 撤销 const undo = throttle(function() { store.dispatch(ActionTypes.UN_DO) }, 100, { leading: true, trailing: false }) diff --git a/src/hooks/useLockElement.ts b/src/hooks/useLockElement.ts index 4d88c241..c7d98e12 100644 --- a/src/hooks/useLockElement.ts +++ b/src/hooks/useLockElement.ts @@ -10,6 +10,7 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() + // 锁定选中的元素,并清空选中元素状态 const lockElement = () => { const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements)) @@ -21,6 +22,10 @@ export default () => { addHistorySnapshot() } + /** + * 解除元素的锁定状态,并将其设置为当前选择元素 + * @param handleElement 需要解锁的元素 + */ const unlockElement = (handleElement: PPTElement) => { const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements)) diff --git a/src/hooks/useMoveElement.ts b/src/hooks/useMoveElement.ts index 80d81a82..55fd0559 100644 --- a/src/hooks/useMoveElement.ts +++ b/src/hooks/useMoveElement.ts @@ -11,22 +11,27 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() - const moveElement = (command: string) => { + /** + * 将元素向指定方向移动指定的距离 + * @param command 移动方向 + * @param step 移动距离 + */ + const moveElement = (command: string, step = 1) => { const newElementList = currentSlide.value.elements.map(el => { if (activeElementIdList.value.includes(el.id)) { let { left, top } = el switch (command) { case KEYS.LEFT: - left = left - 1 + left = left - step break case KEYS.RIGHT: - left = left + 1 + left = left + step break case KEYS.UP: - top = top - 1 + top = top - step break case KEYS.DOWN: - top = top + 1 + top = top + step break default: break } diff --git a/src/hooks/useOrderElement.ts b/src/hooks/useOrderElement.ts index d4d18038..ed6b1fc4 100644 --- a/src/hooks/useOrderElement.ts +++ b/src/hooks/useOrderElement.ts @@ -10,165 +10,190 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() - // 获取组合元素层级范围(组合成员中的最大层级和最小层级) - const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => { - const minIndex = elementList.findIndex(_element => _element.id === combineElementList[0].id) - const maxIndex = elementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id) - return { minIndex, maxIndex } + /** + * 获取组合元素层级范围 + * @param elementList 本页所有元素列表 + * @param combineElementList 组合元素列表 + */ + const getCombineElementLevelRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => { + return { + minLevel: elementList.findIndex(_element => _element.id === combineElementList[0].id), + maxLevel: elementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id), + } } - // 上移一层,返回移动后新的元素列表(下移一层逻辑类似) + /** + * 上移一层 + * @param elementList 本页所有元素列表 + * @param element 当前操作的元素 + */ const moveUpElement = (elementList: PPTElement[], element: PPTElement) => { const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) - // 被操作的元素是组合元素成员 + // 如果被操作的元素是组合元素成员,需要将该组合全部成员一起进行移动 if (element.groupId) { - // 获取该组合元素全部成员,以及组合元素层级范围 + // 获取到该组合全部成员,以及所有成员的层级范围 const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId) - const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList) + const { minLevel, maxLevel } = getCombineElementLevelRange(elementList, combineElementList) - // 无法移动(已经处在顶层) - if (maxIndex === elementList.length - 1) return null + // 已经处在顶层,无法继续移动 + if (maxLevel === elementList.length - 1) return - // 该组合元素上一层的元素,以下简称为【元素next】 - const nextElement = copyOfElementList[maxIndex + 1] + // 通过组合成员范围的最大值,获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表) + // 若上层元素处在另一个组合中,则将上述被移除的组合元素插入到该上层组合上方 + // 若上层元素不处于任何分组中,则将上述被移除的组合元素插入到该上层元素上方 + const nextElement = copyOfElementList[maxLevel + 1] + const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length) - // 从元素列表中移除该组合元素全部成员 - const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length) - - // 如果【元素next】也是组合元素成员(另一个组合,不是上面被移除的那一组,以下简称为【组合next】) - // 需要获取【组合next】全部成员的长度,将上面移除的组合元素全部成员添加到【组合next】全部成员的上方 if (nextElement.groupId) { const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId) - copyOfElementList.splice(minIndex + nextCombineElementList.length, 0, ...movedElementList) + copyOfElementList.splice(minLevel + nextCombineElementList.length, 0, ...movedElementList) } - - // 如果【元素next】是单独的元素(非组合成员),将上面移除的组合元素全部成员添加到【元素next】上方 - else copyOfElementList.splice(minIndex + 1, 0, ...movedElementList) + else copyOfElementList.splice(minLevel + 1, 0, ...movedElementList) } - // 被操作的元素是单独的元素(非组合成员) + // 如果被操作的元素不是组合元素成员 else { - // 元素在元素列表中的层级 - const elementIndex = elementList.findIndex(item => item.id === element.id) + // 获取该元素在列表中的层级 + const level = elementList.findIndex(item => item.id === element.id) - // 无法移动(已经处在顶层) - if (elementIndex === elementList.length - 1) return null + // 已经处在顶层,无法继续移动 + if (level === elementList.length - 1) return - // 上一层的元素,以下简称为【元素next】 - const nextElement = copyOfElementList[elementIndex + 1] + // 获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表) + const nextElement = copyOfElementList[level + 1] + const movedElement = copyOfElementList.splice(level, 1)[0] - // 从元素列表中移除被操作的元素 - const movedElement = copyOfElementList.splice(elementIndex, 1)[0] - - // 如果【元素next】是组合元素成员 - // 需要获取该组合全部成员的长度,将上面移除的元素添加到该组合全部成员的上方 + // 通过组合成员范围的最大值,获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表) + // 若上层元素处在另一个组合中,则将上述被移除的组合元素插入到该上层组合上方 + // 若上层元素不处于任何分组中,则将上述被移除的组合元素插入到该上层元素上方 if (nextElement.groupId) { const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId) - copyOfElementList.splice(elementIndex + combineElementList.length, 0, movedElement) + copyOfElementList.splice(level + combineElementList.length, 0, movedElement) } - - // 如果【元素next】是单独的元素(非组合成员),将上面移除的元素添加到【元素next】上方 - else copyOfElementList.splice(elementIndex + 1, 0, movedElement) + else copyOfElementList.splice(level + 1, 0, movedElement) } return copyOfElementList } - // 下移一层 + /** + * 下移一层,操作方式同上移 + * @param elementList 本页所有元素列表 + * @param element 当前操作的元素 + */ const moveDownElement = (elementList: PPTElement[], element: PPTElement) => { const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) if (element.groupId) { const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId) - const { minIndex } = getCombineElementIndexRange(elementList, combineElementList) - if (minIndex === 0) return null - const prevElement = copyOfElementList[minIndex - 1] - const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length) + const { minLevel } = getCombineElementLevelRange(elementList, combineElementList) + if (minLevel === 0) return + + const prevElement = copyOfElementList[minLevel - 1] + const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length) + if (prevElement.groupId) { const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId) - copyOfElementList.splice(minIndex - prevCombineElementList.length, 0, ...movedElementList) + copyOfElementList.splice(minLevel - prevCombineElementList.length, 0, ...movedElementList) } - else copyOfElementList.splice(minIndex - 1, 0, ...movedElementList) + else copyOfElementList.splice(minLevel - 1, 0, ...movedElementList) } else { - const elementIndex = elementList.findIndex(item => item.id === element.id) - if (elementIndex === 0) return null - const prevElement = copyOfElementList[elementIndex - 1] - const movedElement = copyOfElementList.splice(elementIndex, 1)[0] + const level = elementList.findIndex(item => item.id === element.id) + if (level === 0) return + + const prevElement = copyOfElementList[level - 1] + const movedElement = copyOfElementList.splice(level, 1)[0] + if (prevElement.groupId) { const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId) - copyOfElementList.splice(elementIndex - combineElementList.length, 0, movedElement) + copyOfElementList.splice(level - combineElementList.length, 0, movedElement) } - else copyOfElementList.splice(elementIndex - 1, 0, movedElement) + else copyOfElementList.splice(level - 1, 0, movedElement) } return copyOfElementList } - // 置顶层,返回移动后新的元素列表(置底层逻辑类似) + /** + * 置顶层 + * @param elementList 本页所有元素列表 + * @param element 当前操作的元素 + */ const moveTopElement = (elementList: PPTElement[], element: PPTElement) => { const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) - // 被操作的元素是组合元素成员 + // 如果被操作的元素是组合元素成员,需要将该组合全部成员一起进行移动 if (element.groupId) { - // 获取该组合元素全部成员,以及组合元素层级范围 + // 获取到该组合全部成员,以及所有成员的层级范围 const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId) - const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList) + const { minLevel, maxLevel } = getCombineElementLevelRange(elementList, combineElementList) - // 无法移动(已经处在顶层) - if (maxIndex === elementList.length - 1) return null + // 已经处在顶层,无法继续移动 + if (maxLevel === elementList.length - 1) return null - // 从元素列表中移除该组合元素全部成员,然后添加到元素列表最上方 - const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length) + // 将该组合元素从元素列表中移除,然后将被移除的元素添加到元素列表顶部 + const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length) copyOfElementList.push(...movedElementList) } - // 被操作的元素是单独的元素(非组合成员) + // 如果被操作的元素不是组合元素成员 else { - // 元素在元素列表中的层级 - const elementIndex = elementList.findIndex(item => item.id === element.id) + // 获取该元素在列表中的层级 + const level = elementList.findIndex(item => item.id === element.id) - // 无法移动(已经处在顶层) - if (elementIndex === elementList.length - 1) return null + // 已经处在顶层,无法继续移动 + if (level === elementList.length - 1) return null - // 从元素列表中移除该元素,然后添加到元素列表最上方 - copyOfElementList.splice(elementIndex, 1) + // 将该组合元素从元素列表中移除,然后将被移除的元素添加到元素列表底部 + copyOfElementList.splice(level, 1) copyOfElementList.push(element) } return copyOfElementList } - // 置底层 + /** + * 置底层,操作方式同置顶 + * @param elementList 本页所有元素列表 + * @param element 当前操作的元素 + */ const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => { const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) if (element.groupId) { const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId) - const { minIndex } = getCombineElementIndexRange(elementList, combineElementList) - if (minIndex === 0) return null - const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length) + const { minLevel } = getCombineElementLevelRange(elementList, combineElementList) + if (minLevel === 0) return + + const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length) copyOfElementList.unshift(...movedElementList) } else { - const elementIndex = elementList.findIndex(item => item.id === element.id) - if (elementIndex === 0) return null - copyOfElementList.splice(elementIndex, 1) + const level = elementList.findIndex(item => item.id === element.id) + if (level === 0) return + + copyOfElementList.splice(level, 1) copyOfElementList.unshift(element) } return copyOfElementList } + /** + * 调整元素层级 + * @param element 需要调整层级的元素 + * @param command 调整命令:上移、下移、置顶、置底 + */ const orderElement = (element: PPTElement, command: ElementOrderCommand) => { - let newElementList = null + let newElementList if (command === ElementOrderCommands.UP) newElementList = moveUpElement(currentSlide.value.elements, element) else if (command === ElementOrderCommands.DOWN) newElementList = moveDownElement(currentSlide.value.elements, element) diff --git a/src/hooks/usePasteTextClipboardData.ts b/src/hooks/usePasteTextClipboardData.ts index b6f3ad92..4ff66d11 100644 --- a/src/hooks/usePasteTextClipboardData.ts +++ b/src/hooks/usePasteTextClipboardData.ts @@ -18,6 +18,10 @@ export default () => { const { addHistorySnapshot } = useHistorySnapshot() const { createTextElement } = useCreateElement() + /** + * 粘贴元素(一组) + * @param elements 元素列表数据 + */ const pasteElement = (elements: PPTElement[]) => { const groupIdMap = {} const elIdMap = {} @@ -47,6 +51,10 @@ export default () => { addHistorySnapshot() } + /** + * 粘贴页面 + * @param slide 页面数据 + */ const pasteSlide = (slide: Slide) => { store.commit(MutationTypes.ADD_SLIDE, { ...slide, @@ -55,6 +63,10 @@ export default () => { addHistorySnapshot() } + /** + * 粘贴普通文本:创建为新的文本元素 + * @param text 文本 + */ const pasteText = (text: string) => { createTextElement({ left: 0, @@ -64,6 +76,11 @@ export default () => { }, text) } + /** + * 解析剪贴板内容,根据解析结果选择合适的粘贴方式 + * @param text 剪贴板内容 + * @param options 配置项:onlySlide -- 仅处理页面粘贴;onlyElements -- 仅处理元素粘贴; + */ const pasteTextClipboardData = (text: string, options?: PasteTextClipboardDataOptions) => { const onlySlide = options?.onlySlide || false const onlyElements = options?.onlyElements || false @@ -76,7 +93,7 @@ export default () => { clipboardData = text } - // 粘贴自定义元素或页面 + // 元素或页面 if (typeof clipboardData === 'object') { const { type, data } = clipboardData @@ -84,7 +101,7 @@ export default () => { else if (type === 'slide' && !onlyElements) pasteSlide(data) } - // 粘贴普通文本 + // 普通文本 else if (!onlyElements && !onlySlide) pasteText(clipboardData) } diff --git a/src/hooks/useScaleCanvas.ts b/src/hooks/useScaleCanvas.ts index fb20db6c..a7302699 100644 --- a/src/hooks/useScaleCanvas.ts +++ b/src/hooks/useScaleCanvas.ts @@ -5,6 +5,10 @@ export default () => { const store = useStore() const canvasPercentage = computed(() => store.state.canvasPercentage) + /** + * 缩放画布百分比 + * @param command 缩放命令:放大、缩小 + */ const scaleCanvas = (command: '+' | '-') => { let percentage = canvasPercentage.value const step = 5 @@ -16,6 +20,10 @@ export default () => { store.commit(MutationTypes.SET_CANVAS_PERCENTAGE, percentage) } + /** + * 设置画笔百分比 + * @param percentage 百分比(小数形式,如0.8) + */ const setCanvasPercentage = (percentage: number) => { store.commit(MutationTypes.SET_CANVAS_PERCENTAGE, percentage) } diff --git a/src/hooks/useScreening.ts b/src/hooks/useScreening.ts index c4fd293c..563d8ede 100644 --- a/src/hooks/useScreening.ts +++ b/src/hooks/useScreening.ts @@ -4,16 +4,19 @@ import { enterFullscreen, exitFullscreen, isFullscreen } from '@/utils/fullscree export default () => { const store = useStore() + // 进入放映状态(从当前页开始) const enterScreening = () => { enterFullscreen() store.commit(MutationTypes.SET_SCREENING, true) } + // 进入放映状态(从第一页开始) const enterScreeningFromStart = () => { store.commit(MutationTypes.UPDATE_SLIDE_INDEX, 0) enterScreening() } + // 退出放映状态 const exitScreening = () => { store.commit(MutationTypes.SET_SCREENING, false) if (isFullscreen()) exitFullscreen() diff --git a/src/hooks/useSelectAllElement.ts b/src/hooks/useSelectAllElement.ts index 688f3c2e..28971a23 100644 --- a/src/hooks/useSelectAllElement.ts +++ b/src/hooks/useSelectAllElement.ts @@ -6,6 +6,7 @@ export default () => { const store = useStore() const currentSlide = computed(() => store.getters.currentSlide) + // 将当前页面全部元素设置为被选择状态 const selectAllElement = () => { const unlockedElements = currentSlide.value.elements.filter(el => !el.lock) const newActiveElementIdList = unlockedElements.map(el => el.id) diff --git a/src/hooks/useSlideBackgroundStyle.ts b/src/hooks/useSlideBackgroundStyle.ts index 9e227ae6..3a0f4607 100644 --- a/src/hooks/useSlideBackgroundStyle.ts +++ b/src/hooks/useSlideBackgroundStyle.ts @@ -1,6 +1,7 @@ import { Ref, computed } from 'vue' import { SlideBackground } from '@/types/slides' +// 将页面背景数据转换为css样式 export default (background: Ref) => { const backgroundStyle = computed(() => { if (!background.value) return { backgroundColor: '#fff' } @@ -15,7 +16,11 @@ export default (background: Ref) => { gradientType, } = background.value + // 纯色背景 if (type === 'solid') return { backgroundColor: color } + + // 背景图模式 + // 包括:背景图、背景大小,是否重复 else if (type === 'image') { if (!image) return { backgroundColor: '#fff' } if (imageSize === 'repeat') { @@ -31,6 +36,8 @@ export default (background: Ref) => { backgroundSize: imageSize || 'cover', } } + + // 渐变色背景 else if (type === 'gradient') { const rotate = gradientRotate || 0 const color1 = gradientColor ? gradientColor[0] : '#fff' diff --git a/src/hooks/useSlideHandler.ts b/src/hooks/useSlideHandler.ts index 66fee464..739e3bf1 100644 --- a/src/hooks/useSlideHandler.ts +++ b/src/hooks/useSlideHandler.ts @@ -19,6 +19,10 @@ export default () => { const { pasteTextClipboardData } = usePasteTextClipboardData() const { addHistorySnapshot } = useHistorySnapshot() + /** + * 移动页面焦点 + * @param command 移动页面焦点命令:上移、下移 + */ const updateSlideIndex = (command: string) => { let targetIndex = 0 if (command === KEYS.UP && slideIndex.value > 0) { @@ -30,6 +34,7 @@ export default () => { store.commit(MutationTypes.UPDATE_SLIDE_INDEX, targetIndex) } + // 将当前页面数据加密后复制到剪贴板 const copySlide = () => { const text = encrypt(JSON.stringify({ type: 'slide', @@ -41,12 +46,14 @@ export default () => { }) } + // 尝试将剪贴板页面数据解密后添加到下一页(粘贴) const pasteSlide = () => { readClipboard().then(text => { pasteTextClipboardData(text, { onlySlide: true }) }).catch(err => message.warning(err)) } + // 创建一页空白页并添加到下一页 const createSlide = () => { const emptySlide = { id: createRandomCode(8), @@ -60,6 +67,7 @@ export default () => { addHistorySnapshot() } + // 将当前页复制一份到下一页 const copyAndPasteSlide = () => { store.commit(MutationTypes.ADD_SLIDE, { ...currentSlide.value, @@ -68,6 +76,7 @@ export default () => { addHistorySnapshot() } + // 删除当前页 const deleteSlide = () => { if (slidesLength.value === 1) return message.warning('无法继续删除') @@ -75,6 +84,7 @@ export default () => { addHistorySnapshot() } + // 将当前页复制后删除(剪切) const cutSlide = () => { copySlide() deleteSlide() diff --git a/src/plugins/iconPark.ts b/src/plugins/iconPark.ts index a04a1951..d39ea2b3 100644 --- a/src/plugins/iconPark.ts +++ b/src/plugins/iconPark.ts @@ -1,3 +1,5 @@ +// https://iconpark.bytedance.com/official + import { App } from 'vue' import { PlayOne, @@ -24,7 +26,6 @@ import { BackgroundColor, Group, Ungroup, - ClearFormat, Back, Next, Fullwidth, @@ -160,7 +161,6 @@ export default { app.component('IconSearch', Search) app.component('IconPpt', Ppt) app.component('IconHelpcenter', Helpcenter) - app.component('IconClearFormat', ClearFormat) app.component('IconGithub', Github) app.component('IconWrite', Write) app.component('IconEffects', Effects)