refactor: 学生列表
This commit is contained in:
parent
3199fb7748
commit
70375c04a7
|
|
@ -0,0 +1,405 @@
|
||||||
|
# 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'] })
|
||||||
|
```
|
||||||
|
|
@ -1,161 +1,339 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { StudentInfo } from '@/service/types'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { useDebounceFn, whenever } from '@vueuse/core'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { teacherAnalysisExportStudentListUsingPost, teacherAnalysisStudentListUsingPost } from '@/service/laoshichengjifenxi'
|
||||||
|
import { useHomeStore } from '@/store/home'
|
||||||
|
|
||||||
interface Student {
|
const homeStore = useHomeStore()
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
studentId: string
|
|
||||||
className: string
|
|
||||||
status: string
|
|
||||||
avatar?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const students = ref<Student[]>([])
|
|
||||||
|
|
||||||
// 模拟学生数据
|
// 检查是否可以加载学生数据
|
||||||
const mockStudents: Student[] = [
|
const canLoadStudents = computed(() => {
|
||||||
{
|
return homeStore.selectedClassId && homeStore.selectedExamId && homeStore.selectedSubjectId && homeStore.selectedGradeKey
|
||||||
id: '1',
|
|
||||||
name: '张三',
|
|
||||||
studentId: '20230001',
|
|
||||||
className: '一年级1班',
|
|
||||||
status: '在校',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: '李四',
|
|
||||||
studentId: '20230002',
|
|
||||||
className: '一年级1班',
|
|
||||||
status: '在校',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: '王五',
|
|
||||||
studentId: '20230003',
|
|
||||||
className: '一年级1班',
|
|
||||||
status: '请假',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: '赵六',
|
|
||||||
studentId: '20230004',
|
|
||||||
className: '一年级2班',
|
|
||||||
status: '在校',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: '孙七',
|
|
||||||
studentId: '20230005',
|
|
||||||
className: '一年级2班',
|
|
||||||
status: '在校',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 过滤后的学生列表
|
|
||||||
const filteredStudents = computed(() => {
|
|
||||||
if (!searchQuery.value) {
|
|
||||||
return students.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase()
|
|
||||||
return students.value.filter(student =>
|
|
||||||
student.name.toLowerCase().includes(query)
|
|
||||||
|| student.studentId.toLowerCase().includes(query),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索处理
|
// 防抖搜索关键词
|
||||||
function handleSearch() {
|
const debouncedSearchQuery = ref('')
|
||||||
// 这里可以添加防抖逻辑
|
const updateDebouncedSearch = useDebounceFn((value: string) => {
|
||||||
|
debouncedSearchQuery.value = value
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// 监听搜索关键词变化
|
||||||
|
whenever(() => searchQuery.value, (newValue) => {
|
||||||
|
updateDebouncedSearch(newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用 TanStack Query 获取学生数据
|
||||||
|
const {
|
||||||
|
data: studentsData,
|
||||||
|
isLoading: loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: computed(() => [
|
||||||
|
'students',
|
||||||
|
homeStore.selectedClassId,
|
||||||
|
homeStore.selectedExamId,
|
||||||
|
homeStore.selectedSubjectId,
|
||||||
|
homeStore.selectedGradeKey,
|
||||||
|
debouncedSearchQuery.value,
|
||||||
|
]),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await teacherAnalysisStudentListUsingPost({
|
||||||
|
body: {
|
||||||
|
class_id: homeStore.selectedClassId!,
|
||||||
|
exam_subject_id: homeStore.selectedSubjectId!,
|
||||||
|
grade_id: homeStore.selectedGradeKey!,
|
||||||
|
keyword: debouncedSearchQuery.value || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const responseData = response as any
|
||||||
|
return responseData.data || { attentioned_students: [], other_students: [] }
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!canLoadStudents.value),
|
||||||
|
staleTime: 30000, // 30秒内数据保持新鲜
|
||||||
|
gcTime: 300000, // 5分钟后清理缓存
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算学生列表
|
||||||
|
const attentionedStudents = computed(() => studentsData.value?.attentioned_students || [])
|
||||||
|
const otherStudents = computed(() => studentsData.value?.other_students || [])
|
||||||
|
|
||||||
|
// 获取排名变化图标
|
||||||
|
function getRankChangeIcon(diff?: number) {
|
||||||
|
if (!diff || diff === 0)
|
||||||
|
return ''
|
||||||
|
return diff > 0 ? 'i-fluent:arrow-up-20-filled' : 'i-fluent:arrow-down-20-filled'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取排名变化颜色
|
||||||
|
function getRankChangeColor(diff?: number) {
|
||||||
|
if (!diff || diff === 0)
|
||||||
|
return 'text-gray-400'
|
||||||
|
return diff > 0 ? 'text-red-500' : 'text-green-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到学生详情
|
// 跳转到学生详情
|
||||||
function goToStudentDetail(student: Student) {
|
function goToStudentDetail(student: StudentInfo) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/student/detail?id=${student.id}`,
|
url: `/pages/student/detail?id=${student.info_id}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载学生数据
|
// 切换收藏状态
|
||||||
async function loadStudents() {
|
function toggleFavorite(student: StudentInfo) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '功能开发中',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载功能
|
||||||
|
async function handleDownload() {
|
||||||
try {
|
try {
|
||||||
// 这里可以调用实际的API
|
await teacherAnalysisExportStudentListUsingPost({
|
||||||
students.value = mockStudents
|
body: {
|
||||||
|
class_id: homeStore.selectedClassId!,
|
||||||
|
exam_subject_id: homeStore.selectedSubjectId!,
|
||||||
|
grade_id: homeStore.selectedGradeKey!,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('加载学生数据失败:', error)
|
console.error('下载学生数据失败:', error)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '加载失败',
|
title: '下载失败',
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 分享功能
|
||||||
loadStudents()
|
function handleShare() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '分享功能开发中',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索输入
|
||||||
|
function handleSearch() {
|
||||||
|
// 搜索逻辑已通过 whenever 和防抖处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理查询错误
|
||||||
|
whenever(() => error.value, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('加载学生数据失败:', err)
|
||||||
|
uni.showToast({
|
||||||
|
title: '加载失败',
|
||||||
|
icon: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 初始化选项数据
|
||||||
|
if (homeStore.examOptions.length === 0) {
|
||||||
|
await homeStore.fetchOptions()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view>
|
<view class="min-h-screen bg-gray-50">
|
||||||
<!-- 头部 -->
|
<!-- 标题栏 -->
|
||||||
<view class="border-b border-gray-100 bg-white px-4 py-3">
|
<view class="relative flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
|
||||||
<text class="text-lg text-gray-800 font-semibold">学生名单</text>
|
<view class="flex-1 text-center">
|
||||||
|
<text class="text-lg text-gray-800 font-semibold">学生名单</text>
|
||||||
|
</view>
|
||||||
|
<view class="absolute right-4 flex items-center gap-3">
|
||||||
|
<wd-button
|
||||||
|
type="icon"
|
||||||
|
size="small"
|
||||||
|
@click="handleDownload"
|
||||||
|
>
|
||||||
|
<view class="i-fluent:arrow-download-20-regular text-lg" />
|
||||||
|
</wd-button>
|
||||||
|
<wd-button
|
||||||
|
type="icon"
|
||||||
|
size="small"
|
||||||
|
@click="handleShare"
|
||||||
|
>
|
||||||
|
<view class="i-fluent:share-20-regular text-lg" />
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
<!-- 选择器区域 -->
|
||||||
<view class="border-b border-gray-100 bg-white px-4 py-3">
|
<view class="border-b border-gray-100 bg-white px-4 py-3">
|
||||||
<view class="relative">
|
<wd-drop-menu>
|
||||||
<input
|
<!-- 班级选择 -->
|
||||||
v-model="searchQuery"
|
<wd-drop-menu-item
|
||||||
class="h-10 w-full rounded-full bg-gray-100 pl-10 pr-4 text-sm placeholder-gray-500"
|
v-model="homeStore.selectedClassId"
|
||||||
placeholder="搜索学生姓名或学号"
|
:options="homeStore.classOptions"
|
||||||
@input="handleSearch"
|
|
||||||
>
|
|
||||||
<uni-icons
|
|
||||||
type="search"
|
|
||||||
size="18"
|
|
||||||
color="#9CA3AF"
|
|
||||||
class="absolute left-3 top-1/2 -translate-y-1/2"
|
|
||||||
/>
|
/>
|
||||||
|
<!-- 考试选择 -->
|
||||||
|
<wd-drop-menu-item
|
||||||
|
v-model="homeStore.selectedExamId"
|
||||||
|
:options="homeStore.examOptions"
|
||||||
|
/>
|
||||||
|
<!-- 科目选择 -->
|
||||||
|
<wd-drop-menu-item
|
||||||
|
v-model="homeStore.selectedSubjectId"
|
||||||
|
:options="homeStore.subjectOptions"
|
||||||
|
/>
|
||||||
|
</wd-drop-menu>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="border-b border-gray-100 bg-white px-4 py-3">
|
||||||
|
<wd-search
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="请输入学生姓名"
|
||||||
|
hide-cancel
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 重点关注的学生 -->
|
||||||
|
<view v-if="attentionedStudents.length > 0" class="mt-4">
|
||||||
|
<view class="flex items-center px-4 py-2">
|
||||||
|
<view class="i-fluent:star-20-filled mr-2 text-yellow-500" />
|
||||||
|
<text class="text-sm text-gray-700 font-medium">重点关注的学生</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<wd-cell-group border custom-class="mx-4 rounded-lg overflow-hidden">
|
||||||
|
<wd-cell
|
||||||
|
v-for="(student, index) in attentionedStudents"
|
||||||
|
:key="student.info_id"
|
||||||
|
:is-link="true"
|
||||||
|
@click="goToStudentDetail(student)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<view class="mr-3 h-8 w-8 flex items-center justify-center rounded-full bg-red-100">
|
||||||
|
<text class="text-sm text-red-600 font-semibold">{{ index + 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<text class="text-base text-gray-800 font-medium">
|
||||||
|
{{ student.student_name }}
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #value>
|
||||||
|
<view class="flex items-center gap-3">
|
||||||
|
<!-- 分数 -->
|
||||||
|
<text class="text-base text-gray-800 font-semibold">
|
||||||
|
{{ student.full_score || '--' }}分
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 排名变化 -->
|
||||||
|
<view class="flex items-center">
|
||||||
|
<view
|
||||||
|
v-if="getRankChangeIcon(student.class_diff_rank)"
|
||||||
|
:class="[getRankChangeIcon(student.class_diff_rank), getRankChangeColor(student.class_diff_rank)]"
|
||||||
|
class="mr-1 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<text :class="getRankChangeColor(student.class_diff_rank)" class="text-sm">
|
||||||
|
{{ Math.abs(student.class_diff_rank || 0) }}名
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 收藏按钮 -->
|
||||||
|
<wd-button
|
||||||
|
type="icon"
|
||||||
|
size="small"
|
||||||
|
@click.stop="toggleFavorite(student)"
|
||||||
|
>
|
||||||
|
<view class="i-fluent:star-20-filled text-yellow-500" />
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-cell>
|
||||||
|
</wd-cell-group>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 学生列表 -->
|
<!-- 学生列表 -->
|
||||||
<view class="flex-1 p-4">
|
<view v-if="otherStudents.length > 0" class="mt-4">
|
||||||
<view
|
<view class="px-4 py-2">
|
||||||
v-for="student in filteredStudents"
|
<text class="text-sm text-gray-700 font-medium">学生列表</text>
|
||||||
:key="student.id"
|
|
||||||
class="mb-3 flex items-center border border-gray-100 rounded-xl bg-white p-4 shadow-sm"
|
|
||||||
@tap="goToStudentDetail(student)"
|
|
||||||
>
|
|
||||||
<!-- 头像 -->
|
|
||||||
<view class="mr-3 h-12 w-12 flex items-center justify-center rounded-full bg-blue-100">
|
|
||||||
<text class="text-base text-blue-600 font-semibold">{{ student.name.slice(-2) }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 学生信息 -->
|
|
||||||
<view class="flex-1">
|
|
||||||
<view class="mb-1 flex items-center justify-between">
|
|
||||||
<text class="text-base text-gray-800 font-semibold">{{ student.name }}</text>
|
|
||||||
<text class="text-xs text-gray-500">{{ student.status }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="flex items-center text-sm text-gray-600">
|
|
||||||
<text class="mr-4">学号:{{ student.studentId }}</text>
|
|
||||||
<text>班级:{{ student.className }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 右箭头 -->
|
|
||||||
<uni-icons type="right" size="16" color="#D1D5DB" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<wd-cell-group border custom-class="mx-4 rounded-lg overflow-hidden">
|
||||||
<view v-if="filteredStudents.length === 0" class="flex flex-col items-center justify-center py-20">
|
<wd-cell
|
||||||
<uni-icons type="info" size="48" color="#D1D5DB" />
|
v-for="(student, index) in otherStudents"
|
||||||
<text class="mt-2 text-gray-500">暂无学生数据</text>
|
:key="student.info_id"
|
||||||
</view>
|
:is-link="true"
|
||||||
|
@click="goToStudentDetail(student)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<view class="mr-3 h-8 w-8 flex items-center justify-center rounded-full bg-gray-100">
|
||||||
|
<text class="text-sm text-gray-600 font-semibold">
|
||||||
|
{{ (attentionedStudents.length + index + 1) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<text class="text-base text-gray-800 font-medium">
|
||||||
|
{{ student.student_name }}
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #value>
|
||||||
|
<view class="flex items-center gap-3">
|
||||||
|
<!-- 分数 -->
|
||||||
|
<text class="text-base text-gray-800 font-semibold">
|
||||||
|
{{ student.full_score || '--' }}分
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 排名变化 -->
|
||||||
|
<view class="flex items-center">
|
||||||
|
<view
|
||||||
|
v-if="getRankChangeIcon(student.class_diff_rank)"
|
||||||
|
:class="[getRankChangeIcon(student.class_diff_rank), getRankChangeColor(student.class_diff_rank)]"
|
||||||
|
class="mr-1 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<text :class="getRankChangeColor(student.class_diff_rank)" class="text-sm">
|
||||||
|
{{ Math.abs(student.class_diff_rank || 0) }}名
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 收藏按钮 -->
|
||||||
|
<wd-button
|
||||||
|
type="icon"
|
||||||
|
size="small"
|
||||||
|
@click.stop="toggleFavorite(student)"
|
||||||
|
>
|
||||||
|
<view class="i-fluent:star-20-regular text-gray-400" />
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-cell>
|
||||||
|
</wd-cell-group>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<wd-status-tip
|
||||||
|
v-if="!loading && !canLoadStudents"
|
||||||
|
image="search"
|
||||||
|
tip="请选择班级、考试和科目"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<wd-status-tip
|
||||||
|
v-else-if="!loading && canLoadStudents && attentionedStudents.length === 0 && otherStudents.length === 0"
|
||||||
|
image="search"
|
||||||
|
tip="暂无学生数据"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading" class="flex items-center justify-center py-20">
|
||||||
|
<wd-loading />
|
||||||
|
<text class="ml-2 text-gray-500">加载中...</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ export * from './displayEnumLabel';
|
||||||
|
|
||||||
export * from './renzheng';
|
export * from './renzheng';
|
||||||
export * from './banjifenxi';
|
export * from './banjifenxi';
|
||||||
export * from './shujuzidianxiang';
|
export * from './shujuzidixiang';
|
||||||
export * from './shujuzidianleixing';
|
export * from './shujuzidileixing';
|
||||||
export * from './kaoshitongji';
|
export * from './kaoshitongji';
|
||||||
export * from './wenjianguanli';
|
export * from './wenjianguanli';
|
||||||
export * from './wenjianqiepianguanli';
|
export * from './wenjianqiepianguanli';
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue