mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2026-09-01 15:07:16 +08:00
perf: 表格行列操作优化,气泡菜单补充
This commit is contained in:
Vendored
+2
@@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
'ICustom:filePptist': typeof import('~icons/custom/file-pptist')['default']
|
||||
'ICustom:left': typeof import('~icons/custom/left')['default']
|
||||
'ICustom:right': typeof import('~icons/custom/right')['default']
|
||||
'IIconParkOutline:add': typeof import('~icons/icon-park-outline/add')['default']
|
||||
'IIconParkOutline:addText': typeof import('~icons/icon-park-outline/add-text')['default']
|
||||
'IIconParkOutline:alignBottom': typeof import('~icons/icon-park-outline/align-bottom')['default']
|
||||
'IIconParkOutline:alignHorizontally': typeof import('~icons/icon-park-outline/align-horizontally')['default']
|
||||
@@ -109,6 +110,7 @@ declare module 'vue' {
|
||||
'IIconParkOutline:previewOpen': typeof import('~icons/icon-park-outline/preview-open')['default']
|
||||
'IIconParkOutline:quote': typeof import('~icons/icon-park-outline/quote')['default']
|
||||
'IIconParkOutline:radarChart': typeof import('~icons/icon-park-outline/radar-chart')['default']
|
||||
'IIconParkOutline:reduce': typeof import('~icons/icon-park-outline/reduce')['default']
|
||||
'IIconParkOutline:refresh': typeof import('~icons/icon-park-outline/refresh')['default']
|
||||
'IIconParkOutline:right': typeof import('~icons/icon-park-outline/right')['default']
|
||||
'IIconParkOutline:rotate': typeof import('~icons/icon-park-outline/rotate')['default']
|
||||
|
||||
@@ -2,6 +2,7 @@ import mitt, { type Emitter } from 'mitt'
|
||||
|
||||
export const enum EmitterEvents {
|
||||
RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND',
|
||||
TABLE_COMMAND = 'TABLE_COMMAND',
|
||||
SYNC_RICH_TEXT_ATTRS_TO_STORE = 'SYNC_RICH_TEXT_ATTRS_TO_STORE',
|
||||
OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR',
|
||||
OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR',
|
||||
@@ -17,8 +18,15 @@ export interface RichTextCommand {
|
||||
action: RichTextAction | RichTextAction[]
|
||||
}
|
||||
|
||||
export interface TableCommand {
|
||||
targetId: string
|
||||
command: 'insert-row' | 'insert-col' | 'delete-row' | 'delete-col'
|
||||
position?: 'before' | 'after'
|
||||
}
|
||||
|
||||
type Events = {
|
||||
[EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand
|
||||
[EmitterEvents.TABLE_COMMAND]: TableCommand
|
||||
[EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE]: void
|
||||
[EmitterEvents.OPEN_CHART_DATA_EDITOR]: void
|
||||
[EmitterEvents.OPEN_LATEX_EDITOR]: void
|
||||
@@ -26,4 +34,4 @@ type Events = {
|
||||
|
||||
const emitter: Emitter<Events> = mitt<Events>()
|
||||
|
||||
export default emitter
|
||||
export default emitter
|
||||
|
||||
@@ -10,6 +10,35 @@
|
||||
</button>
|
||||
</Popover>
|
||||
<BorderPanel />
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<div class="table-command-menu">
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'before')">上方插入行</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'after')">下方插入行</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'before')">左侧插入列</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'after')">右侧插入列</PopoverMenuItem>
|
||||
</div>
|
||||
</template>
|
||||
<button class="toolbar-btn">
|
||||
<i-icon-park-outline:add class="icon" />
|
||||
<span>添加</span>
|
||||
</button>
|
||||
</Popover>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<div class="table-command-menu">
|
||||
<PopoverMenuItem center @click="emitTableCommand('delete-row')">删除行</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('delete-col')">删除列</PopoverMenuItem>
|
||||
</div>
|
||||
</template>
|
||||
<button class="toolbar-btn">
|
||||
<i-icon-park-outline:reduce class="icon" />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,13 +47,15 @@ import { computed, type Ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMainStore, useSlidesStore } from '@/store'
|
||||
import type { PPTTableElement, TableCell, TableCellStyle } from '@/types/slides'
|
||||
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import BorderPanel from './BorderPanel.vue'
|
||||
import Popover from '@/components/Popover.vue'
|
||||
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
|
||||
import ColorPicker from '@/components/ColorPicker/index.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
elementInfo: PPTTableElement
|
||||
}>()
|
||||
|
||||
@@ -49,6 +80,14 @@ const cellBackcolor = computed(() => {
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const emitTableCommand = (command: TableCommand['command'], position?: TableCommand['position']) => {
|
||||
emitter.emit(EmitterEvents.TABLE_COMMAND, {
|
||||
targetId: props.elementInfo.id,
|
||||
command,
|
||||
position,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCellBackcolor = (backcolor: string) => {
|
||||
const el = handleTableElement.value
|
||||
if (!el || el.type !== 'table') return
|
||||
@@ -105,4 +144,14 @@ const updateCellBackcolor = (backcolor: string) => {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: $borderColor;
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.table-command-menu {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -113,20 +113,32 @@
|
||||
<Divider />
|
||||
|
||||
<div class="row">
|
||||
<div style="width: 40%;">行数:</div>
|
||||
<div class="set-count" style="width: 60%;">
|
||||
<Button class="btn" :disabled="rowCount <= 1" @click="setTableRow(rowCount - 1)"><i-icon-park-outline:minus /></Button>
|
||||
<div class="count-text">{{rowCount}}</div>
|
||||
<Button class="btn" :disabled="rowCount >= 30" @click="setTableRow(rowCount + 1)"><i-icon-park-outline:plus /></Button>
|
||||
</div>
|
||||
<div style="width: 40%;">操作行:</div>
|
||||
<ButtonGroup style="width: 60%;" passive>
|
||||
<Button first style="flex: 1;" @click="emitTableCommand('insert-row', 'after')">添加行</Button>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'before')">上方添加</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'after')">下方添加</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('delete-row')">删除行</PopoverMenuItem>
|
||||
</template>
|
||||
<Button last class="popover-btn"><i-icon-park-outline:down /></Button>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="width: 40%;">列数:</div>
|
||||
<div class="set-count" style="width: 60%;">
|
||||
<Button class="btn" :disabled="colCount <= 1" @click="setTableCol(colCount - 1)"><i-icon-park-outline:minus /></Button>
|
||||
<div class="count-text">{{colCount}}</div>
|
||||
<Button class="btn" :disabled="colCount >= 30" @click="setTableCol(colCount + 1)"><i-icon-park-outline:plus /></Button>
|
||||
</div>
|
||||
<div style="width: 40%;">操作列:</div>
|
||||
<ButtonGroup style="width: 60%;" passive>
|
||||
<Button first style="flex: 1;" @click="emitTableCommand('insert-col', 'after')">添加列</Button>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'before')">左侧添加</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'after')">右侧添加</PopoverMenuItem>
|
||||
<PopoverMenuItem center @click="emitTableCommand('delete-col')">删除列</PopoverMenuItem>
|
||||
</template>
|
||||
<Button last class="popover-btn"><i-icon-park-outline:down /></Button>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
@@ -185,10 +197,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useMainStore, useSlidesStore } from '@/store'
|
||||
import type { PPTTableElement, TableCell, TableCellStyle, TableTheme, TextAlign, TextAlignVertical } from '@/types/slides'
|
||||
import { FONTS } from '@/configs/font'
|
||||
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ElementOutline from '../common/ElementOutline.vue'
|
||||
@@ -206,6 +218,7 @@ import RadioGroup from '@/components/RadioGroup.vue'
|
||||
import Select from '@/components/Select.vue'
|
||||
import SelectGroup from '@/components/SelectGroup.vue'
|
||||
import Popover from '@/components/Popover.vue'
|
||||
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
|
||||
|
||||
const slidesStore = useSlidesStore()
|
||||
const { handleElement, handleElementId, selectedTableCells: selectedCells } = storeToRefs(useMainStore())
|
||||
@@ -230,22 +243,12 @@ const textAttrs = ref({
|
||||
|
||||
const theme = ref<TableTheme>()
|
||||
const hasTheme = ref(false)
|
||||
const rowCount = ref(0)
|
||||
const colCount = ref(0)
|
||||
const minRowCount = ref(0)
|
||||
const minColCount = ref(0)
|
||||
|
||||
watch(handleElement, () => {
|
||||
if (!handleElement.value || handleElement.value.type !== 'table') return
|
||||
|
||||
theme.value = handleElement.value.theme
|
||||
hasTheme.value = !!theme.value
|
||||
|
||||
rowCount.value = handleElement.value.data.length
|
||||
colCount.value = handleElement.value.data[0].length
|
||||
|
||||
minRowCount.value = handleElement.value.data.length
|
||||
minColCount.value = handleElement.value.data[0].length
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
@@ -349,67 +352,12 @@ const toggleTheme = (checked: boolean) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表格行数
|
||||
const setTableRow = (value: number) => {
|
||||
const _handleElement = handleElement.value as PPTTableElement
|
||||
const rowCount = _handleElement.data.length
|
||||
|
||||
if (value > rowCount) {
|
||||
const newTableCells: TableCell[][] = []
|
||||
for (let i = 0; i < value - rowCount; i++) {
|
||||
const rowCells: TableCell[] = []
|
||||
for (let j = 0; j < colCount.value; j++) {
|
||||
rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
|
||||
}
|
||||
newTableCells.push(rowCells)
|
||||
}
|
||||
|
||||
const tableCells: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
|
||||
tableCells.push(...newTableCells)
|
||||
|
||||
updateElement({ data: tableCells })
|
||||
}
|
||||
else {
|
||||
const tableCells: TableCell[][] = _handleElement.data.slice(0, value)
|
||||
updateElement({ data: tableCells })
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表格列数
|
||||
const setTableCol = (value: number) => {
|
||||
const _handleElement = handleElement.value as PPTTableElement
|
||||
const colCount = _handleElement.data[0].length
|
||||
|
||||
let tableCells = _handleElement.data
|
||||
let colSizeList = _handleElement.colWidths.map(item => item * _handleElement.width)
|
||||
|
||||
if (value > colCount) {
|
||||
tableCells = tableCells.map(item => {
|
||||
const cells: TableCell[] = []
|
||||
for (let i = 0; i < value - colCount; i++) {
|
||||
cells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
|
||||
}
|
||||
item.push(...cells)
|
||||
return item
|
||||
})
|
||||
|
||||
const newColSizeList: number[] = new Array(value - colCount).fill(100)
|
||||
colSizeList.push(...newColSizeList)
|
||||
}
|
||||
else {
|
||||
tableCells = tableCells.map(item => item.slice(0, value))
|
||||
colSizeList = colSizeList.slice(0, value)
|
||||
}
|
||||
|
||||
const width = colSizeList.reduce((a, b) => a + b)
|
||||
const colWidths = colSizeList.map(item => item / width)
|
||||
|
||||
const props = {
|
||||
width,
|
||||
data: tableCells,
|
||||
colWidths,
|
||||
}
|
||||
updateElement(props)
|
||||
const emitTableCommand = (command: TableCommand['command'], position?: TableCommand['position']) => {
|
||||
emitter.emit(EmitterEvents.TABLE_COMMAND, {
|
||||
targetId: handleElementId.value,
|
||||
command,
|
||||
position,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -426,19 +374,7 @@ const setTableCol = (value: number) => {
|
||||
.switch-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
.set-count {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.count-text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.popover-btn {
|
||||
padding: 0 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,6 +70,8 @@ import { useMainStore } from '@/store'
|
||||
import type { PPTElementOutline, TableCell, TableTheme } from '@/types/slides'
|
||||
import type { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { KEYS } from '@/configs/hotkey'
|
||||
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
|
||||
import message from '@/utils/message'
|
||||
import { getCellStyle, getTextStyle, formatText } from './utils'
|
||||
import useHideCells from './useHideCells'
|
||||
import useSubThemeColor from './useSubThemeColor'
|
||||
@@ -77,6 +79,7 @@ import useSubThemeColor from './useSubThemeColor'
|
||||
import CustomTextarea from './CustomTextarea.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
elementId: string
|
||||
data: TableCell[][]
|
||||
width: number
|
||||
cellMinHeight: number
|
||||
@@ -659,6 +662,49 @@ const checkCanDeleteRowOrCol = () => {
|
||||
return { canDeleteRow, canDeleteCol }
|
||||
}
|
||||
|
||||
const getCommandTargetCell = () => {
|
||||
const selectedCell = selectedCells.value.find(cell => !hideCells.value.includes(cell))
|
||||
if (!selectedCell) return null
|
||||
|
||||
const [rowIndex, colIndex] = selectedCell.split('_').map(Number)
|
||||
return { rowIndex, colIndex }
|
||||
}
|
||||
|
||||
const execTableCommand = (payload: TableCommand) => {
|
||||
if (payload.targetId !== props.elementId) return
|
||||
|
||||
const targetCell = getCommandTargetCell()
|
||||
const isBefore = payload.position === 'before'
|
||||
|
||||
if (payload.command === 'insert-row') {
|
||||
const rowIndex = targetCell ? (isBefore ? targetCell.rowIndex : targetCell.rowIndex + 1) : (isBefore ? 0 : tableCells.value.length)
|
||||
return insertRow(rowIndex)
|
||||
}
|
||||
if (payload.command === 'insert-col') {
|
||||
const colCount = tableCells.value[0]?.length || 0
|
||||
const colIndex = targetCell ? (isBefore ? targetCell.colIndex : targetCell.colIndex + 1) : (isBefore ? 0 : colCount)
|
||||
return insertCol(colIndex)
|
||||
}
|
||||
const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()
|
||||
|
||||
if (payload.command === 'delete-row') {
|
||||
if (!canDeleteRow) return message.warning('表格至少保留一行')
|
||||
return deleteRow(targetCell ? targetCell.rowIndex : tableCells.value.length - 1)
|
||||
}
|
||||
if (payload.command === 'delete-col') {
|
||||
if (!canDeleteCol) return message.warning('表格至少保留一列')
|
||||
const colCount = tableCells.value[0]?.length || 1
|
||||
return deleteCol(targetCell ? targetCell.colIndex : colCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EmitterEvents.TABLE_COMMAND, execTableCommand)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
emitter.off(EmitterEvents.TABLE_COMMAND, execTableCommand)
|
||||
})
|
||||
|
||||
// 检查是否可以合并或拆分
|
||||
// 必须多选才可以合并
|
||||
// 必须单选且所选单元格为合并单元格才可以拆分
|
||||
@@ -835,4 +881,4 @@ table {
|
||||
z-index: 2;
|
||||
cursor: col-resize;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
>
|
||||
<EditableTable
|
||||
@mousedown.stop
|
||||
:elementId="elementInfo.id"
|
||||
:data="elementInfo.data"
|
||||
:width="elementInfo.width"
|
||||
:cellMinHeight="elementInfo.cellMinHeight"
|
||||
|
||||
Reference in New Issue
Block a user