# useTableRequest

用于表格请求的 Hook，支持分页、防抖、自动/手动请求、参数变化自动刷新等功能。

## 特性

- 🔄 **自动/手动请求**：支持组件挂载时自动请求，也可手动触发
- 📄 **内置分页管理**：自动处理分页状态，支持自定义分页参数名
- ⏱️ **可选防抖**：支持参数变化时防抖请求（默认不防抖）
- 🏎️ **竞态控制**：自动处理请求竞态，只有最后一次请求的结果会被使用
- 🔧 **数据转换**：支持请求前转换参数、请求后转换响应数据
- 📊 **响应式参数**：支持 `ref`、`computed` 和 getter 函数形式的参数

## 全局配置

可以在应用入口处设置全局默认配置，避免每次使用时重复配置：

```typescript
import { setTableRequestGlobalConfig } from 'xn-fe-tools'

// 在 main.ts 中设置全局配置
setTableRequestGlobalConfig({
  immediate: true,
  debounceDelay: 500,
  pagination: {
    defaultPageSize: 20,
    pageKey: 'pageNum',
    pageSizeKey: 'size',
  },
  dataField: 'data', // 响应数据中的数据字段
  totalField: 'total', // 响应数据中的总数字段
  currentPageField: 'page', // 响应数据中的当前页码字段
})
```

局部配置会覆盖全局配置。

## 基本用法

```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useTableRequest } from 'xn-fe-tools'

interface User {
  id: number
  name: string
}

interface ApiResponse {
  code: number
  data: {
    records: User[]
    total: number
  }
}

interface QueryParams {
  keyword: string
}

// 模拟请求函数
const fetchUserList = (params: QueryParams) => {
  // 这里的 fetch 返回 Promise<ApiResponse>
  return fetch(`/api/users?${new URLSearchParams(params as any)}`).then(
    res => res.json() as Promise<ApiResponse>
  )
}

const search = ref('')

const { data, loading, error, pagination, run, onPageChange, onSizeChange } = useTableRequest<
  User,
  ApiResponse,
  QueryParams
>(fetchUserList, {
  params: computed(() => ({ keyword: search.value })),
  immediate: true,
  debounceDelay: 300,
  transformResult: (res: ApiResponse) => ({
    list: res.data.records,
    total: res.data.total,
  }),
})
</script>

<template>
  <div>
    <input v-model="search" placeholder="搜索..." />

    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <table v-else>
      <tr v-for="item in data" :key="item.id">
        <td>{{ item.name }}</td>
      </tr>
    </table>

    <div class="pagination">
      <span>共 {{ pagination.total }} 条</span>
      <button @click="onPageChange(pagination.page - 1)" :disabled="pagination.page <= 1">
        上一页
      </button>
      <span>第 {{ pagination.page }} 页</span>
      <button
        @click="onPageChange(pagination.page + 1)"
        :disabled="pagination.page * pagination.pageSize >= pagination.total"
      >
        下一页
      </button>
    </div>
  </div>
</template>
```

## API

### 函数签名

```ts
function useTableRequest<T = any, R = any, P extends object = any>(
  requestFn: RequestFunction<any, R>,
  options?: UseTableRequestOptions<T, R, P>
): UseTableRequestReturn<T>
```

### 参数

#### requestFn

- **类型**: `(params: any) => Promise<R>`
- **必填**: 是
- **说明**: 请求函数，接收合并后的参数，返回 Promise

#### options

配置选项对象：

| 参数               | 类型                  | 默认值  | 说明                                                 |
| ------------------ | --------------------- | ------- | ---------------------------------------------------- |
| `params`           | `MaybeRefOrGetter<P>` | `{}`    | 请求参数，支持 ref、computed 或 getter 函数          |
| `immediate`        | `boolean`             | `true`  | 是否立即执行请求                                     |
| `debounceDelay`    | `number`              | `0`     | 防抖延迟（毫秒），默认不防抖                         |
| `pagination`       | `PaginationConfig`    | 见下方  | 分页配置                                             |
| `dataField`        | `string`              | `''`    | 数据列表字段名，空则自动推断，支持点号分隔的嵌套路径 |
| `totalField`       | `string`              | `''`    | 总数字段名，空则自动推断，支持点号分隔的嵌套路径     |
| `currentPageField` | `string`              | `''`    | 当前页码字段名，空则不修正，支持点号分隔的嵌套路径   |
| `transformParams`  | `Function`            | -       | 请求前转换参数                                       |
| `transformResult`  | `Function`            | -       | 请求后转换结果                                       |
| `onSuccess`        | `Function`            | -       | 请求成功回调                                         |
| `onError`          | `Function`            | -       | 请求失败回调                                         |
| `incremental`      | `boolean`             | `false` | 是否开启数据增量模式（适合滚动加载）                 |

#### PaginationConfig

分页配置选项：

| 参数              | 类型     | 默认值       | 说明           |
| ----------------- | -------- | ------------ | -------------- |
| `defaultPage`     | `number` | `1`          | 默认页码       |
| `defaultPageSize` | `number` | `10`         | 默认每页数量   |
| `pageKey`         | `string` | `'page'`     | 页码字段名     |
| `pageSizeKey`     | `string` | `'pageSize'` | 每页数量字段名 |

### 返回值

| 属性           | 类型                              | 说明         |
| -------------- | --------------------------------- | ------------ |
| `data`         | `Ref<T[]>`                        | 列表数据     |
| `loading`      | `Ref<boolean>`                    | 加载状态     |
| `error`        | `Ref<Error \| null>`              | 错误信息     |
| `pagination`   | `PaginationState`                 | 分页信息     |
| `run`          | `() => Promise<void>`             | 手动触发请求 |
| `onPageChange` | `(page: number) => Promise<void>` | 翻页         |
| `onSizeChange` | `(size: number) => Promise<void>` | 改变每页数量 |
| `refresh`      | `() => Promise<void>`             | 刷新数据     |

#### PaginationState

```ts
interface PaginationState {
  page: number // 当前页码
  pageSize: number // 每页数量
  total: number // 总数
}
```

## 高级用法

### 自定义分页参数名

```ts
const { data, pagination } = useTableRequest(fetchData, {
  pagination: {
    defaultPage: 1,
    defaultPageSize: 20,
    pageKey: 'pageNum', // 自定义页码字段名
    pageSizeKey: 'size', // 自定义每页数量字段名
  },
  transformResult: res => ({
    list: res.records,
    total: res.total,
  }),
})
```

### 自定义参数转换

```ts
const { data } = useTableRequest(fetchData, {
  params: computed(() => ({ keyword: search.value, status: status.value })),
  transformParams: (params, pagination) => ({
    // 完全自定义请求参数格式
    searchKeyword: params.keyword,
    filterStatus: params.status,
    current: pagination.page,
    size: pagination.pageSize,
  }),
  transformResult: res => ({
    list: res.data.list,
    total: res.data.totalCount,
  }),
})
```

### 自定义数据字段

当后端返回的数据格式不标准时，可以指定数据字段：

```ts
// 后端返回格式: { result: { items: [...], count: 100 } }
const { data } = useTableRequest(fetchData, {
  dataField: 'result.items', // 嵌套路径: response.result.items
  totalField: 'result.count', // 嵌套路径: response.result.count
})

// 支持多层嵌套: { data: { list: { items: [...] }, pagination: { total: 100 } } }
const { data } = useTableRequest(fetchData, {
  dataField: 'data.list.items', // response.data.list.items
  totalField: 'data.pagination.total', // response.data.pagination.total
})

// 页码修正: 后端返回的当前页码与客户端不一致时自动修正
const { data } = useTableRequest(fetchData, {
  currentPageField: 'data.currentPage', // 后端返回的当前页码字段
  // 如果后端返回的页码与客户端不一致，会自动更新内部页码状态
})

// 或者使用全局配置
setTableRequestGlobalConfig({
  dataField: 'data.records',
  totalField: 'data.total',
})

const { data } = useTableRequest(fetchData) // 自动使用全局配置
```

### 手动控制请求

```ts
const { data, run } = useTableRequest(fetchData, {
  immediate: false, // 不自动请求
  debounceDelay: 0, // 禁用防抖
})

// 手动触发请求
const handleSearch = () => {
  run()
}

// 刷新数据（重置页码为 1）
const handleRefresh = () => {
  refresh()
}
```

### 滚动加载 (增量模式)

对于无限滚动或“加载更多”的场景，可以开启 `incremental` 模式：

```vue
<script setup lang="ts">
import { useTableRequest } from 'xn-fe-tools'

const { data, pagination, refresh } = useTableRequest(fetchData, {
  incremental: true, // 开启增量模式
  pagination: { defaultPageSize: 20 },
})

const loadMore = () => {
  if (data.value.length < pagination.total) {
    pagination.page++
  }
}

const onPullRefresh = () => {
  refresh() // 增量模式下，refresh 会清空 data 并重置 page 为 1
}
</script>
```

### 请求回调

```ts
const { data } = useTableRequest(fetchData, {
  onSuccess: (list, response) => {
    console.log('请求成功', list)
    // 可以在这里处理其他逻辑
  },
  onError: error => {
    console.error('请求失败', error)
    // 显示错误提示
    message.error(error.message)
  },
})
```

### 配合 toAwaitFetch 使用

`useTableRequest` 可以与 `toAwaitFetch` 无缝配合使用，只需将解构返回值转换为 Promise：

```ts
import { ref, computed } from 'vue'
import { useTableRequest, sendGet } from 'xn-fe-tools'

interface User {
  id: number
  name: string
}

// 定义接口响应类型
interface UserListResponse {
  code: number
  data: {
    list: User[]
    total: number
  }
}

// 封装请求函数，将 toAwaitFetch 的解构返回转换为 Promise
const fetchUserList = async (params: any): Promise<UserListResponse> => {
  const [data, error, success] = await sendGet<UserListResponse>('/api/users', params)

  if (!success) {
    // 将错误转换为 Error 对象抛出，由 useTableRequest 的 onError 处理
    throw new Error(error?.message || '请求失败')
  }

  return data
}

const search = ref('')

const { data, loading, pagination, onPageChange, onSizeChange } = useTableRequest<
  User,
  UserListResponse,
  any
>(fetchUserList, {
  params: computed(() => ({ keyword: search.value })),
  immediate: true,
  transformResult: (res: UserListResponse) => ({
    list: res.data.list,
    total: res.data.total,
  }),
  onError: error => {
    console.error('请求失败:', error.message)
  },
})
```

如果你已经通过 `createInstance` 创建了全局配置的实例，也可以直接使用：

```ts
import http from '@/utils/http' // 已配置好的 toAwaitFetch 实例

const fetchUserList = async (params: any) => {
  const [data, error, success] = await http.sendGet('/api/users', params)
  if (!success) throw new Error(error?.message || '请求失败')
  return data
}

const { data, loading, pagination } = useTableRequest(fetchUserList, {
  // toAwaitFetch 实例已配置 transformResponse，这里直接使用转换后的数据
  dataField: 'list',
  totalField: 'total',
})
```

### 配合 Element Plus 分页组件

```vue
<script setup lang="ts">
import { useTableRequest } from 'xn-fe-tools'

const { data, loading, pagination, onPageChange, onSizeChange } = useTableRequest(fetchData, {
  transformResult: res => ({ list: res.data, total: res.total }),
})
</script>

<template>
  <el-table :data="data" v-loading="loading">
    <!-- columns -->
  </el-table>

  <el-pagination
    v-model:current-page="pagination.page"
    v-model:page-size="pagination.pageSize"
    :total="pagination.total"
    :page-sizes="[10, 20, 50, 100]"
    layout="total, sizes, prev, pager, next"
  />
</template>
```

### 配合 Ant Design Vue Table

`useTableRequest` 的 `pagination` 状态可以很容易地转换为 Ant Design Vue 所需的格式。

```vue
<script setup lang="ts">
import { computed } from 'vue'
import { useTableRequest } from 'xn-fe-tools'

const { data, loading, pagination, run, onPageChange, onSizeChange } = useTableRequest(fetchData, {
  // ... options
})

// 适配 Ant Design Vue 的分页对象
const antdPagination = computed(() => ({
  current: pagination.page,
  pageSize: pagination.pageSize,
  total: pagination.total,
  showSizeChanger: true,
  onChange: onPageChange,
  onShowSizeChange: (_: number, size: number) => onSizeChange(size),
}))

const handleTableChange = (pag: any) => {
  onPageChange(pag.current)
  if (pag.pageSize !== pagination.pageSize) {
    onSizeChange(pag.pageSize)
  }
}
</script>

<template>
  <a-table
    :dataSource="data"
    :loading="loading"
    :pagination="antdPagination"
    @change="handleTableChange"
  >
    <!-- columns -->
  </a-table>
</template>
```

## 注意事项

1. **参数变化自动重置页码**：当 `params` 变化时，会自动重置到第一页并触发请求
2. **防抖仅对参数变化生效**：`run`、`onPageChange`、`onSizeChange` 不受防抖影响
3. **竞态控制**：当快速发起多次请求时，只有最后一次请求的结果会被使用，旧请求的响应会被自动忽略
4. **transformResult 必须返回固定格式**：必须返回 `{ list: T[], total: number }` 格式
5. **不要在循环或条件语句中调用**：遵循 Vue Composition API 的使用规范

## 类型定义

```ts
export type MaybeRef<T> = T | Ref<T>
export type MaybeRefOrGetter<T> = MaybeRef<T> | ComputedRef<T> | (() => T)

export type RequestFunction<P, R> = (params: P) => Promise<R>

export interface UseTableRequestGlobalConfig {
  immediate?: boolean
  debounceDelay?: number
  pagination?: PaginationConfig
  dataField?: string // 支持点号分隔的嵌套路径，如 "data" 或 "data.list"
  totalField?: string // 支持点号分隔的嵌套路径，如 "total" 或 "data.total"
  currentPageField?: string // 支持点号分隔的嵌套路径，如 "page" 或 "data.currentPage"
}

// 设置全局配置
export function setTableRequestGlobalConfig(config: UseTableRequestGlobalConfig): void

// 获取当前全局配置
export function getTableRequestGlobalConfig(): UseTableRequestGlobalConfig

export interface PaginationConfig {
  defaultPage?: number
  defaultPageSize?: number
  pageKey?: string
  pageSizeKey?: string
}

export interface TransformResultData<T> {
  list: T[]
  total: number
}

export interface UseTableRequestOptions<T = any, R = any, P = any> {
  params?: MaybeRefOrGetter<P>
  immediate?: boolean
  debounceDelay?: number
  pagination?: PaginationConfig
  dataField?: string
  totalField?: string
  currentPageField?: string
  transformParams?: (params: P, pagination: { page: number; pageSize: number }) => any
  transformResult?: (response: R) => TransformResultData<T>
  onSuccess?: (data: T[], response: R) => void
  onError?: (error: Error) => void
  incremental?: boolean
}

export interface PaginationState {
  page: number
  pageSize: number
  total: number
}

export interface UseTableRequestReturn<T> {
  data: Ref<T[]>
  loading: Ref<boolean>
  error: Ref<Error | null>
  pagination: PaginationState
  run: () => Promise<void>
  onPageChange: (page: number) => Promise<void>
  onSizeChange: (size: number) => Promise<void>
  refresh: () => Promise<void>
}
```
