# TanStack Query 使用指南

## 📋 概述

TanStack Query (原名 React Query) 是一个强大的数据获取和状态管理库，为 Vue 3 项目提供：

- 🚀 **智能缓存**: 自动缓存和去重请求
- 🔄 **后台更新**: 自动在后台重新获取数据
- ⚡ **实时同步**: 窗口聚焦时自动刷新
- 📱 **离线支持**: 网络恢复时自动重试
- 🎯 **响应式**: 与 Vue 3 Composition API 完美集成

## 🚀 快速开始

### 1. 基础配置

项目已配置好 TanStack Query，在 `main.ts` 中：

```typescript
import { VueQueryPlugin } from '@tanstack/vue-query'

app.use(VueQueryPlugin)
```

### 2. 基础用法

```vue
<template>
  <div v-if="isLoading">加载中...</div>
  <div v-else-if="error">发生错误: {{ error.message }}</div>
  <div v-else>
    <div v-for="item in data" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { apiService } from '@/api'

const { isLoading, error, data } = useQuery({
  queryKey: ['users'],
  queryFn: () => apiService.getUserList()
})
</script>
```

## 📚 核心 API

### useQuery - 数据获取

用于获取数据的基础 hook：

```typescript
const {
  data,           // 查询结果
  isLoading,      // 首次加载状态
  isFetching,     // 获取状态（包括后台刷新）
  error,          // 错误信息
  refetch,        // 手动重新获取
  remove          // 移除查询缓存
} = useQuery({
  queryKey: ['queryKey'],
  queryFn: fetchFunction,
  enabled: true,  // 是否启用查询
  staleTime: 30000, // 数据过期时间（30秒）
  gcTime: 300000    // 缓存垃圾回收时间（5分钟）
})
```

### useMutation - 数据修改

用于创建、更新、删除操作：

```typescript
const {
  mutate,         // 执行变更
  mutateAsync,    // 异步执行变更
  isLoading,      // 变更加载状态
  error,          // 变更错误
  isSuccess       // 变更成功状态
} = useMutation({
  mutationFn: (data) => apiService.createUser(data),
  onSuccess: (data) => {
    // 成功回调
    queryClient.invalidateQueries(['users'])
  },
  onError: (error) => {
    // 重要： ElMessage 已经在httpClient 封装过了，如果不需要对异常进行特殊处理，你完全不需要 onError
  }
})
```

### useQueryClient - 查询客户端

用于手动管理缓存：

```typescript
import { useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

// 手动设置查询数据
queryClient.setQueryData(['user', userId], userData)

// 使特定查询失效
queryClient.invalidateQueries({ queryKey: ['users'] })

// 移除查询
queryClient.removeQueries(['user', userId])

// 获取查询数据
const userData = queryClient.getQueryData(['user', userId])
```

## 🎯 实际应用场景

### 1. 列表页面数据获取

列表不需要使用这个，使用封装好的 useTable 即可

### 2. 详情页面数据获取

```vue
<template>
  <div v-if="isLoading">
    <ElSkeleton />
  </div>
  <div v-else-if="error">
    <ElResult icon="error" title="加载失败" :sub-title="error.message">
      <template #extra>
        <ElButton @click="refetch">重试</ElButton>
      </template>
    </ElResult>
  </div>
  <div v-else>
    <h1>{{ data?.name }}</h1>
    <p>{{ data?.description }}</p>
  </div>
</template>

<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { userApi } from '@/api'

interface Props {
  userId: number
}

const props = defineProps<Props>()

const { isLoading, error, data, refetch } = useQuery({
  queryKey: computed(() => ['user', props.userId]),
  queryFn: () => userApi.getUserDetail(props.userId),
  enabled: computed(() => !!props.userId)
})
</script>
```

### 3. 表单提交和数据更新

```vue
<template>
  <ElForm @submit="handleSubmit">
    <ElFormItem label="姓名">
      <ElInput v-model="form.name" />
    </ElFormItem>
    <ElFormItem>
      <ElButton 
        type="primary" 
        @click="handleSubmit"
        :loading="isLoading"
      >
        {{ isLoading ? '保存中...' : '保存' }}
      </ElButton>
    </ElFormItem>
  </ElForm>
</template>

<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { userApi } from '@/api'

const queryClient = useQueryClient()

const form = reactive({
  name: '',
  email: ''
})

const { mutate: createUser, isLoading } = useMutation({
  mutationFn: (userData) => userApi.createUser(userData),
  onSuccess: (newUser) => {
    ElMessage.success('创建成功')
    
    // 方法1: 使列表查询失效，重新获取
    queryClient.invalidateQueries(['users'])
    
    // 方法2: 手动更新缓存（性能更好）
    queryClient.setQueryData(['users'], (oldData) => {
      return {
        ...oldData,
        list: [newUser, ...oldData.list]
      }
    })
    
    // 清空表单
    Object.assign(form, { name: '', email: '' })
  },
  // 不用 onError，因为 ElMessage 已经在httpClient 封装过了
})

const handleSubmit = () => {
  createUser(form)
}
</script>
```

### 4. 依赖查询

```vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'

const props = defineProps<{
  userId: number
}>()

const otherRefVariable = ref(null)

// 用户列表查询
const { data: users } = useQuery({
  queryKey: ['users'],
  queryFn: () => userApi.getUserList()
})

// 用户详情查询（依赖于选中的用户ID）
// 如果是 props 的变量，需要 computed 包裹
const { data: userDetail, isLoading: isLoadingDetail } = useQuery({
  queryKey: computed(() => ['user', props.userId]),
  queryFn: () => userApi.getUserDetail(props.userId),
  enabled: computed(() => !!props.userId)
})

// 用户权限查询（依赖于 otherRefVariable）
// 如果依赖的变量是 ref 且不需要进行计算，则不需要 computed 包裹
const { data: userPermissions } = useQuery({
  queryKey: ['userPermissions', otherRefVariable],
  queryFn: () => userApi.getUserPermissions(otherRefVariable.value),
  enabled: computed(() => !!userDetail.value)
})
</script>
```

### 5. 无限滚动加载

```vue
<template>
  <div class="infinite-list">
    <div v-for="item in flatData" :key="item.id">
      {{ item.name }}
    </div>
    
    <div v-if="isFetchingNextPage" class="loading">
      加载更多...
    </div>
    
    <ElButton 
      v-if="hasNextPage" 
      @click="fetchNextPage"
      :loading="isFetchingNextPage"
    >
      加载更多
    </ElButton>
  </div>
</template>

<script setup lang="ts">
import { useInfiniteQuery } from '@tanstack/vue-query'
import { userApi } from '@/api'

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage
} = useInfiniteQuery({
  queryKey: ['users', 'infinite'],
  queryFn: ({ pageParam = 1 }) => userApi.getUserList({
    current: pageParam,
    size: 20
  }),
  getNextPageParam: (lastPage, pages) => {
    // 判断是否还有下一页
    if (lastPage.current < lastPage.pages) {
      return lastPage.current + 1
    }
    return undefined
  }
})

// 展平数据
const flatData = computed(() => {
  return data.value?.pages.flatMap(page => page.list) || []
})
</script>
```

## 🔧 高级功能

### 3. 条件查询

```typescript
const searchQuery = ref('')
const shouldSearch = computed(() => searchQuery.value.length >= 2)

const { data: searchResults } = useQuery({
  queryKey: computed(() => ['search', searchQuery.value]),
  queryFn: () => searchApi.search(searchQuery.value),
  enabled: shouldSearch,
  staleTime: 30000 // 搜索结果缓存30秒
})
```

## ⚙️ 配置选项

### 常用配置项

```typescript
useQuery({
  queryKey: ['key'],
  queryFn: fetchFunction,
  
  // 缓存配置
  staleTime: 30000,        // 数据新鲜时间
  gcTime: 300000,          // 垃圾回收时间
  
  // 重试配置
  retry: 3,                // 重试次数
  retryDelay: 1000,        // 重试延迟
  
  // 刷新配置
  refetchOnMount: true,    // 组件挂载时刷新
  refetchOnWindowFocus: false, // 窗口聚焦时刷新
  refetchOnReconnect: true,    // 网络重连时刷新
  
  // 条件配置
  enabled: true,           // 是否启用查询
  
  // 占位数据
  placeholderData: [],     // 占位数据
  keepPreviousData: true   // 保留之前的数据
})
```

## 📋 最佳实践

### 3. 性能优化

```typescript
// 使用 keepPreviousData 避免加载闪烁
const { data } = useQuery({
  queryKey: computed(() => ['users', pagination.value]),
  queryFn: () => userApi.getUserList(pagination.value),
  keepPreviousData: true
})
```

## 🐛 常见问题

### Q: 数据没有自动更新？

A: 检查查询键是否正确设置为响应式：

```typescript
// ❌ 错误：查询键不是响应式的
useQuery({
  queryKey: ['users', searchQuery.value], // 不会响应变化
  queryFn: () => api.search(searchQuery.value)
})

// ✅ 正确：使用 computed 让查询键响应式
useQuery({
  queryKey: computed(() => ['users', searchQuery.value]),
  queryFn: () => api.search(searchQuery.value)
})
```

### Q: 如何清除特定查询的缓存？

A: 使用 queryClient 的相关方法：

```typescript
const queryClient = useQueryClient()

// 使查询失效（会重新获取）
queryClient.invalidateQueries({ queryKey: ['users'] })

// 移除查询（从缓存中删除）
queryClient.removeQueries({ queryKey: ['users'] })

// 重置查询（重置为初始状态）
queryClient.resetQueries({ queryKey: ['users'] })
```