EditableTable.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. <template>
  2. <div
  3. class="editable-table"
  4. :style="{ width: totalWidth + 'px' }"
  5. >
  6. <div class="handler" v-if="editable">
  7. <div
  8. class="drag-line"
  9. v-for="(pos, index) in dragLinePosition"
  10. :key="index"
  11. :style="{ left: pos + 'px' }"
  12. @mousedown="$event => handleMousedownColHandler($event, index)"
  13. ></div>
  14. </div>
  15. <table
  16. :class="{
  17. 'theme': theme,
  18. 'row-header': theme?.rowHeader,
  19. 'row-footer': theme?.rowFooter,
  20. 'col-header': theme?.colHeader,
  21. 'col-footer': theme?.colFooter,
  22. }"
  23. :style="`--themeColor: ${theme?.color}; --subThemeColor1: ${subThemeColor[0]}; --subThemeColor2: ${subThemeColor[1]}`"
  24. >
  25. <colgroup>
  26. <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
  27. </colgroup>
  28. <tbody>
  29. <tr v-for="(rowCells, rowIndex) in tableCells" :key="rowIndex" :style="{ height: cellMinHeight + 'px' }">
  30. <td
  31. class="cell"
  32. :class="{
  33. 'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
  34. 'active': activedCell === `${rowIndex}_${colIndex}`,
  35. }"
  36. :style="{
  37. borderStyle: outline.style,
  38. borderColor: outline.color,
  39. borderWidth: outline.width + 'px',
  40. ...getTextStyle(cell.style),
  41. }"
  42. v-for="(cell, colIndex) in rowCells"
  43. :key="cell.id"
  44. :rowspan="cell.rowspan"
  45. :colspan="cell.colspan"
  46. :data-cell-index="`${rowIndex}_${colIndex}`"
  47. v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
  48. @mousedown="$event => handleCellMousedown($event, rowIndex, colIndex)"
  49. @mouseenter="handleCellMouseenter(rowIndex, colIndex)"
  50. v-contextmenu="(el: HTMLElement) => contextmenus(el)"
  51. >
  52. <CustomTextarea
  53. v-if="activedCell === `${rowIndex}_${colIndex}`"
  54. class="cell-text"
  55. :class="{ 'active': activedCell === `${rowIndex}_${colIndex}` }"
  56. :style="{ minHeight: (cellMinHeight - 4) + 'px' }"
  57. :value="cell.text"
  58. @updateValue="value => handleInput(value, rowIndex, colIndex)"
  59. @insertExcelData="value => insertExcelData(value, rowIndex, colIndex)"
  60. />
  61. <div v-else class="cell-text" :style="{ minHeight: (cellMinHeight - 4) + 'px' }" v-html="formatText(cell.text)" />
  62. </td>
  63. </tr>
  64. </tbody>
  65. </table>
  66. </div>
  67. </template>
  68. <script lang="ts" setup>
  69. import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
  70. import { debounce, isEqual } from 'lodash'
  71. import { storeToRefs } from 'pinia'
  72. import { nanoid } from 'nanoid'
  73. import { useMainStore } from '@/store'
  74. import { lang } from '@/main'
  75. import type { PPTElementOutline, TableCell, TableTheme } from '@/types/slides'
  76. import type { ContextmenuItem } from '@/components/Contextmenu/types'
  77. import { KEYS } from '@/configs/hotkey'
  78. import { getTextStyle, formatText } from './utils'
  79. import useHideCells from './useHideCells'
  80. import useSubThemeColor from './useSubThemeColor'
  81. import CustomTextarea from './CustomTextarea.vue'
  82. const props = withDefaults(defineProps<{
  83. data: TableCell[][]
  84. width: number
  85. cellMinHeight: number
  86. colWidths: number[]
  87. outline: PPTElementOutline
  88. theme?: TableTheme
  89. editable?: boolean
  90. }>(), {
  91. editable: true,
  92. })
  93. const emit = defineEmits<{
  94. (event: 'change', payload: TableCell[][]): void
  95. (event: 'changeColWidths', payload: number[]): void
  96. (event: 'changeSelectedCells', payload: string[]): void
  97. }>()
  98. const { canvasScale } = storeToRefs(useMainStore())
  99. const isStartSelect = ref(false)
  100. const startCell = ref<number[]>([])
  101. const endCell = ref<number[]>([])
  102. const tableCells = computed<TableCell[][]>({
  103. get() {
  104. return props.data
  105. },
  106. set(newData) {
  107. emit('change', newData)
  108. },
  109. })
  110. // 主题辅助色
  111. const theme = computed(() => props.theme)
  112. const { subThemeColor } = useSubThemeColor(theme)
  113. // 计算表格每一列的列宽和总宽度
  114. const colSizeList = ref<number[]>([])
  115. const totalWidth = computed(() => colSizeList.value.reduce((a, b) => a + b))
  116. watch([
  117. () => props.colWidths,
  118. () => props.width,
  119. ], () => {
  120. colSizeList.value = props.colWidths.map(item => item * props.width)
  121. }, { immediate: true })
  122. // 清除全部单元格的选中状态
  123. // 表格处于不可编辑状态时也需要清除
  124. const removeSelectedCells = () => {
  125. startCell.value = []
  126. endCell.value = []
  127. }
  128. watch(() => props.editable, () => {
  129. if (!props.editable) removeSelectedCells()
  130. })
  131. // 用于拖拽列宽的操作节点位置
  132. const dragLinePosition = computed(() => {
  133. const dragLinePosition: number[] = []
  134. for (let i = 1; i < colSizeList.value.length + 1; i++) {
  135. const pos = colSizeList.value.slice(0, i).reduce((a, b) => (a + b))
  136. dragLinePosition.push(pos)
  137. }
  138. return dragLinePosition
  139. })
  140. // 无效的单元格位置(被合并的单元格位置)集合
  141. const cells = computed(() => props.data)
  142. const { hideCells } = useHideCells(cells)
  143. // 当前选中的单元格集合
  144. const selectedCells = computed(() => {
  145. if (!startCell.value.length) return []
  146. const [startX, startY] = startCell.value
  147. if (!endCell.value.length) return [`${startX}_${startY}`]
  148. const [endX, endY] = endCell.value
  149. if (startX === endX && startY === endY) return [`${startX}_${startY}`]
  150. const selectedCells = []
  151. const minX = Math.min(startX, endX)
  152. const minY = Math.min(startY, endY)
  153. const maxX = Math.max(startX, endX)
  154. const maxY = Math.max(startY, endY)
  155. for (let i = 0; i < tableCells.value.length; i++) {
  156. const rowCells = tableCells.value[i]
  157. for (let j = 0; j < rowCells.length; j++) {
  158. if (i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)
  159. }
  160. }
  161. return selectedCells
  162. })
  163. watch(selectedCells, (value, oldValue) => {
  164. if (isEqual(value, oldValue)) return
  165. emit('changeSelectedCells', selectedCells.value)
  166. })
  167. // 当前激活的单元格:当且仅当只有一个选中单元格时,该单元格为激活的单元格
  168. const activedCell = computed(() => {
  169. if (selectedCells.value.length > 1) return null
  170. return selectedCells.value[0]
  171. })
  172. // 设置选中单元格状态(鼠标点击或拖选)
  173. const handleMouseup = () => isStartSelect.value = false
  174. const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
  175. if (e.button === 0) {
  176. endCell.value = []
  177. isStartSelect.value = true
  178. startCell.value = [rowIndex, colIndex]
  179. }
  180. }
  181. const handleCellMouseenter = (rowIndex: number, colIndex: number) => {
  182. if (!isStartSelect.value) return
  183. endCell.value = [rowIndex, colIndex]
  184. }
  185. onMounted(() => {
  186. document.addEventListener('mouseup', handleMouseup)
  187. })
  188. onUnmounted(() => {
  189. document.removeEventListener('mouseup', handleMouseup)
  190. })
  191. // 判断某位置是否为无效单元格(被合并掉的位置)
  192. const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
  193. // 选中指定的列
  194. const selectCol = (index: number) => {
  195. const maxRow = tableCells.value.length - 1
  196. startCell.value = [0, index]
  197. endCell.value = [maxRow, index]
  198. }
  199. // 选中指定的行
  200. const selectRow = (index: number) => {
  201. const maxCol = tableCells.value[index].length - 1
  202. startCell.value = [index, 0]
  203. endCell.value = [index, maxCol]
  204. }
  205. // 选中全部单元格
  206. const selectAll = () => {
  207. const maxRow = tableCells.value.length - 1
  208. const maxCol = tableCells.value[maxRow].length - 1
  209. startCell.value = [0, 0]
  210. endCell.value = [maxRow, maxCol]
  211. }
  212. // 删除一行
  213. const deleteRow = (rowIndex: number) => {
  214. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  215. const targetCells = tableCells.value[rowIndex]
  216. const hideCellsPos = []
  217. for (let i = 0; i < targetCells.length; i++) {
  218. if (isHideCell(rowIndex, i)) hideCellsPos.push(i)
  219. }
  220. for (const pos of hideCellsPos) {
  221. for (let i = rowIndex; i >= 0; i--) {
  222. if (!isHideCell(i, pos)) {
  223. _tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1
  224. break
  225. }
  226. }
  227. }
  228. _tableCells.splice(rowIndex, 1)
  229. tableCells.value = _tableCells
  230. }
  231. // 删除一列
  232. const deleteCol = (colIndex: number) => {
  233. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  234. const hideCellsPos = []
  235. for (let i = 0; i < tableCells.value.length; i++) {
  236. if (isHideCell(i, colIndex)) hideCellsPos.push(i)
  237. }
  238. for (const pos of hideCellsPos) {
  239. for (let i = colIndex; i >= 0; i--) {
  240. if (!isHideCell(pos, i)) {
  241. _tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1
  242. break
  243. }
  244. }
  245. }
  246. tableCells.value = _tableCells.map(item => {
  247. item.splice(colIndex, 1)
  248. return item
  249. })
  250. colSizeList.value.splice(colIndex, 1)
  251. emit('changeColWidths', colSizeList.value)
  252. }
  253. // 插入一行
  254. const insertRow = (rowIndex: number) => {
  255. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  256. const rowCells: TableCell[] = []
  257. for (let i = 0; i < _tableCells[0].length; i++) {
  258. rowCells.push({
  259. colspan: 1,
  260. rowspan: 1,
  261. text: '',
  262. id: nanoid(10),
  263. })
  264. }
  265. _tableCells.splice(rowIndex, 0, rowCells)
  266. tableCells.value = _tableCells
  267. }
  268. // 插入一列
  269. const insertCol = (colIndex: number) => {
  270. tableCells.value = tableCells.value.map(item => {
  271. const cell = {
  272. colspan: 1,
  273. rowspan: 1,
  274. text: '',
  275. id: nanoid(10),
  276. }
  277. item.splice(colIndex, 0, cell)
  278. return item
  279. })
  280. colSizeList.value.splice(colIndex, 0, 100)
  281. emit('changeColWidths', colSizeList.value)
  282. }
  283. // 填充指定的行/列数
  284. const fillTable = (rowCount: number, colCount: number) => {
  285. let _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  286. const defaultCell = { colspan: 1, rowspan: 1, text: '' }
  287. if (rowCount) {
  288. const newRows = []
  289. for (let i = 0; i < rowCount; i++) {
  290. const rowCells: TableCell[] = []
  291. for (let j = 0; j < _tableCells[0].length; j++) {
  292. rowCells.push({
  293. ...defaultCell,
  294. id: nanoid(10),
  295. })
  296. }
  297. newRows.push(rowCells)
  298. }
  299. _tableCells = [..._tableCells, ...newRows]
  300. }
  301. if (colCount) {
  302. _tableCells = _tableCells.map(item => {
  303. const cells: TableCell[] = []
  304. for (let i = 0; i < colCount; i++) {
  305. const cell = {
  306. ...defaultCell,
  307. id: nanoid(10),
  308. }
  309. cells.push(cell)
  310. }
  311. return [...item, ...cells]
  312. })
  313. colSizeList.value = [...colSizeList.value, ...new Array(colCount).fill(100)]
  314. emit('changeColWidths', colSizeList.value)
  315. }
  316. tableCells.value = _tableCells
  317. }
  318. // 合并单元格
  319. const mergeCells = () => {
  320. const [startX, startY] = startCell.value
  321. const [endX, endY] = endCell.value
  322. const minX = Math.min(startX, endX)
  323. const minY = Math.min(startY, endY)
  324. const maxX = Math.max(startX, endX)
  325. const maxY = Math.max(startY, endY)
  326. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  327. _tableCells[minX][minY].rowspan = maxX - minX + 1
  328. _tableCells[minX][minY].colspan = maxY - minY + 1
  329. tableCells.value = _tableCells
  330. removeSelectedCells()
  331. }
  332. // 拆分单元格
  333. const splitCells = (rowIndex: number, colIndex: number) => {
  334. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  335. _tableCells[rowIndex][colIndex].rowspan = 1
  336. _tableCells[rowIndex][colIndex].colspan = 1
  337. tableCells.value = _tableCells
  338. removeSelectedCells()
  339. }
  340. // 鼠标拖拽调整列宽
  341. const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => {
  342. removeSelectedCells()
  343. let isMouseDown = true
  344. const originWidth = colSizeList.value[colIndex]
  345. const startPageX = e.pageX
  346. const minWidth = 50
  347. document.onmousemove = e => {
  348. if (!isMouseDown) return
  349. const moveX = (e.pageX - startPageX) / canvasScale.value
  350. const width = originWidth + moveX < minWidth ? minWidth : Math.round(originWidth + moveX)
  351. colSizeList.value[colIndex] = width
  352. }
  353. document.onmouseup = () => {
  354. isMouseDown = false
  355. document.onmousemove = null
  356. document.onmouseup = null
  357. emit('changeColWidths', colSizeList.value)
  358. }
  359. }
  360. // 清空选中单元格内的文字
  361. const clearSelectedCellText = () => {
  362. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  363. for (let i = 0; i < _tableCells.length; i++) {
  364. for (let j = 0; j < _tableCells[i].length; j++) {
  365. if (selectedCells.value.includes(`${i}_${j}`)) {
  366. _tableCells[i][j].text = ''
  367. }
  368. }
  369. }
  370. tableCells.value = _tableCells
  371. }
  372. const focusActiveCell = () => {
  373. nextTick(() => {
  374. const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
  375. if (textRef) textRef.focus()
  376. })
  377. }
  378. // 将焦点移动到下一个单元格
  379. // 当前行右边有单元格时,焦点右移
  380. // 当前行右边无单元格(已处在行末),且存在下一行时,焦点移动至下一行行首
  381. // 当前行右边无单元格(已处在行末),且不存在下一行(已处在最后一行)时,新建一行并将焦点移动至下一行行首
  382. const tabActiveCell = () => {
  383. const getNextCell = (i: number, j: number): [number, number] | null => {
  384. if (!tableCells.value[i]) return null
  385. if (!tableCells.value[i][j]) return getNextCell(i + 1, 0)
  386. if (isHideCell(i, j)) return getNextCell(i, j + 1)
  387. return [i, j]
  388. }
  389. endCell.value = []
  390. const nextRow = startCell.value[0]
  391. const nextCol = startCell.value[1] + 1
  392. const nextCell = getNextCell(nextRow, nextCol)
  393. if (!nextCell) {
  394. insertRow(nextRow + 1)
  395. startCell.value = [nextRow + 1, 0]
  396. }
  397. else startCell.value = nextCell
  398. // 移动焦点后自动聚焦文本
  399. focusActiveCell()
  400. }
  401. // 移动焦点(上下左右)
  402. const moveActiveCell = (dir: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT') => {
  403. const rowIndex = +selectedCells.value[0].split('_')[0]
  404. const colIndex = +selectedCells.value[0].split('_')[1]
  405. const rowLen = tableCells.value.length
  406. const colLen = tableCells.value[0].length
  407. const getEffectivePos = (pos: [number, number]): [number, number] => {
  408. if (pos[0] < 0 || pos[1] < 0 || pos[0] > rowLen - 1 || pos[1] > colLen - 1) return [0, 0]
  409. const p = `${pos[0]}_${pos[1]}`
  410. if (!hideCells.value.includes(p)) return pos
  411. if (dir === 'UP') {
  412. return getEffectivePos([pos[0], pos[1] - 1])
  413. }
  414. if (dir === 'DOWN') {
  415. return getEffectivePos([pos[0], pos[1] - 1])
  416. }
  417. if (dir === 'LEFT') {
  418. return getEffectivePos([pos[0] - 1, pos[1]])
  419. }
  420. if (dir === 'RIGHT') {
  421. return getEffectivePos([pos[0] - 1, pos[1]])
  422. }
  423. return [0, 0]
  424. }
  425. if (dir === 'UP') {
  426. const _rowIndex = rowIndex - 1
  427. if (_rowIndex < 0) return
  428. endCell.value = []
  429. startCell.value = getEffectivePos([_rowIndex, colIndex])
  430. }
  431. else if (dir === 'DOWN') {
  432. const _rowIndex = rowIndex + 1
  433. if (_rowIndex > rowLen - 1) return
  434. endCell.value = []
  435. startCell.value = getEffectivePos([_rowIndex, colIndex])
  436. }
  437. else if (dir === 'LEFT') {
  438. const _colIndex = colIndex - 1
  439. if (_colIndex < 0) return
  440. endCell.value = []
  441. startCell.value = getEffectivePos([rowIndex, _colIndex])
  442. }
  443. else if (dir === 'RIGHT') {
  444. const _colIndex = colIndex + 1
  445. if (_colIndex > colLen - 1) return
  446. endCell.value = []
  447. startCell.value = getEffectivePos([rowIndex, _colIndex])
  448. }
  449. focusActiveCell()
  450. }
  451. // 获取光标位置
  452. const getCaretPosition = (element: HTMLDivElement) => {
  453. const selection = window.getSelection()
  454. if (selection && selection.rangeCount > 0) {
  455. const range = selection.getRangeAt(0)
  456. const preCaretRange = range.cloneRange()
  457. preCaretRange.selectNodeContents(element)
  458. preCaretRange.setEnd(range.startContainer, range.startOffset)
  459. const start = preCaretRange.toString().length
  460. preCaretRange.setEnd(range.endContainer, range.endOffset)
  461. const end = preCaretRange.toString().length
  462. const len = element.textContent?.length || 0
  463. return { start, end, len }
  464. }
  465. return null
  466. }
  467. // 表格快捷键监听
  468. const keydownListener = (e: KeyboardEvent) => {
  469. if (!props.editable || !selectedCells.value.length) return
  470. const key = e.key.toUpperCase()
  471. if (selectedCells.value.length < 2) {
  472. if (key === KEYS.TAB) {
  473. e.preventDefault()
  474. tabActiveCell()
  475. }
  476. else if (e.ctrlKey && key === KEYS.UP) {
  477. e.preventDefault()
  478. const rowIndex = +selectedCells.value[0].split('_')[0]
  479. insertRow(rowIndex)
  480. }
  481. else if (e.ctrlKey && key === KEYS.DOWN) {
  482. e.preventDefault()
  483. const rowIndex = +selectedCells.value[0].split('_')[0]
  484. insertRow(rowIndex + 1)
  485. }
  486. else if (e.ctrlKey && key === KEYS.LEFT) {
  487. e.preventDefault()
  488. const colIndex = +selectedCells.value[0].split('_')[1]
  489. insertCol(colIndex)
  490. }
  491. else if (e.ctrlKey && key === KEYS.RIGHT) {
  492. e.preventDefault()
  493. const colIndex = +selectedCells.value[0].split('_')[1]
  494. insertCol(colIndex + 1)
  495. }
  496. else if (key === KEYS.UP) {
  497. const range = getCaretPosition(e.target as HTMLDivElement)
  498. if (range && range.start === range.end && range.start === 0) {
  499. moveActiveCell('UP')
  500. }
  501. }
  502. else if (key === KEYS.DOWN) {
  503. const range = getCaretPosition(e.target as HTMLDivElement)
  504. if (range && range.start === range.end && range.start === range.len) {
  505. moveActiveCell('DOWN')
  506. }
  507. }
  508. else if (key === KEYS.LEFT) {
  509. const range = getCaretPosition(e.target as HTMLDivElement)
  510. if (range && range.start === range.end && range.start === 0) {
  511. moveActiveCell('LEFT')
  512. }
  513. }
  514. else if (key === KEYS.RIGHT) {
  515. const range = getCaretPosition(e.target as HTMLDivElement)
  516. if (range && range.start === range.end && range.start === range.len) {
  517. moveActiveCell('RIGHT')
  518. }
  519. }
  520. }
  521. else if (key === KEYS.DELETE) {
  522. clearSelectedCellText()
  523. }
  524. }
  525. onMounted(() => {
  526. document.addEventListener('keydown', keydownListener)
  527. })
  528. onUnmounted(() => {
  529. document.removeEventListener('keydown', keydownListener)
  530. })
  531. // 单元格文字输入时更新表格数据
  532. const handleInput = debounce(function(value, rowIndex, colIndex) {
  533. tableCells.value[rowIndex][colIndex].text = value
  534. emit('change', tableCells.value)
  535. }, 300, { trailing: true })
  536. // 插入来自Excel的数据,表格的行/列数不够时自动补足
  537. const insertExcelData = (data: string[][], rowIndex: number, colIndex: number) => {
  538. const maxRow = data.length
  539. const maxCol = data[0].length
  540. let fillRowCount = 0
  541. let fillColCount = 0
  542. if (rowIndex + maxRow > tableCells.value.length) fillRowCount = rowIndex + maxRow - tableCells.value.length
  543. if (colIndex + maxCol > tableCells.value[0].length) fillColCount = colIndex + maxCol - tableCells.value[0].length
  544. if (fillRowCount || fillColCount) fillTable(fillRowCount, fillColCount)
  545. nextTick(() => {
  546. for (let i = 0; i < maxRow; i++) {
  547. for (let j = 0; j < maxCol; j++) {
  548. if (tableCells.value[rowIndex + i][colIndex + j]) {
  549. tableCells.value[rowIndex + i][colIndex + j].text = data[i][j]
  550. }
  551. }
  552. }
  553. emit('change', tableCells.value)
  554. })
  555. }
  556. // 获取有效的单元格(排除掉被合并的单元格)
  557. const getEffectiveTableCells = () => {
  558. const effectiveTableCells = []
  559. for (let i = 0; i < tableCells.value.length; i++) {
  560. const rowCells = tableCells.value[i]
  561. const _rowCells = []
  562. for (let j = 0; j < rowCells.length; j++) {
  563. if (!isHideCell(i, j)) _rowCells.push(rowCells[j])
  564. }
  565. if (_rowCells.length) effectiveTableCells.push(_rowCells)
  566. }
  567. return effectiveTableCells
  568. }
  569. // 检查是否可以删除行和列:有效的行/列数大于1
  570. const checkCanDeleteRowOrCol = () => {
  571. const effectiveTableCells = getEffectiveTableCells()
  572. const canDeleteRow = effectiveTableCells.length > 1
  573. const canDeleteCol = effectiveTableCells[0].length > 1
  574. return { canDeleteRow, canDeleteCol }
  575. }
  576. // 检查是否可以合并或拆分
  577. // 必须多选才可以合并
  578. // 必须单选且所选单元格为合并单元格才可以拆分
  579. const checkCanMergeOrSplit = (rowIndex: number, colIndex: number) => {
  580. const isMultiSelected = selectedCells.value.length > 1
  581. const targetCell = tableCells.value[rowIndex][colIndex]
  582. const canMerge = isMultiSelected
  583. const canSplit = !isMultiSelected && (targetCell.rowspan > 1 || targetCell.colspan > 1)
  584. return { canMerge, canSplit }
  585. }
  586. const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
  587. const cellIndex = el.dataset.cellIndex as string
  588. const rowIndex = +cellIndex.split('_')[0]
  589. const colIndex = +cellIndex.split('_')[1]
  590. if (!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {
  591. startCell.value = [rowIndex, colIndex]
  592. endCell.value = []
  593. }
  594. const { canMerge, canSplit } = checkCanMergeOrSplit(rowIndex, colIndex)
  595. const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()
  596. return [
  597. {
  598. text: lang.ssInsertCol,
  599. children: [
  600. { text: lang.ssToLeft, handler: () => insertCol(colIndex) },
  601. { text: lang.ssToRight, handler: () => insertCol(colIndex + 1) },
  602. ],
  603. },
  604. {
  605. text: lang.ssInsertRow,
  606. children: [
  607. { text: lang.ssAbove, handler: () => insertRow(rowIndex) },
  608. { text: lang.ssBelow, handler: () => insertRow(rowIndex + 1) },
  609. ],
  610. },
  611. {
  612. text: lang.ssDeleteCol,
  613. disable: !canDeleteCol,
  614. handler: () => deleteCol(colIndex),
  615. },
  616. {
  617. text: lang.ssDeleteRow,
  618. disable: !canDeleteRow,
  619. handler: () => deleteRow(rowIndex),
  620. },
  621. { divider: true },
  622. {
  623. text: lang.ssMergeCells,
  624. disable: !canMerge,
  625. handler: mergeCells,
  626. },
  627. {
  628. text: lang.ssUnmergeCells,
  629. disable: !canSplit,
  630. handler: () => splitCells(rowIndex, colIndex),
  631. },
  632. { divider: true },
  633. {
  634. text: lang.ssSelectCurCol,
  635. handler: () => selectCol(colIndex),
  636. },
  637. {
  638. text: lang.ssSelectCurRow,
  639. handler: () => selectRow(rowIndex),
  640. },
  641. {
  642. text: lang.ssSelectAllCells,
  643. handler: selectAll,
  644. },
  645. ]
  646. }
  647. </script>
  648. <style lang="scss" scoped>
  649. .editable-table {
  650. position: relative;
  651. user-select: none;
  652. }
  653. table {
  654. width: 100%;
  655. position: relative;
  656. table-layout: fixed;
  657. border-collapse: collapse;
  658. border-spacing: 0;
  659. border: 0;
  660. word-wrap: break-word;
  661. user-select: none;
  662. --themeColor: $themeColor;
  663. --subThemeColor1: $themeColor;
  664. --subThemeColor2: $themeColor;
  665. &.theme {
  666. background-color: #fff;
  667. tr:nth-child(2n) .cell {
  668. background-color: var(--subThemeColor1);
  669. }
  670. tr:nth-child(2n + 1) .cell {
  671. background-color: var(--subThemeColor2);
  672. }
  673. &.row-header {
  674. tr:first-child .cell {
  675. background-color: var(--themeColor);
  676. }
  677. }
  678. &.row-footer {
  679. tr:last-child .cell {
  680. background-color: var(--themeColor);
  681. }
  682. }
  683. &.col-header {
  684. tr .cell:first-child {
  685. background-color: var(--themeColor);
  686. }
  687. }
  688. &.col-footer {
  689. tr .cell:last-child {
  690. background-color: var(--themeColor);
  691. }
  692. }
  693. }
  694. .cell {
  695. position: relative;
  696. white-space: normal;
  697. word-wrap: break-word;
  698. vertical-align: middle;
  699. font-size: 14px;
  700. background-clip: padding-box;
  701. cursor: default;
  702. &.selected::after {
  703. content: '';
  704. width: 100%;
  705. height: 100%;
  706. position: absolute;
  707. top: 0;
  708. left: 0;
  709. background-color: rgba($color: #666, $alpha: .4);
  710. }
  711. }
  712. .cell-text {
  713. padding: 5px;
  714. line-height: 1.5;
  715. user-select: none;
  716. cursor: text;
  717. &.active {
  718. user-select: text;
  719. }
  720. }
  721. }
  722. .drag-line {
  723. position: absolute;
  724. top: 0;
  725. bottom: 0;
  726. width: 3px;
  727. background-color: $themeColor;
  728. margin-left: -1px;
  729. opacity: 0;
  730. z-index: 2;
  731. cursor: col-resize;
  732. }
  733. </style>