【PPTist】表格功能

devtools/2024/12/29 13:01:46/

前言:这篇文章来探讨一下表格功能是怎么实现的吧!

一、插入表格

我们可以看到,鼠标移动到菜单项上出现的提示语是“插入表格”
在这里插入图片描述
那么就全局搜索一下,就发现这个菜单在 src/views/Editor/CanvasTool/index.vue 文件中

<Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10"><template #content><TableGenerator@close="tableGeneratorVisible = false"@insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"/></template><IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
</Popover>

看一下组件 TableGenerator,是用来选择表格的长宽的组件。

src/views/Editor/CanvasTool/TableGenerator.vue

<table @mouseleave="endCell = []" @click="handleClickTable()" v-if="!isCustom"
><tbody><tr v-for="row in 10" :key="row"><td @mouseenter="endCell = [row, col]"v-for="col in 10" :key="col"><div class="cell" :class="{ 'active': endCell.length && row <= endCell[0] && col <= endCell[1] }"></div></td></tr></tbody>
</table>

可以看到主要是通过监听鼠标移入事件和鼠标离开时间。鼠标移入的时候,将鼠标移入的当前的 td 的位置赋值给 endCell,并且高亮在endCell 范围内的 td
点击的时候,创建表格元素并且插入。创建元素的方法在下面的文件中统一管理
关于表格的位置的处理还比较简单,统一放在水平垂直居中的位置。
src/hooks/useCreateElement.ts

/*** 创建表格元素* @param row 行数* @param col 列数*/
const createTableElement = (row: number, col: number) => {const style: TableCellStyle = {fontname: theme.value.fontName,color: theme.value.fontColor,}// 创建表格数据 空的二维数组const data: TableCell[][] = []for (let i = 0; i < row; i++) {const rowCells: TableCell[] = []for (let j = 0; j < col; j++) {rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '', style })}data.push(rowCells)}const DEFAULT_CELL_WIDTH = 100const DEFAULT_CELL_HEIGHT = 36// 创建列宽数组 每个元素的值为1/colconst colWidths: number[] = new Array(col).fill(1 / col)const width = col * DEFAULT_CELL_WIDTHconst height = row * DEFAULT_CELL_HEIGHT// 创建表格元素createElement({type: 'table',id: nanoid(10),width,height,colWidths,rotate: 0,data,left: (VIEWPORT_SIZE - width) / 2,top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,outline: {width: 2,style: 'solid',color: '#eeece1',},theme: {color: theme.value.themeColor,rowHeader: true,rowFooter: false,colHeader: false,colFooter: false,},cellMinHeight: 36,})
}

以及来看一下公用的 createElement 方法都做了什么

// 创建(插入)一个元素并将其设置为被选中元素
const createElement = (element: PPTElement, callback?: () => void) => {// 添加元素到元素列表slidesStore.addElement(element)// 设置被选中元素列表mainStore.setActiveElementIdList([element.id])if (creatingElement.value) mainStore.setCreatingElement(null)setTimeout(() => {// 设置编辑器区域为聚焦状态mainStore.setEditorareaFocus(true)}, 0)if (callback) callback()// 添加历史快照addHistorySnapshot()
}

以及添加元素的方法 slidesStore.addElement
src/store/slides.ts

addElement(element: PPTElement | PPTElement[]) {const elements = Array.isArray(element) ? element : [element]const currentSlideEls = this.slides[this.slideIndex].elementsconst newEls = [...currentSlideEls, ...elements]this.slides[this.slideIndex].elements = newEls
},

新添加的元素就放在当前的幻灯片的元素列表的最后就行,也不用考虑按顺序摆放,因为元素里面都有各自的位置信息

mainStore.setCreatingElement() 这个方法就是设置一个公用的对象 creatingElement,设置为 null 表示创建结束啦
src/store/main.ts

setCreatingElement(element: CreatingElement | null) {this.creatingElement = element
},

mainStore.setEditorareaFocus(true) 聚焦于编辑区域,这个方法也简单
src/store/main.ts

setEditorareaFocus(isFocus: boolean) {this.editorAreaFocus = isFocus
},

还有两个方法是以前见过的

mainStore.setActiveElementIdList() 方法见 【PPTist】网格线、对齐线、标尺
addHistorySnapshot() 方法见 【PPTist】历史记录功能
总结来说, createElement 里面都干了这些事情

  • 添加元素到当前幻灯片的元素列表
  • 将这个新的元素设置为被选中的状态
  • creatingElement 置空
  • 将焦点放在编辑区域
  • 执行回调函数(如果有的话)
  • 将创建元素的行为添加到历史快照中

ok,这是表格的创建阶段完成了。

二、表格编辑

接下来要看一下表格右键的一些方法
进入表格的编辑状态,右键出来的菜单长这样
在这里插入图片描述
这个创建出来的表格的组件是 src/views/components/element/TableElement/EditableTable.vue
表格的数据是 tableCells 二维数组。这个文件里的代码有点复杂了。一个一个来吧。

1、右键菜单

菜单由指令 v-contextmenu 添加,这是一个自定义指令,定义在 src/plugins/directive/contextmenu.ts

① 自定义指令

自定义指令定义了两个生命周期函数,一个是 mounted,一个是 unmounted。自定义指令被挂载的时候,会接受一个参数。mounted 的第一个参数是默认参数,表示使用自定义指令的元素,第二个参数是通过自定义指定传递过来的参数。
然后绑定了右键菜单事件 contextmenu,并且将事件记录了一个索引值,便于元素卸载的时候解绑右键菜单时间

// 定义自定义指令
const ContextmenuDirective: Directive = {// 在元素挂载时mounted(el: CustomHTMLElement, binding) {// 保存事件处理器引用,方便后续解绑el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)// 绑定右键菜单事件el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])},// 在元素卸载时unmounted(el: CustomHTMLElement) {// 清理事件监听,避免内存泄漏if (el && el[CTX_CONTEXTMENU_HANDLER]) {el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])delete el[CTX_CONTEXTMENU_HANDLER]}},
}
② 创建右键菜单
// 核心的右键菜单处理函数
const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {// 阻止默认右键菜单和事件冒泡event.stopPropagation()event.preventDefault()// 调用指令绑定的值函数,获取菜单配置const menus = binding.value(el)if (!menus) returnlet container: HTMLDivElement | null = null// 清理函数:移除右键菜单并清理相关事件监听const removeContextmenu = () => {if (container) {document.body.removeChild(container)container = null}// 移除目标元素的激活状态样式el.classList.remove('contextmenu-active')// 清理全局事件监听document.body.removeEventListener('scroll', removeContextmenu)window.removeEventListener('resize', removeContextmenu)}// 准备创建菜单所需的配置项const options = {axis: { x: event.x, y: event.y },  // 鼠标点击位置el,                                // 目标元素menus,                            // 菜单配置removeContextmenu,                // 清理函数}// 创建容器并渲染菜单组件container = document.createElement('div')const vm = createVNode(ContextmenuComponent, options, null)render(vm, container)document.body.appendChild(container)// 为目标元素添加激活状态样式el.classList.add('contextmenu-active')// 监听可能导致菜单需要关闭的全局事件document.body.addEventListener('scroll', removeContextmenu)window.addEventListener('resize', removeContextmenu)
}

其中的 removeContextmenu 是一个闭包,在闭包内销毁指令创建出来的元素,并且清除自身的监听回调。
菜单配置是通过自定义指令传递过来的方法获取的。
例如表格 v-contextmenu="(el: HTMLElement) => contextmenus(el)",返回的是菜单项的数组。

const contextmenus = (el: HTMLElement): ContextmenuItem[] => {// 获取单元格索引const cellIndex = el.dataset.cellIndex as stringconst rowIndex = +cellIndex.split('_')[0]const colIndex = +cellIndex.split('_')[1]// 如果当前单元格未被选中,则将当前单元格设置为选中状态if (!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {startCell.value = [rowIndex, colIndex]endCell.value = []}const { canMerge, canSplit } = checkCanMergeOrSplit(rowIndex, colIndex)const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()return [{text: '插入列',children: [{ text: '到左侧', handler: () => insertCol(colIndex) },{ text: '到右侧', handler: () => insertCol(colIndex + 1) },],},{text: '插入行',children: [{ text: '到上方', handler: () => insertRow(rowIndex) },{ text: '到下方', handler: () => insertRow(rowIndex + 1) },],},{text: '删除列',disable: !canDeleteCol,handler: () => deleteCol(colIndex),},{text: '删除行',disable: !canDeleteRow,handler: () => deleteRow(rowIndex),},{ divider: true },{text: '合并单元格',disable: !canMerge,handler: mergeCells,},{text: '取消合并单元格',disable: !canSplit,handler: () => splitCells(rowIndex, colIndex),},{ divider: true },{text: '选中当前列',handler: () => selectCol(colIndex),},{text: '选中当前行',handler: () => selectRow(rowIndex),},{text: '选中全部单元格',handler: selectAll,},]
}

创建组件使用的是 createVNode 方法,ContextmenuComponentsrc/components/Contextmenu/index.vue 组件
createVNode 方法参数列表:

  1. type
    类型: string | object
    描述: VNode 的类型,可以是一个 HTML 标签名(如 ‘div’、‘span’ 等),也可以是一个组件的定义(如一个 Vue 组件的对象或异步组件的工厂函数)。
  2. props
    类型: object | null
    描述: 传递给组件或元素的属性。对于组件,这些属性会被作为 props 传递;对于 DOM 元素,这些属性会被直接应用到元素上。
  3. children
    类型: string | VNode | Array<VNode | string> | null
    描述: VNode 的子节点,可以是一个字符串(文本节点)、一个 VNode、一个 VNode 数组,或者是 null。如果提供了多个子节点,可以用数组的形式传递。

通过createVNode 方法,会将鼠标点击的位置、目标元素、菜单配置以及清理函数传递给自定义指令的组件。

并且给全局增加了滚动事件的监听和调整大小事件的监听,当滚动鼠标或者调整页面大小的时候,就隐藏右键菜单。

③ 右键菜单组件

右键菜单组件是 src/components/Contextmenu/index.vue,其中的菜单项是 src/components/Contextmenu/MenuContent.vue
菜单里面的具体的菜单项上面已经讲过是咋来的,使用自定义指令的时候,通过方法返回一个对象数组。点击菜单项的时候,执行回调函数

const handleClickMenuItem = (item: ContextmenuItem) => {if (item.disable) returnif (item.children && !item.handler) returnif (item.handler) item.handler(props.el)props.removeContextmenu()
}

2、插入列

// 插入一列
const insertCol = (colIndex: number) => {tableCells.value = tableCells.value.map(item => {// 每一行都要在 colIndex 的地方添加一个元素const cell = {colspan: 1,rowspan: 1,text: '',id: nanoid(10),}item.splice(colIndex, 0, cell)return item })colSizeList.value.splice(colIndex, 0, 100)emit('changeColWidths', colSizeList.value)
}

在模版中,表格项遍历的时候,会给每一个 td 元素添加一个属性 :data-cell-index="KaTeX parse error: Expected group after '_' at position 11: {rowIndex}_̲{colIndex}"
插入列的时候,如果是向左插入,colIndex 直接取元素上绑定的值,如果是向右插入,需要取 colIndex + 1
输出一下 colSizeList.value,它记录的是所有列的宽度,所以这里插入的是 100,即默认插入列的宽度是 100px
在这里插入图片描述

3、插入行

行的数据就复杂那么一丢丢

// 插入一行
const insertRow = (rowIndex: number) => {const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))const rowCells: TableCell[] = []for (let i = 0; i < _tableCells[0].length; i++) {rowCells.push({colspan: 1,rowspan: 1,text: '',id: nanoid(10),})}_tableCells.splice(rowIndex, 0, rowCells)tableCells.value = _tableCells
}

插入的时候需要创建一个数组

我们看一下里面的几个数据分别长什么样子
如图下面这个表格,我在第一行的下面增加一行的时候
在这里插入图片描述
新的一行的数据如下:
在这里插入图片描述

_tableCells 的数据如下:
是一个二维数组
在这里插入图片描述

在模版中,表格是遍历二维数组 tableCells 创建的。至于单元格的宽度,是通过 colgroup标签,循环 colSizeList 制定的。

<colgroup><col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
</colgroup>

这个标签主要用来指定列的宽度。span 属性我看官网说已经禁用了。

删除行或列类似,主要通过 splice 方法进行数组元素的剪切

4、合并单元格

这是比较复杂的功能了。它会修改最小的坐标处的单元格的 colSpanrowspan ,表示当前这个单元格占多少单位行或者单位列。但是后面的单元格,不会删除,会隐藏掉。也就是说,二维数组的结构不变,只是其中的合并单元格的开头单元格的 rowSpancolSpan 变了
下面这个单元格,就是被合并的效果,第一个单元格撑宽,第二个单元格 display: none
在这里插入图片描述
看一下被合并的单元格的数据,只有第一个格格的数据会被修改
在这里插入图片描述

// 合并单元格
const mergeCells = () => {const [startX, startY] = startCell.valueconst [endX, endY] = endCell.valueconst minX = Math.min(startX, endX)const minY = Math.min(startY, endY)const maxX = Math.max(startX, endX)const maxY = Math.max(startY, endY)const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))// 更新坐标最小的单元格的rowspan和colspan_tableCells[minX][minY].rowspan = maxX - minX + 1_tableCells[minX][minY].colspan = maxY - minY + 1tableCells.value = _tableCellsremoveSelectedCells()
}

起始的单元格 startCell 在鼠标落下的时候会更新它的值

const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {if (e.button === 0) {endCell.value = []isStartSelect.value = truestartCell.value = [rowIndex, colIndex]}
}

结束的单元格在鼠标移入单元格的时候就会更新

const handleCellMouseenter = (rowIndex: number, colIndex: number) => {if (!isStartSelect.value) returnendCell.value = [rowIndex, colIndex]
}

隐藏后面的单元格是怎么实现的呢?是通过 td 标签上的 v-show="!hideCells.includes(KaTeX parse error: Expected group after '_' at position 11: {rowIndex}_̲{colIndex})" 这个判断实现的。
hideCells 的计算在 src/views/components/element/TableElement/useHideCells.ts 文件中,它是个计算属性,但是竟然也分成一个文件写,所以这代码管理的层级很好哦

// 这是一个组合式函数 (Composable),用于处理表格合并时的单元格隐藏逻辑
export default (cells: Ref<TableCell[][]>) => {// computed 会创建一个响应式的计算属性const hideCells = computed(() => {const hideCells: string[] = []// 双重循环遍历表格的每一个单元格for (let i = 0; i < cells.value.length; i++) {      // 遍历行const rowCells = cells.value[i]for (let j = 0; j < rowCells.length; j++) {       // 遍历列const cell = rowCells[j]// 如果当前单元格设置了合并if (cell.colspan > 1 || cell.rowspan > 1) {// 遍历被合并的区域for (let row = i; row < i + cell.rowspan; row++) {for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) {// 将被合并的单元格位置添加到数组// 例如:如果是第2行第3列的单元格,会生成 "2_3"hideCells.push(`${row}_${col}`)}}}}}return hideCells})return {hideCells, // 返回需要隐藏的单元格位置数组}
}

5、拆分单元格

这个方法就挺简单的了,之前我们合并单元格的时候,是把坐标最小的单元格的 rowSpancolSpan 修改成合并单元格选中的横向格数和纵向格数。那么拆分单元格,直接把单元格的 rowSpancolSpan 都变回 1 就可以了。

// 拆分单元格
const splitCells = (rowIndex: number, colIndex: number) => {const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))_tableCells[rowIndex][colIndex].rowspan = 1_tableCells[rowIndex][colIndex].colspan = 1tableCells.valuesremoveSelectedCells()
}

修改表格数据的方法,基本上都使用的是 tableCells.value 重新给表格数据赋值的方法。这也就确保上面的计算属性 hideCells 能触发更新。

6、选中当前列/行、选中全部单元格

选中这个操作,处理起来很简单,只是修改两个表示范围的响应式数据 startCellendCell

// 选中指定的列
const selectCol = (index: number) => {const maxRow = tableCells.value.length - 1startCell.value = [0, index]endCell.value = [maxRow, index]
}

另外两个也类似,就不粘贴了。
然后选中的单元格会有高亮效果,在模版中 td 标签上

:class="{'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,'active': activedCell === `${rowIndex}_${colIndex}`,}"

选中的单元格是计算属性 selectedCells

// 当前选中的单元格集合
const selectedCells = computed(() => {if (!startCell.value.length) return []const [startX, startY] = startCell.valueif (!endCell.value.length) return [`${startX}_${startY}`]const [endX, endY] = endCell.valueif (startX === endX && startY === endY) return [`${startX}_${startY}`]const selectedCells = []const minX = Math.min(startX, endX)const minY = Math.min(startY, endY)const maxX = Math.max(startX, endX)const maxY = Math.max(startY, endY)for (let i = 0; i < tableCells.value.length; i++) {const rowCells = tableCells.value[i]for (let j = 0; j < rowCells.length; j++) {if (i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)}}return selectedCells
})

然后捏,选中的单元格修改的时候,还需要触发一个自定义函数

watch(selectedCells, (value, oldValue) => {if (isEqual(value, oldValue)) returnemit('changeSelectedCells', selectedCells.value)
})

在父组件中监听这个函数,更新全局的 selectedTableCells 属性

// 更新表格当前选中的单元格
const updateSelectedCells = (cells: string[]) => {nextTick(() => mainStore.setSelectedTableCells(cells))
}

7、删除列/行

删除列/行的代码差不多,都是使用 splice 方法,将删除的单元格截取掉。

// 删除一行
const deleteRow = (rowIndex: number) => {const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))const targetCells = tableCells.value[rowIndex]const hideCellsPos = []for (let i = 0; i < targetCells.length; i++) {if (isHideCell(rowIndex, i)) hideCellsPos.push(i)}for (const pos of hideCellsPos) {for (let i = rowIndex; i >= 0; i--) {if (!isHideCell(i, pos)) {_tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1break}}}_tableCells.splice(rowIndex, 1)tableCells.value = _tableCells
}
// 删除一列
const deleteCol = (colIndex: number) => {const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))const hideCellsPos = []for (let i = 0; i < tableCells.value.length; i++) {if (isHideCell(i, colIndex)) hideCellsPos.push(i)}for (const pos of hideCellsPos) {for (let i = colIndex; i >= 0; i--) {if (!isHideCell(pos, i)) {_tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1break}}}tableCells.value = _tableCells.map(item => {item.splice(colIndex, 1)return item})colSizeList.value.splice(colIndex, 1)emit('changeColWidths', colSizeList.value)
}

8、快捷键

快捷键是上下左右箭头,以及 ctrl + 上下左右箭头。代码看起来还是比较好理解的

// 表格快捷键监听
const keydownListener = (e: KeyboardEvent) => {if (!props.editable || !selectedCells.value.length) returnconst key = e.key.toUpperCase()if (selectedCells.value.length < 2) {if (key === KEYS.TAB) {e.preventDefault()tabActiveCell()}else if (e.ctrlKey && key === KEYS.UP) {e.preventDefault()const rowIndex = +selectedCells.value[0].split('_')[0]insertRow(rowIndex)}else if (e.ctrlKey && key === KEYS.DOWN) {e.preventDefault()const rowIndex = +selectedCells.value[0].split('_')[0]insertRow(rowIndex + 1)}else if (e.ctrlKey && key === KEYS.LEFT) {e.preventDefault()const colIndex = +selectedCells.value[0].split('_')[1]insertCol(colIndex)}else if (e.ctrlKey && key === KEYS.RIGHT) {e.preventDefault()const colIndex = +selectedCells.value[0].split('_')[1]insertCol(colIndex + 1)}else if (key === KEYS.UP) {const range = getCaretPosition(e.target as HTMLDivElement)if (range && range.start === range.end && range.start === 0) {moveActiveCell('UP')}}else if (key === KEYS.DOWN) {const range = getCaretPosition(e.target as HTMLDivElement)if (range && range.start === range.end && range.start === range.len) {moveActiveCell('DOWN')}}else if (key === KEYS.LEFT) {const range = getCaretPosition(e.target as HTMLDivElement)if (range && range.start === range.end && range.start === 0) {moveActiveCell('LEFT')}}else if (key === KEYS.RIGHT) {const range = getCaretPosition(e.target as HTMLDivElement)if (range && range.start === range.end && range.start === range.len) {moveActiveCell('RIGHT')}}}else if (key === KEYS.DELETE) {clearSelectedCellText()}
}

关于 moveActiveCell() 方法,里面的主要做的事情,就是调整 startCell ,起始单元格的位置。

9、快捷键bug

然后这里发现了一个小bug。我使用的是搜狗输入法。如果我正在输入中文,然后点击了上下左右箭头,想选择输入法中的目标文字,焦点就会直接跳转到目标单元格,编辑器的快捷键覆盖了输入法的快捷键。所以应该判断一下,如果当前正在编辑,就不进行单元格的跳转了。
使用 KeyboardEvent.isComposing 事件的 isComposing 属性判断是否在进行输入法输入即可。

const keydownListener = (e: KeyboardEvent) => {if (!props.editable || !selectedCells.value.length) return// 添加输入法检查if (e.isComposing) returnconst key = e.key.toUpperCase()// ... 
}

表格功能确实是很复杂啊,细节太多了。


http://www.ppmy.cn/devtools/146026.html

相关文章

掌握软件工程基础:知识点全面解析【chap02】

chap02 软件项目管理 1.代码行度量与功能点度量的比较 1.规模度量 是一种直接度量方法。 代码行数 LOC或KLOC 生产率 P1L/E 其中 L 软件项目代码行数 E 软件项目工作量&#xff08;人月 PM&#xff09; P1 软件项目生产率&#xff08;LOC/PM&#xff09; 代码出错…

如何使用MySQL WorkBench操作MySQL数据库

1. 说明 最原始的对MySQL的数据库、表等信息的操作是在命令提示符中进行&#xff0c;但是这样的操作方式不是十分的方便&#xff0c;有些操作进行起来会比较麻烦&#xff0c;所以MySQL官方推出了一个对于MySQL数据库进行图形化管理的工具&#xff0c;那就是MySQL WorkBench&am…

期权懂|期权合约是如何划分月份的?如何换月移仓?

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 期权合约是如何划分月份的&#xff1f;如何换月移仓&#xff1f; 合约月份&#xff1a;一般是指期权合约指定交易的月份&#xff0c;也可以理解成期权合约到期的月份&#xff0c…

【商城源码的开发环境】

商城源码的开发环境要求主要包括技术选型、硬件配置、软件配置以及安全性和性能优化。以下是一些商城源码开发环境的要求&#xff1a; 技术选型 编程语言&#xff1a;选择合适的编程语言&#xff0c;如PHP、Java、Python等&#xff0c;这取决于项目需求和团队的技术栈。 数据…

NVIDIA GPU 内部架构介绍

NVIDIA GPU 架构 NVIDIA GPU 的 SM&#xff08;Streaming Multiprocessor&#xff09; 和 GPC&#xff08;Graphics Processing Cluster&#xff09; 是 GPU 架构中的关键组成部分。它们决定了 GPU 的计算能力和性能&#xff0c;以下是对这两个参数的详细介绍&#xff1a; 1. …

基于 Python Django 的二手电子设备交易平台(附源码,文档)

大家好&#xff0c;我是stormjun&#xff0c;今天为大家带来的是Python实战项目-基于 Python Django 的二手电子设备交易平台&#xff08;附源码&#xff0c;文档&#xff09;。该系统采用 Java 语言 开发&#xff0c;MySql 作为数据库&#xff0c;系统功能完善 &#xff0c;实…

[SAP ABAP] 程序备份

备份当前程序到本地的方式如下&#xff1a; 1.复制粘贴 Ctrl A 、Ctrl V 2.【实用程序】|【更多实用程序】|【上载/下载】|【下载】 ​ 3.快捷键&#xff0c;支持多种格式导出(.abap .html .pdf 等) 在事务码SE38(ABAP编辑器)屏幕右下角&#xff0c;点击【Options选项】图…

springcloud篇2-feign、gateway

一、Feign(http客户端) 1.1 简介 之前不同的服务之间进行远程调用使用的是RestTemplate。 存在下面的问题&#xff1a; &#xff08;1&#xff09;代码可读性差&#xff0c;编程体验不统一&#xff1b; &#xff08;2&#xff09;参数复杂&#xff0c;URL难以维护。 Feign(…