feat: 添加教师团队管理功能,包括搜索、添加和删除教师团队成员
This commit is contained in:
parent
b05f954923
commit
9a36188322
@ -19,6 +19,8 @@ export interface ITeacher {
|
||||
employeeId: string;
|
||||
schoolId: number;
|
||||
collegeId: number;
|
||||
schoolName: string;
|
||||
collegeName: string;
|
||||
sex: number;
|
||||
email: string;
|
||||
phonenumber: string;
|
||||
@ -143,6 +145,22 @@ export const deleteCourseResource = async (resourceId: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const addTeacherToCourse = async (params: {
|
||||
courseId: number;
|
||||
teacherId: number;
|
||||
}) => {
|
||||
return await http<IResponse>(`/system/teacherteam`, {
|
||||
method: "POST",
|
||||
body: params,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTeacherTeamRecord = async (recordId: number) => {
|
||||
return await http<IResponse>(`/system/teacherteam/${recordId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
};
|
||||
|
||||
export const getTeacherTeamByCourse = async (courseId: number) => {
|
||||
return await http<
|
||||
IResponse<{
|
||||
|
14
api/user.ts
14
api/user.ts
@ -35,3 +35,17 @@ export const userProfile = async () => {
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const userSearch = async (pararms: {
|
||||
searchType: "student" | "teacher";
|
||||
keyword: string;
|
||||
}) => {
|
||||
return await http<
|
||||
IResponse & {
|
||||
data: IUser[];
|
||||
}
|
||||
>(`/system/user/search`, {
|
||||
method: "GET",
|
||||
query: pararms,
|
||||
});
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [teacherId: number];
|
||||
delete: [recordId: number];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -17,11 +17,12 @@ const emit = defineEmits<{
|
||||
class="absolute top-0 right-0 flex flex-col gap-1 invisible group-hover/member-card:visible"
|
||||
>
|
||||
<Button
|
||||
v-if="!isCurrentUser"
|
||||
variant="link"
|
||||
size="icon"
|
||||
@click.stop="emit('delete', member.teacherId)"
|
||||
@click="emit('delete', member.id)"
|
||||
>
|
||||
<Icon name="tabler:trash" size="16px" class="text-red-500" />
|
||||
<Icon name="tabler:logout" size="20px" class="text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -46,10 +47,10 @@ const emit = defineEmits<{
|
||||
工号:{{ member.teacher.employeeId || "未知" }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground/80">
|
||||
{{ member.teacher.schoolId || "未知学校" }}
|
||||
{{ member.teacher.schoolName || "未知学校" }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground/80">
|
||||
{{ member.teacher.collegeId || "未知学院" }}
|
||||
{{ member.teacher.collegeName || "未知学院" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@ export default defineNuxtConfig({
|
||||
"pinia-plugin-persistedstate",
|
||||
"dayjs-nuxt",
|
||||
"@formkit/auto-animate",
|
||||
"@vueuse/nuxt",
|
||||
],
|
||||
|
||||
icon: {
|
||||
|
@ -41,6 +41,7 @@
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/tailwindcss": "^6.13.2",
|
||||
"@pinia/nuxt": "^0.10.1",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs-nuxt": "^2.1.11",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
|
@ -1,7 +1,43 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { getCourseDetail } from "~/api/course";
|
||||
|
||||
definePageMeta({
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const {
|
||||
params: { id: courseId },
|
||||
} = useRoute();
|
||||
|
||||
// const loginState = useLoginState();
|
||||
const course = await getCourseDetail(courseId as string);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Classes</div>
|
||||
<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">
|
||||
<Button variant="secondary" size="sm" class="flex items-center gap-1">
|
||||
<Icon name="tabler:plus" size="16px" />
|
||||
<span>创建班级</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="false"></div>
|
||||
<EmptyScreen
|
||||
v-else
|
||||
title="暂无班级"
|
||||
description="课程下没有班级,请创建新的班级"
|
||||
icon="fluent-color:people-list-24"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,21 +1,99 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCourseDetail, getTeacherTeamByCourse } from "~/api/course";
|
||||
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 loginState = useLoginState();
|
||||
|
||||
const {
|
||||
params: { id: courseId },
|
||||
} = useRoute();
|
||||
|
||||
const loginState = useLoginState();
|
||||
const course = await getCourseDetail(courseId as string);
|
||||
|
||||
const { data: teacherTeam } = useAsyncData(() =>
|
||||
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>
|
||||
@ -28,12 +106,106 @@ const { data: teacherTeam } = useAsyncData(() =>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="secondary" size="sm" class="flex items-center gap-1">
|
||||
<Icon name="tabler:plus" size="16px" />
|
||||
<span>添加教师</span>
|
||||
</Button>
|
||||
<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"
|
||||
@ -43,6 +215,7 @@ const { data: teacherTeam } = useAsyncData(() =>
|
||||
:key="member.teacherId"
|
||||
:is-current-user="loginState.user?.userId === member.teacherId"
|
||||
:member
|
||||
@delete="onDeleteTeacher"
|
||||
/>
|
||||
</div>
|
||||
<EmptyScreen
|
||||
@ -51,9 +224,6 @@ const { data: teacherTeam } = useAsyncData(() =>
|
||||
description="请添加教师作为团队成员"
|
||||
icon="fluent-color:people-list-24"
|
||||
/>
|
||||
<DevOnly>
|
||||
<pre>{{ teacherTeam }}</pre>
|
||||
</DevOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -93,6 +93,9 @@ importers:
|
||||
'@pinia/nuxt':
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
|
||||
'@vueuse/nuxt':
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@ -1412,6 +1415,12 @@ packages:
|
||||
'@vueuse/metadata@13.0.0':
|
||||
resolution: {integrity: sha512-TRNksqmvtvqsuHf7bbgH9OSXEV2b6+M3BSN4LR5oxWKykOFT9gV78+C2/0++Pq9KCp9KQ1OQDPvGlWNQpOb2Mw==}
|
||||
|
||||
'@vueuse/nuxt@13.0.0':
|
||||
resolution: {integrity: sha512-tVb57PW0aUGMHwvzp4uH2mo8ut3D/3c7DA936E4ValhQq2VMZMCMxaKGz1nE8etFC7p18fVypyzpe8o6CBAYFw==}
|
||||
peerDependencies:
|
||||
nuxt: ^3.0.0 || ^4.0.0-0
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/shared@12.8.2':
|
||||
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||
|
||||
@ -6162,6 +6171,17 @@ snapshots:
|
||||
|
||||
'@vueuse/metadata@13.0.0': {}
|
||||
|
||||
'@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/metadata': 13.0.0
|
||||
local-pkg: 1.1.1
|
||||
nuxt: 3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@vueuse/shared@12.8.2(typescript@5.8.2)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from "./user";
|
||||
export * from "./course";
|
||||
|
||||
export type { FetchError } from "ofetch";
|
||||
|
@ -1,6 +1,7 @@
|
||||
export type LoginType = "admin" | "teacher" | "student";
|
||||
|
||||
export interface IUser {
|
||||
id?: number;
|
||||
createBy: number;
|
||||
createTime: Date;
|
||||
updateBy: string;
|
||||
@ -8,6 +9,11 @@ export interface IUser {
|
||||
remark: string;
|
||||
userId: number;
|
||||
deptId: number;
|
||||
collegeName: string;
|
||||
schoolName: string;
|
||||
employeeId: string;
|
||||
schoolId: number;
|
||||
collegeId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
email: string;
|
||||
|
Loading…
Reference in New Issue
Block a user