IntelliClass_FE/components/course/class/Card.vue

322 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>