322 lines
9.4 KiB
Vue
322 lines
9.4 KiB
Vue
<script lang="ts" setup>
|
||
import dayjs from 'dayjs'
|
||
import { toast } from 'vue-sonner'
|
||
import { userSearch } from '~/api'
|
||
import {
|
||
addStudentToClass,
|
||
deleteStudentClassRecord,
|
||
getStudentListByClass,
|
||
type ICourseClass,
|
||
} from '~/api/course'
|
||
import type { FetchError } from '~/types'
|
||
|
||
const props = defineProps<{
|
||
classItem: ICourseClass
|
||
}>()
|
||
|
||
const { data: students, refresh: refreshStudents } = useAsyncData(
|
||
`students-${props.classItem.classId}`,
|
||
() => getStudentListByClass(props.classItem.classId),
|
||
{
|
||
immediate: false,
|
||
watch: [() => props.classItem.classId],
|
||
},
|
||
)
|
||
|
||
const studentsSheetOpen = ref(false)
|
||
|
||
watch(studentsSheetOpen, (isOpen) => {
|
||
if (isOpen) {
|
||
refreshStudents()
|
||
}
|
||
})
|
||
|
||
const searchKeyword = ref('')
|
||
|
||
const {
|
||
data: searchResults,
|
||
refresh: refreshSearch,
|
||
clear: clearSearch,
|
||
} = useAsyncData(
|
||
() =>
|
||
userSearch({
|
||
searchType: 'student',
|
||
keyword: searchKeyword.value,
|
||
}),
|
||
{
|
||
immediate: false,
|
||
},
|
||
)
|
||
|
||
const triggerSearch = useDebounceFn(() => {
|
||
if (searchKeyword.value.length > 0) {
|
||
refreshSearch()
|
||
}
|
||
else {
|
||
clearSearch()
|
||
}
|
||
}, 500)
|
||
|
||
watch(searchKeyword, (newValue) => {
|
||
if (newValue.length > 0) {
|
||
triggerSearch()
|
||
}
|
||
})
|
||
|
||
const isInClass = (userId: number) => {
|
||
return students.value?.data?.some(item => item.studentId === userId)
|
||
}
|
||
|
||
const onAddStudent = async (userId: number) => {
|
||
toast.promise(
|
||
addStudentToClass({
|
||
classId: props.classItem.classId,
|
||
studentId: userId,
|
||
}),
|
||
{
|
||
loading: '正在添加学生...',
|
||
success: () => {
|
||
return '添加学生成功'
|
||
},
|
||
error: (error: FetchError) => {
|
||
if (error.status === 409) {
|
||
return '该学生已在班级中'
|
||
}
|
||
return '添加学生失败'
|
||
},
|
||
finally: () => {
|
||
refreshStudents()
|
||
},
|
||
},
|
||
)
|
||
}
|
||
|
||
const onDeleteRecord = async (recordId: number) => {
|
||
toast.promise(deleteStudentClassRecord(recordId), {
|
||
loading: '正在移除学生...',
|
||
success: () => {
|
||
return '移除学生成功'
|
||
},
|
||
error: () => {
|
||
return '移除学生失败'
|
||
},
|
||
finally: () => {
|
||
refreshStudents()
|
||
},
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<Card v-bind="props">
|
||
<CardHeader>
|
||
<CardTitle class="text-xl">
|
||
{{
|
||
classItem.className || "未命名班级"
|
||
}}
|
||
</CardTitle>
|
||
<CardDescription>
|
||
{{ classItem.notes || "没有描述" }}
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent class="flex flex-col gap-2">
|
||
<!-- <p class="text-xs text-muted-foreground">
|
||
班级人数:{{ students?.data.length || 0 }}
|
||
</p> -->
|
||
<p class="text-xs text-muted-foreground">
|
||
班级ID:{{ classItem.classId }}
|
||
</p>
|
||
<p class="text-xs text-muted-foreground">
|
||
创建时间:{{
|
||
dayjs(classItem.createTime).format("YYYY-MM-DD HH:mm:ss")
|
||
}}
|
||
</p>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
class="flex items-center gap-1"
|
||
@click="studentsSheetOpen = true"
|
||
>
|
||
<Icon
|
||
name="tabler:chevron-right"
|
||
size="16px"
|
||
/>
|
||
<span>班级详情</span>
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
<Sheet v-model:open="studentsSheetOpen">
|
||
<SheetContent class="w-[480px] !max-w-none space-y-4">
|
||
<SheetHeader>
|
||
<SheetTitle>{{ classItem.className || "未命名班级" }}</SheetTitle>
|
||
<SheetDescription>班级成员管理</SheetDescription>
|
||
</SheetHeader>
|
||
<div class="flex flex-col gap-4">
|
||
<Popover>
|
||
<PopoverTrigger as-child>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
class="flex items-center gap-1"
|
||
>
|
||
<Icon
|
||
name="tabler:plus"
|
||
size="16px"
|
||
/>
|
||
<span>添加学生</span>
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent
|
||
class="w-96"
|
||
:align="'center'"
|
||
>
|
||
<div class="flex flex-col gap-4">
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="keyword"
|
||
>
|
||
<FormItem>
|
||
<FormLabel>搜索学生</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
v-bind="componentField"
|
||
v-model="searchKeyword"
|
||
type="text"
|
||
placeholder="搜索学号/姓名/手机号"
|
||
/>
|
||
</FormControl>
|
||
<FormDescription>
|
||
<p class="text-xs">
|
||
搜索教师学号/姓名/手机号,然后添加到班级中
|
||
</p>
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
<hr />
|
||
<div class="flex flex-col gap-2">
|
||
<p class="text-sm text-muted-foreground">
|
||
搜索结果
|
||
</p>
|
||
<div
|
||
v-if="searchResults?.data && searchResults.data.length > 0"
|
||
class="flex flex-col gap-2"
|
||
>
|
||
<div
|
||
v-for="user in searchResults.data"
|
||
:key="user.userId"
|
||
class="flex justify-between items-center gap-2"
|
||
>
|
||
<div class="flex items-center gap-4">
|
||
<Avatar class="w-12 h-12 text-base">
|
||
<AvatarImage
|
||
:src="user.avatar || ''"
|
||
:alt="user.userName"
|
||
/>
|
||
<AvatarFallback class="rounded-lg">
|
||
{{ user.userName.slice(0, 2).toUpperCase() }}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
|
||
<div class="flex flex-col gap-1">
|
||
<h1
|
||
class="text-sm font-medium text-ellipsis line-clamp-1"
|
||
>
|
||
{{ user.userName || "未知学生" }}
|
||
</h1>
|
||
<p class="text-xs text-muted-foreground/80">
|
||
工号:{{ user.employeeId || "未知" }}
|
||
</p>
|
||
<p class="text-xs text-muted-foreground/80">
|
||
{{ user.collegeName || "未知学院" }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
class="flex items-center gap-1"
|
||
:disabled="isInClass(user.id!)"
|
||
@click="onAddStudent(user.id!)"
|
||
>
|
||
<Icon
|
||
v-if="!isInClass(user.id!)"
|
||
name="tabler:plus"
|
||
size="16px"
|
||
/>
|
||
<span>
|
||
{{ isInClass(user.id!) ? "已在班级" : "添加" }}
|
||
</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<EmptyScreen
|
||
v-else
|
||
title="没有搜索结果"
|
||
description="没有找到符合条件的学生"
|
||
icon="fluent-color:people-list-24"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<Table v-if="students?.data && students.data.length > 0">
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead class="w-[100px]">
|
||
学号
|
||
</TableHead>
|
||
<TableHead>姓名</TableHead>
|
||
<TableHead class="text-right">
|
||
操作
|
||
</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
<TableRow
|
||
v-for="student in students.data"
|
||
:key="student.student.studentId"
|
||
>
|
||
<TableCell class="font-medium">
|
||
{{ student.student.studentId }}
|
||
</TableCell>
|
||
<TableCell>
|
||
{{ student.student.userName }}
|
||
</TableCell>
|
||
<TableCell class="text-right">
|
||
<Button
|
||
variant="link"
|
||
size="xs"
|
||
class="p-0 text-red-500"
|
||
@click="onDeleteRecord(student.id)"
|
||
>
|
||
<Icon
|
||
name="tabler:trash"
|
||
size="16px"
|
||
/>
|
||
移出
|
||
</Button>
|
||
</TableCell>
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
<TableEmpty
|
||
v-else
|
||
class="flex justify-center items-center"
|
||
>
|
||
<p class="text-sm text-muted-foreground">
|
||
该班级暂无成员
|
||
</p>
|
||
</TableEmpty>
|
||
</div>
|
||
</SheetContent>
|
||
</Sheet>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped></style>
|