- Expanded CourseResourceType to include "resource" and "temp". - Renamed ICourseResource to IResource and updated its properties for consistency. - Introduced ICreateResource type for resource creation. - Modified ICourseSection and ICourseChapter interfaces to use the new IResource type and updated property names for camelCase. - Implemented uploadFile function in file API for handling file uploads. - Created ResourceUploader component for uploading resources with validation and feedback. - Developed Card component for displaying course class details and managing student enrollment. - Added AlertDialog components for consistent alert dialog UI. - Enhanced table components for better data presentation and management. - Implemented preview page for displaying various resource types based on file extension.
293 lines
9.0 KiB
Vue
293 lines
9.0 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>
|