Cursor

201 阅读2分钟

使用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键加入购物车功能冲突,功能逻辑并未确定)懂的都懂,尊重但不理解

image.png