使用cursor和AI程序员沟通实现组件n-data-table的键盘上下左右定位单元格
动态的多个表格之间无阻碍键盘定位单元格(不得不说,刚接触时,它确实比我牛逼,但它也常常犯错哈哈哈)
// 1. 首先声明所有需要的 ref
const tableRefs = ref([])
const activeTableIndex = ref(0)
const currentCells = ref(new Map())
const initialized = ref(false)
// 添加当前选中行的数据引用
const currentRowData = ref(null)
// 更新当前行数据的函数
const updateCurrentRowData = (tableIndex, rowIndex) => {
// console.log(tableIndex, rowIndex, data.value)
if (tableIndex >= 0 && data.value[tableIndex] && data.value[tableIndex].list) {
currentRowData.value = data.value[tableIndex].list[rowIndex]
// if (!selectRow.value.includes(currentRowData.value.id)) {
// selectRow.value.push(currentRowData.value.id)
// }
// emit('selectRowFun', selectRow.value)
console.log('当前行数据:', currentRowData.value.id, selectRow.value)
}
}
// 2. 表格注册相关函数
const registerTable = (el) => {
if (el && !tableRefs.value.includes(el)) {
tableRefs.value.push(el)
}
}
const unregisterTable = (el) => {
const index = tableRefs.value.indexOf(el)
if (index > -1) {
tableRefs.value.splice(index, 1)
}
}
// 3. 初始化函数
const initializeFirstCell = () => {
if (initialized.value) return
nextTick(() => {
if (tableRefs.value.length > 0) {
const firstTable = tableRefs.value[0]
const firstCell = firstTable.$el.querySelector('.n-data-table-tbody .n-data-table-tr .n-data-table-td')
if (firstCell) {
activeTableIndex.value = 0
currentCells.value.set('table0', {
rowIndex: 0,
columnIndex: 0
})
// 初始化时设置第一行数据
updateCurrentRowData(0, 0)
firstCell.focus()
initialized.value = true
}
}
})
}
// 4. 处理键盘事件
const handleKeyDown = (e, rowIndex, columnIndex, tableId) => {
// 确保已初始化
if (!initialized.value) {
initializeFirstCell()
return
}
e.preventDefault()
const table = e.target.closest('.n-data-table')
if (!table) return
// 获取当前表格的索引
const currentTableIndex = tableRefs.value.findIndex(t => t.$el === table)
if (currentTableIndex === -1) return
const rows = Array.from(table.querySelectorAll('.n-data-table-tbody .n-data-table-tr'))
const maxRow = rows.length - 1
const maxColumn = columns.length - 1
let newRowIndex = rowIndex
let newColumnIndex = columnIndex
let newTableIndex = currentTableIndex
switch (e.key) {
case 'ArrowUp':
if (rowIndex === 0 && currentTableIndex > 0) {
// 跳转到上一个表格的最后一行
newTableIndex = currentTableIndex - 1
const prevTable = tableRefs.value[newTableIndex]
const prevRows = Array.from(prevTable.$el.querySelectorAll('.n-data-table-tbody .n-data-table-tr'))
newRowIndex = prevRows.length - 1
} else {
newRowIndex = Math.max(0, rowIndex - 1)
}
break
case 'ArrowDown':
if (rowIndex === maxRow && currentTableIndex < tableRefs.value.length - 1) {
newTableIndex = currentTableIndex + 1
newRowIndex = 0
} else {
newRowIndex = Math.min(maxRow, rowIndex + 1)
}
break
case 'ArrowLeft':
// 修改:修复左移逻辑
newColumnIndex = Math.max(0, columnIndex - 1)
break
case 'ArrowRight':
// 修改:修复右移逻辑,移除错误的 -1
newColumnIndex = Math.min(maxColumn, columnIndex + 1)
break
case 'Tab':
e.preventDefault()
newTableIndex = (currentTableIndex + (e.shiftKey ? -1 : 1) + tableRefs.value.length) % tableRefs.value.length
newRowIndex = 0
newColumnIndex = 0
break
default:
return
}
// 更新激活的表格索引
activeTableIndex.value = newTableIndex
// 在设置新位置后更新当前行数据
activeTableIndex.value = newTableIndex
currentCells.value.set(`table${newTableIndex}`, {
rowIndex: newRowIndex,
columnIndex: newColumnIndex
})
// 更新当前行数据
updateCurrentRowData(newTableIndex, newRowIndex)
// 使用 nextTick 确保 DOM 更新后再聚焦
nextTick(() => {
const targetTable = tableRefs.value[newTableIndex]
if (targetTable) {
const rows = Array.from(targetTable.$el.querySelectorAll('.n-data-table-tbody .n-data-table-tr'))
const targetCell = rows[newRowIndex]?.querySelectorAll('.n-data-table-td')[newColumnIndex]
if (targetCell) {
targetCell.focus()
targetCell.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}
}
})
}
// 5. 点击事件处理
const handleCellClick = (e, rowIndex, columnIndex, tableIndex) => {
// console.log(e, rowIndex, columnIndex, tableIndex, 11111111)
if (!initialized.value) {
initializeFirstCell()
}
activeTableIndex.value = tableIndex
currentCells.value.set(`table${tableIndex}`, {
rowIndex,
columnIndex
})
// 更新当前行数据
updateCurrentRowData(tableIndex, rowIndex)
// nextTick(() => {
// const cell = e.target.closest('.n-data-table-td')
// if (cell) {
// cell.focus()
// }
// })
}
// 6. 创建列配置
const createColumns = (tableIndex) => {
return [
{ type: 'selection', fixed: 'left', width: 40 },
{
// title: '配件编码',
title: () => {
return (
<div>{data.value[tableIndex]?.oe}</div>
)
},
key: 'OE',
align: 'left',
width: 120,
ellipsis: { tooltip: true },
},
{
// title: '配件名称',
title: () => {
return (
<n-space align="center">
<div>配件名称</div>
{data.value[tableIndex]?.img_url && data.value[tableIndex]?.img_url.length > 0
? <el-image
style="width: 20px; height: 20px;"
src={api.URL + data.value[tableIndex]?.img_url[0]}
preview-src-list={data.value[tableIndex]?.img_url.map(i => api.URL + i)}
fit="cover"
/>
: ''}
</n-space>
)
},
key: 'title',
align: 'left',
width: 120,
ellipsis: { tooltip: true },
},
{
title: '商家',
key: 'agency_name_jc',
align: 'left',
width: 150,
ellipsis: { tooltip: true },
render: (row) => {
return (
<template
class="cursor-pointer flex justify-center items-center"
title="联系他"
onClick={async () => {
await api.save_chat_info({
send_type: 0,
id: row.id,
agency_user_id: row.agency_user_id,
OE: row.OE,
title: row.title,
parts_brand: row.parts_brand,
amount_pay: row.amount_pay,
})
chatStore.setShow()
chatStore.setChatObj(row)
}}
>
<span>{row.agency_name_jc}</span>
<el-icon size="16" class="ml-1" style="color: red;"><ChatDotRound /></el-icon>
</template>
)
},
},
{ title: '品质类型', key: 'parts_type', align: 'left', width: 100, ellipsis: { tooltip: true } },
{ title: '商品品牌', key: 'parts_brand', align: 'left', width: 100, ellipsis: { tooltip: true } },
{
title: '销售价',
key: 'amount_pay',
align: 'left',
width: 80,
ellipsis: { tooltip: true },
render(row) {
return h('span', {
style: 'color: red;',
}, `¥${row.amount_pay}`)
},
},
{ title: '备注', key: 'remark', align: 'left', width: 150, ellipsis: { tooltip: true } },
{
title: '操作',
key: 'actions',
fixed: 'right',
align: 'center',
width: 80,
hideInExcel: true,
render(row) {
return (
<>
<n-button
size="small"
type="warning"
tertiary
onClick={() => dblFun(row)}
title="加入购物车"
>
+
<el-icon size="16"><ShoppingCart /></el-icon>
</n-button>
</>
)
},
},
].map((column, columnIndex) => ({
...column,
cellProps: (rowData, rowIndex) => {
const table = document.activeElement?.closest('.n-data-table')
const tableIndex = table ? tableRefs.value.findIndex(t => t.$el === table) : -1
const currentCell = currentCells.value.get(`table${tableIndex}`) || { rowIndex: 0, columnIndex: 0 }
return {
style: {
cursor: 'pointer',
outline: 'none',
backgroundColor:
tableIndex === activeTableIndex.value &&
rowIndex === currentCell.rowIndex &&
columnIndex === currentCell.columnIndex
? 'inherit'
: 'inherit',
// padding: '4px 8px'
},
tabindex: tableIndex === activeTableIndex.value &&
rowIndex === currentCell.rowIndex &&
columnIndex === currentCell.columnIndex ? 0 : -1,
onKeydown: (e) => handleKeyDown(e, rowIndex, columnIndex, `table${tableIndex}`),
onClick: (e) => {
handleCellClick(e, rowIndex, columnIndex, tableIndex)
const clickedTable = e.target.closest('.n-data-table')
const clickedTableIndex = tableRefs.value.findIndex(t => t.$el === clickedTable)
if (clickedTableIndex !== -1) {
activeTableIndex.value = clickedTableIndex
currentCells.value.set(`table${clickedTableIndex}`, { rowIndex, columnIndex })
nextTick(() => {
const cell = e.target.closest('.n-data-table-td')
if (cell) {
cell.focus()
}
})
}
}
}
}
}))
}
// 共享的列配置
const columns = createColumns()
// 7. 监听和生命周期钩子
watch(() => tableRefs.value.length, (newLength) => {
if (newLength > 0) {
initializeFirstCell()
}
})
onBeforeUnmount(() => {
initialized.value = false
currentCells.value.clear()
activeTableIndex.value = 0
})
template部分
<n-data-table
:ref="el => registerTable(el)"
remote
size="small"
:columns="createColumns(index)"
:data="item.list"
:row-key="(row) => row.id"
:checked-row-keys="selectRow"
@update:checked-row-keys="handleCheck"
class="keyboard-navigable-table"
:class="index === 0 ? '' : 'mt-5'"
/>
截图:(关于boss,具体想要定位干嘛用,他也不清楚,所以暂时只是实现定位,作用还未体现,因为与商品页enter键加入购物车功能冲突,功能逻辑并未确定)懂的都懂,尊重但不理解