240 lines
7.0 KiB
Vue
240 lines
7.0 KiB
Vue
<script lang="ts" setup>
|
||
import { toast } from 'vue-sonner'
|
||
import { userSearch } from '~/api'
|
||
import {
|
||
addTeacherToCourse,
|
||
deleteTeacherTeamRecord,
|
||
getCourseDetail,
|
||
getTeacherTeamByCourse,
|
||
} from '~/api/course'
|
||
import type { FetchError } from '~/types'
|
||
|
||
definePageMeta({
|
||
requiresAuth: true,
|
||
})
|
||
|
||
const {
|
||
params: { id: courseId },
|
||
} = useRoute()
|
||
|
||
const loginState = useLoginState()
|
||
const course = await getCourseDetail(courseId as string)
|
||
|
||
const { data: teacherTeam, refresh: refreshTeacherTeam } = useAsyncData(() =>
|
||
getTeacherTeamByCourse(parseInt(courseId as string)),
|
||
)
|
||
|
||
const searchKeyword = ref('')
|
||
|
||
const {
|
||
data: searchResults,
|
||
refresh: refreshSearch,
|
||
clear: clearSearch,
|
||
} = useAsyncData(
|
||
() =>
|
||
userSearch({
|
||
searchType: 'teacher',
|
||
keyword: searchKeyword.value,
|
||
}),
|
||
{
|
||
immediate: false,
|
||
},
|
||
)
|
||
|
||
// watch searchKeyword and refresh search results, with debounce
|
||
const triggerSearch = useDebounceFn(() => {
|
||
if (searchKeyword.value.length > 0) {
|
||
refreshSearch()
|
||
} else {
|
||
clearSearch()
|
||
}
|
||
}, 500)
|
||
|
||
watch(searchKeyword, (newValue) => {
|
||
if (newValue.length > 0) {
|
||
triggerSearch()
|
||
}
|
||
})
|
||
|
||
const isInTeam = (userId: number) => {
|
||
return teacherTeam?.value?.data?.some((item) => item.teacherId === userId)
|
||
}
|
||
|
||
const onAddTeacherToCourse = (teacherId: number) => {
|
||
toast.promise(
|
||
addTeacherToCourse({
|
||
courseId: parseInt(courseId as string),
|
||
teacherId,
|
||
}),
|
||
{
|
||
loading: '正在添加教师...',
|
||
success: () => {
|
||
refreshTeacherTeam()
|
||
return '添加教师成功'
|
||
},
|
||
error: (error: FetchError) => {
|
||
if (error.statusCode === 409) {
|
||
return '该教师已在团队中'
|
||
}
|
||
return `添加教师失败:${error.message}`
|
||
},
|
||
},
|
||
)
|
||
}
|
||
|
||
const onDeleteTeacher = (recordId: number) => {
|
||
toast.promise(deleteTeacherTeamRecord(recordId), {
|
||
loading: '正在移出教师...',
|
||
success: () => {
|
||
refreshTeacherTeam()
|
||
return '移出教师成功'
|
||
},
|
||
error: (error: FetchError) => {
|
||
return `移出教师失败:${error.message}`
|
||
},
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col gap-4 px-4 py-2">
|
||
<div class="flex justify-between items-start">
|
||
<h1 class="text-xl font-medium">
|
||
教师团队管理
|
||
<span class="block text-sm text-muted-foreground">
|
||
课程负责人:{{ course.data.teacherName || '未知' }}
|
||
</span>
|
||
</h1>
|
||
<div class="flex items-center 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="'end'"
|
||
>
|
||
<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="isInTeam(user.id!)"
|
||
@click="onAddTeacherToCourse(user.id!)"
|
||
>
|
||
<Icon
|
||
v-if="!isInTeam(user.id!)"
|
||
name="tabler:plus"
|
||
size="16px"
|
||
/>
|
||
<span>
|
||
{{ isInTeam(user.id!) ? '已在团队' : '添加' }}
|
||
</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<EmptyScreen
|
||
v-else
|
||
title="没有搜索结果"
|
||
description="没有找到符合条件的教师"
|
||
icon="fluent-color:people-list-24"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="teacherTeam?.data && teacherTeam.data.length > 0"
|
||
class="grid gap-6 grid-cols-2 sm:grid-cols-3 2xl:grid-cols-5"
|
||
>
|
||
<CourseTeamMember
|
||
v-for="member in teacherTeam.data"
|
||
:key="member.teacherId"
|
||
:is-current-user="loginState.user?.userId === member.teacherId"
|
||
:member
|
||
@delete="onDeleteTeacher"
|
||
/>
|
||
</div>
|
||
<EmptyScreen
|
||
v-else
|
||
title="暂无团队成员"
|
||
description="请添加教师作为团队成员"
|
||
icon="fluent-color:people-list-24"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped></style>
|