refactor: enable eslint
This commit is contained in:
parent
1faa632965
commit
9e094896bc
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"eslint.useFlatConfig": true
|
"eslint.useFlatConfig": true,
|
||||||
|
"prettier.bracketSameLine": true,
|
||||||
|
"prettier.requireConfig": true,
|
||||||
|
"prettier.semi": false,
|
||||||
|
"prettier.singleAttributePerLine": true,
|
||||||
|
"prettier.singleQuote": true
|
||||||
}
|
}
|
328
api/course.ts
328
api/course.ts
@ -1,293 +1,293 @@
|
|||||||
|
import type { IResponse } from '.'
|
||||||
import type {
|
import type {
|
||||||
ICourse,
|
ICourse,
|
||||||
ICourseChapter,
|
ICourseChapter,
|
||||||
ICreateResource,
|
ICreateResource,
|
||||||
IResource,
|
IResource,
|
||||||
} from "~/types";
|
} from '~/types'
|
||||||
import type { IResponse } from ".";
|
|
||||||
|
|
||||||
export type IPerson<T> = {
|
export type IPerson<T> = {
|
||||||
id: number;
|
id: number
|
||||||
courseId: number;
|
courseId: number
|
||||||
createTime: Date;
|
createTime: Date
|
||||||
updateTime: Date;
|
updateTime: Date
|
||||||
createBy: number;
|
createBy: number
|
||||||
updateBy: number;
|
updateBy: number
|
||||||
remark: string | null;
|
remark: string | null
|
||||||
} & (T extends ITeacher
|
} & (T extends ITeacher
|
||||||
? { teacher: ITeacher; teacherId: number }
|
? { teacher: ITeacher, teacherId: number }
|
||||||
: T extends IStudent
|
: T extends IStudent
|
||||||
? { student: IStudent; studentId: number }
|
? { student: IStudent, studentId: number }
|
||||||
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
{});
|
{})
|
||||||
|
|
||||||
export interface ITeacher {
|
export interface ITeacher {
|
||||||
id: number;
|
id: number
|
||||||
userName: string;
|
userName: string
|
||||||
employeeId: string;
|
employeeId: string
|
||||||
schoolId: number;
|
schoolId: number
|
||||||
collegeId: number;
|
collegeId: number
|
||||||
schoolName: string;
|
schoolName: string
|
||||||
collegeName: string;
|
collegeName: string
|
||||||
sex: number;
|
sex: number
|
||||||
email: string;
|
email: string
|
||||||
phonenumber: string;
|
phonenumber: string
|
||||||
avatar: string;
|
avatar: string
|
||||||
status: number;
|
status: number
|
||||||
delFlag: number;
|
delFlag: number
|
||||||
loginIp: string;
|
loginIp: string
|
||||||
loginDate: Date;
|
loginDate: Date
|
||||||
createBy: number;
|
createBy: number
|
||||||
createTime: Date;
|
createTime: Date
|
||||||
updateBy: number;
|
updateBy: number
|
||||||
updateTime: Date;
|
updateTime: Date
|
||||||
remark: string | null;
|
remark: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStudent {
|
export interface IStudent {
|
||||||
id: number;
|
id: number
|
||||||
userName: string;
|
userName: string
|
||||||
studentId: string;
|
studentId: string
|
||||||
schoolId: number;
|
schoolId: number
|
||||||
collegeId: number;
|
collegeId: number
|
||||||
schoolName: string;
|
schoolName: string
|
||||||
collegeName: string;
|
collegeName: string
|
||||||
sex: number;
|
sex: number
|
||||||
email: string;
|
email: string
|
||||||
phonenumber: string;
|
phonenumber: string
|
||||||
avatar: null | string;
|
avatar: null | string
|
||||||
status: number;
|
status: number
|
||||||
delFlag: null;
|
delFlag: null
|
||||||
loginIp: null;
|
loginIp: null
|
||||||
loginDate: null;
|
loginDate: null
|
||||||
createBy: null;
|
createBy: null
|
||||||
createTime: null;
|
createTime: null
|
||||||
updateBy: null;
|
updateBy: null
|
||||||
updateTime: null;
|
updateTime: null
|
||||||
remark: null;
|
remark: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICourseClass {
|
export interface ICourseClass {
|
||||||
id: number;
|
id: number
|
||||||
courseId: number;
|
courseId: number
|
||||||
classId: number;
|
classId: number
|
||||||
className: string;
|
className: string
|
||||||
createBy: number;
|
createBy: number
|
||||||
createTime: Date;
|
createTime: Date
|
||||||
updateBy: number;
|
updateBy: number
|
||||||
updateTime: Date | null;
|
updateTime: Date | null
|
||||||
remark: string | null;
|
remark: string | null
|
||||||
notes?: string | null;
|
notes?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listCourses = async () => {
|
export const listCourses = async () => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
rows: ICourse[];
|
rows: ICourse[]
|
||||||
}>
|
}>
|
||||||
>("/system/manage/list", {
|
>('/system/manage/list', {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const listUserCourses = async (userId: number) => {
|
export const listUserCourses = async (userId: number) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
rows: ICourse[];
|
rows: ICourse[]
|
||||||
}>
|
}>
|
||||||
>(`/system/manage/leader/${userId}`, {
|
>(`/system/manage/leader/${userId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getCourseDetail = async (courseId: string) => {
|
export const getCourseDetail = async (courseId: string) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: ICourse;
|
data: ICourse
|
||||||
}>
|
}>
|
||||||
>(`/system/manage/${courseId}`, {
|
>(`/system/manage/${courseId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const createCourse = async (
|
export const createCourse = async (
|
||||||
params: Pick<
|
params: Pick<
|
||||||
ICourse,
|
ICourse,
|
||||||
| "courseName"
|
| 'courseName'
|
||||||
| "profile"
|
| 'profile'
|
||||||
| "schoolName"
|
| 'schoolName'
|
||||||
| "teacherName"
|
| 'teacherName'
|
||||||
| "semester"
|
| 'semester'
|
||||||
| "previewUrl"
|
| 'previewUrl'
|
||||||
>
|
>,
|
||||||
) => {
|
) => {
|
||||||
return await http<IResponse>(`/system/manage`, {
|
return await http<IResponse>(`/system/manage`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteCourse = async (courseId: number) => {
|
export const deleteCourse = async (courseId: number) => {
|
||||||
return await http<IResponse>(`/system/manage/${courseId}`, {
|
return await http<IResponse>(`/system/manage/${courseId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getCourseChatpers = async (courseId: number) => {
|
export const getCourseChatpers = async (courseId: number) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
total: number;
|
total: number
|
||||||
rows: ICourseChapter[];
|
rows: ICourseChapter[]
|
||||||
}>
|
}>
|
||||||
>(`/system/chapter/details/${courseId}`, {
|
>(`/system/chapter/details/${courseId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const createCourseChatper = async (params: {
|
export const createCourseChatper = async (params: {
|
||||||
courseId: number;
|
courseId: number
|
||||||
title: string;
|
title: string
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/chapter`, {
|
return await http<IResponse>(`/system/chapter`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteCourseChatper = async (chapterId: number) => {
|
export const deleteCourseChatper = async (chapterId: number) => {
|
||||||
return await http<IResponse>(`/system/chapter/${chapterId}`, {
|
return await http<IResponse>(`/system/chapter/${chapterId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const editCourseChapter = async (chapter: ICourseChapter) => {
|
export const editCourseChapter = async (chapter: ICourseChapter) => {
|
||||||
return await http<IResponse>(`/system/chapter`, {
|
return await http<IResponse>(`/system/chapter`, {
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
body: chapter,
|
body: chapter,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const createCourseSection = async (params: {
|
export const createCourseSection = async (params: {
|
||||||
chapterId: number;
|
chapterId: number
|
||||||
title: string;
|
title: string
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/section`, {
|
return await http<IResponse>(`/system/section`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteCourseSection = async (sectionId: number) => {
|
export const deleteCourseSection = async (sectionId: number) => {
|
||||||
return await http<IResponse>(`/system/section/${sectionId}`, {
|
return await http<IResponse>(`/system/section/${sectionId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const createResource = async (params: ICreateResource) => {
|
export const createResource = async (params: ICreateResource) => {
|
||||||
return await http<IResponse & { resourceId: number }>(`/system/resource`, {
|
return await http<IResponse & { resourceId: number }>(`/system/resource`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteResource = async (resourceId: number) => {
|
export const deleteResource = async (resourceId: number) => {
|
||||||
return await http<IResponse>(`/system/resource/${resourceId}`, {
|
return await http<IResponse>(`/system/resource/${resourceId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const editResource = async (resource: IResource) => {
|
export const editResource = async (resource: IResource) => {
|
||||||
return await http<IResponse>(`/system/resource`, {
|
return await http<IResponse>(`/system/resource`, {
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
body: resource,
|
body: resource,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const addResourceToSection = async (params: {
|
export const addResourceToSection = async (params: {
|
||||||
sectionId: number;
|
sectionId: number
|
||||||
resourceId: number;
|
resourceId: number
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/sectionResource`, {
|
return await http<IResponse>(`/system/sectionResource`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const addTeacherToCourse = async (params: {
|
export const addTeacherToCourse = async (params: {
|
||||||
courseId: number;
|
courseId: number
|
||||||
teacherId: number;
|
teacherId: number
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/teacherteam`, {
|
return await http<IResponse>(`/system/teacherteam`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteTeacherTeamRecord = async (recordId: number) => {
|
export const deleteTeacherTeamRecord = async (recordId: number) => {
|
||||||
return await http<IResponse>(`/system/teacherteam/${recordId}`, {
|
return await http<IResponse>(`/system/teacherteam/${recordId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getTeacherTeamByCourse = async (courseId: number) => {
|
export const getTeacherTeamByCourse = async (courseId: number) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: IPerson<ITeacher>[];
|
data: IPerson<ITeacher>[]
|
||||||
}>
|
}>
|
||||||
>(`/system/teacherteam/course/${courseId}`, {
|
>(`/system/teacherteam/course/${courseId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const createClass = async (params: {
|
export const createClass = async (params: {
|
||||||
className: string;
|
className: string
|
||||||
notes: string;
|
notes: string
|
||||||
courseId: number;
|
courseId: number
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/course/class`, {
|
return await http<IResponse>(`/system/course/class`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteClass = async (classId: number) => {
|
export const deleteClass = async (classId: number) => {
|
||||||
return await http<IResponse>(`/system/course/class/${classId}`, {
|
return await http<IResponse>(`/system/course/class/${classId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getClassListByCourse = async (courseId: number) => {
|
export const getClassListByCourse = async (courseId: number) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: ICourseClass[];
|
data: ICourseClass[]
|
||||||
}>
|
}>
|
||||||
>(`/system/course/class/${courseId}`, {
|
>(`/system/course/class/${courseId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getStudentListByClass = async (classId: number) => {
|
export const getStudentListByClass = async (classId: number) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: IPerson<IStudent>[];
|
data: IPerson<IStudent>[]
|
||||||
}>
|
}>
|
||||||
>(`/system/student/class/${classId}`, {
|
>(`/system/student/class/${classId}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const addStudentToClass = async (params: {
|
export const addStudentToClass = async (params: {
|
||||||
classId: number;
|
classId: number
|
||||||
studentId: number;
|
studentId: number
|
||||||
}) => {
|
}) => {
|
||||||
return await http<IResponse>(`/system/student`, {
|
return await http<IResponse>(`/system/student`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteStudentClassRecord = async (recordId: number) => {
|
export const deleteStudentClassRecord = async (recordId: number) => {
|
||||||
return await http<IResponse>(`/system/student/${recordId}`, {
|
return await http<IResponse>(`/system/student/${recordId}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
30
api/file.ts
30
api/file.ts
@ -1,36 +1,36 @@
|
|||||||
import type { IResponse } from ".";
|
import type { IResponse } from '.'
|
||||||
|
|
||||||
const putFile = (file: File, url: string): Promise<string> => {
|
const putFile = (file: File, url: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
$fetch(url, {
|
$fetch(url, {
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
body: file,
|
body: file,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": file.type,
|
'Content-Type': file.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
resolve(url.split("?")[0]);
|
resolve(url.split('?')[0])
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
reject(new Error("File upload failed"));
|
reject(new Error('File upload failed'))
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const uploadFile = async (file: File, type: "resource" | "temp") => {
|
export const uploadFile = async (file: File, type: 'resource' | 'temp') => {
|
||||||
const signedUrl = await http<IResponse<{ data: string }>>(
|
const signedUrl = await http<IResponse<{ data: string }>>(
|
||||||
`/common/oss/getSignUrl`,
|
`/common/oss/getSignUrl`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
query: {
|
query: {
|
||||||
fileName: encodeURI(file.name),
|
fileName: encodeURI(file.name),
|
||||||
fileType: type,
|
fileType: type,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
fileMime: file.type,
|
fileMime: file.type,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
const url = signedUrl.data;
|
const url = signedUrl.data
|
||||||
return await putFile(file, url);
|
return await putFile(file, url)
|
||||||
};
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export * from "./user";
|
export * from './user'
|
||||||
|
|
||||||
export type IResponse<T = object | undefined> = {
|
export type IResponse<T = object | undefined> = {
|
||||||
msg: string;
|
msg: string
|
||||||
code: number;
|
code: number
|
||||||
} & T;
|
} & T
|
||||||
|
50
api/user.ts
50
api/user.ts
@ -1,21 +1,21 @@
|
|||||||
import type { IUser, LoginType } from "~/types";
|
import type { IResponse } from '.'
|
||||||
import { http } from "~/utils/http";
|
import type { IUser, LoginType } from '~/types'
|
||||||
import type { IResponse } from ".";
|
import { http } from '~/utils/http'
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
account: string;
|
account: string
|
||||||
password: string;
|
password: string
|
||||||
loginType: LoginType;
|
loginType: LoginType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoginResponse = IResponse & {
|
export type LoginResponse = IResponse & {
|
||||||
loginType: LoginType;
|
loginType: LoginType
|
||||||
token: string;
|
token: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type UserProfileResponse = IResponse & {
|
export type UserProfileResponse = IResponse & {
|
||||||
user: IUser;
|
user: IUser
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户登录
|
* 用户登录
|
||||||
@ -24,28 +24,28 @@ export type UserProfileResponse = IResponse & {
|
|||||||
* @see {@link LoginParams}
|
* @see {@link LoginParams}
|
||||||
*/
|
*/
|
||||||
export const userLogin = async (params: LoginParams) => {
|
export const userLogin = async (params: LoginParams) => {
|
||||||
return await http<LoginResponse>("/login", {
|
return await http<LoginResponse>('/login', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const userProfile = async () => {
|
export const userProfile = async () => {
|
||||||
return await http<UserProfileResponse>("/getInfo", {
|
return await http<UserProfileResponse>('/getInfo', {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const userSearch = async (pararms: {
|
export const userSearch = async (pararms: {
|
||||||
searchType: "student" | "teacher";
|
searchType: 'student' | 'teacher'
|
||||||
keyword: string;
|
keyword: string
|
||||||
}) => {
|
}) => {
|
||||||
return await http<
|
return await http<
|
||||||
IResponse & {
|
IResponse & {
|
||||||
data: IUser[];
|
data: IUser[]
|
||||||
}
|
}
|
||||||
>(`/system/user/search`, {
|
>(`/system/user/search`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
query: pararms,
|
query: pararms,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
32
app.vue
32
app.vue
@ -1,25 +1,25 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { toast } from "vue-sonner";
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const onLoginExpired = () => {
|
const onLoginExpired = () => {
|
||||||
toast.error("登录过期,请重新登录");
|
toast.error('登录过期,请重新登录')
|
||||||
router.replace("/user/authenticate");
|
router.replace('/user/authenticate')
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => loginState.isLoggedIn,
|
() => loginState.isLoggedIn,
|
||||||
(isLoggedIn) => {
|
(isLoggedIn) => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
toast.info("账号已退出,请重新登录");
|
toast.info('账号已退出,请重新登录')
|
||||||
router.replace("/user/authenticate");
|
router.replace('/user/authenticate')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (route.meta.requiresAuth && loginState.isLoggedIn) {
|
if (route.meta.requiresAuth && loginState.isLoggedIn) {
|
||||||
@ -27,14 +27,14 @@ onBeforeMount(() => {
|
|||||||
.checkLogin()
|
.checkLogin()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
onLoginExpired();
|
onLoginExpired()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
onLoginExpired();
|
onLoginExpired()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ICourse } from "~/types";
|
import type { ICourse } from '~/types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
data: ICourse;
|
data: ICourse
|
||||||
deleteMode?: boolean;
|
deleteMode?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"delete-course": [courseId: number];
|
'delete-course': [courseId: number]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const openCourse = (id: number) => {
|
const openCourse = (id: number) => {
|
||||||
window.open(`/course/${id}`, "_blank", "noopener,noreferrer");
|
window.open(`/course/${id}`, '_blank', 'noopener,noreferrer')
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -24,7 +24,11 @@ const openCourse = (id: number) => {
|
|||||||
variant="link"
|
variant="link"
|
||||||
@click="emit('delete-course', data.id)"
|
@click="emit('delete-course', data.id)"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:trash" size="16px" class="text-red-500 text-lg" />
|
<Icon
|
||||||
|
name="tabler:trash"
|
||||||
|
size="16px"
|
||||||
|
class="text-red-500 text-lg"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
@ -41,7 +45,10 @@ const openCourse = (id: number) => {
|
|||||||
<p
|
<p
|
||||||
class="text-xs text-muted-foreground font-medium flex items-center gap-0.5"
|
class="text-xs text-muted-foreground font-medium flex items-center gap-0.5"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:user" size="14px" />
|
<Icon
|
||||||
|
name="tabler:user"
|
||||||
|
size="14px"
|
||||||
|
/>
|
||||||
<span>{{ data.teacherName || "未知教师" }}</span>
|
<span>{{ data.teacherName || "未知教师" }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +68,10 @@ const openCourse = (id: number) => {
|
|||||||
v-if="data.status === 0"
|
v-if="data.status === 0"
|
||||||
class="w-2 h-2 rounded-full bg-emerald-400"
|
class="w-2 h-2 rounded-full bg-emerald-400"
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-2 h-2 rounded-full bg-gray-400" />
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-2 h-2 rounded-full bg-gray-400"
|
||||||
|
/>
|
||||||
<p class="text-xs text-muted-foreground/80">
|
<p class="text-xs text-muted-foreground/80">
|
||||||
{{ data.status === 0 ? "开课" : "关课" }}
|
{{ data.status === 0 ? "开课" : "关课" }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{
|
defineProps<{
|
||||||
icon?: string;
|
icon?: string
|
||||||
title?: string;
|
title?: string
|
||||||
description?: string;
|
description?: string
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="py-12 flex flex-col items-center justify-center gap-6 rounded-md bg-muted text-muted-foreground"
|
class="py-12 flex flex-col items-center justify-center gap-6 rounded-md bg-muted text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Icon v-if="icon" :name="icon" size="48px" />
|
<Icon
|
||||||
|
v-if="icon"
|
||||||
|
:name="icon"
|
||||||
|
size="48px"
|
||||||
|
/>
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<p class="text-base">{{ title }}</p>
|
<p class="text-base">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
<slot>
|
<slot>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
{{ description || "没有数据" }}
|
{{ description || "没有数据" }}
|
||||||
|
@ -1,51 +1,51 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { createResource } from "~/api/course";
|
import { createResource } from '~/api/course'
|
||||||
import { uploadFile } from "~/api/file";
|
import { uploadFile } from '~/api/file'
|
||||||
import type { FetchError, IResource } from "~/types";
|
import type { FetchError, IResource } from '~/types'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean
|
||||||
accept?: string;
|
accept?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
accept: ".docx, .pptx, .pdf, .png, .jpg, .mp4, .mp3",
|
accept: '.docx, .pptx, .pdf, .png, .jpg, .mp4, .mp3',
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [isOpen: boolean];
|
'update:modelValue': [isOpen: boolean]
|
||||||
"on-create": [resource: IResource];
|
'on-create': [resource: IResource]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isDialogOpen = computed({
|
const isDialogOpen = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value: boolean) => emit("update:modelValue", value),
|
set: (value: boolean) => emit('update:modelValue', value),
|
||||||
});
|
})
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
|
|
||||||
// const isDialogOpen = ref(false);
|
// const isDialogOpen = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false)
|
||||||
|
|
||||||
const selectedFile = ref<File | null>(null);
|
const selectedFile = ref<File | null>(null)
|
||||||
|
|
||||||
const onUpload = async () => {
|
const onUpload = async () => {
|
||||||
if (!selectedFile.value) {
|
if (!selectedFile.value) {
|
||||||
toast.error("请先选择文件", { id: "file-upload-error-no-file-selected" });
|
toast.error('请先选择文件', { id: 'file-upload-error-no-file-selected' })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true
|
||||||
|
|
||||||
toast.promise(
|
toast.promise(
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
uploadFile(selectedFile.value!, "resource")
|
uploadFile(selectedFile.value!, 'resource')
|
||||||
.then((url) => {
|
.then((url) => {
|
||||||
createResource({
|
createResource({
|
||||||
resourceName: selectedFile.value!.name,
|
resourceName: selectedFile.value!.name,
|
||||||
resourceSize: selectedFile.value!.size,
|
resourceSize: selectedFile.value!.size,
|
||||||
resourceType: "resource",
|
resourceType: 'resource',
|
||||||
resourceUrl: url,
|
resourceUrl: url,
|
||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
isRepo: false,
|
isRepo: false,
|
||||||
@ -53,44 +53,45 @@ const onUpload = async () => {
|
|||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.code !== 200) {
|
if (result.code !== 200) {
|
||||||
reject(new Error(result.msg || "文件上传失败"));
|
reject(new Error(result.msg || '文件上传失败'))
|
||||||
} else {
|
}
|
||||||
emit("on-create", {
|
else {
|
||||||
|
emit('on-create', {
|
||||||
id: result.resourceId,
|
id: result.resourceId,
|
||||||
resourceName: selectedFile.value!.name,
|
resourceName: selectedFile.value!.name,
|
||||||
resourceSize: selectedFile.value!.size,
|
resourceSize: selectedFile.value!.size,
|
||||||
resourceType: "resource",
|
resourceType: 'resource',
|
||||||
resourceUrl: url,
|
resourceUrl: url,
|
||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
isRepo: false,
|
isRepo: false,
|
||||||
ownerId: loginState.user.userId,
|
ownerId: loginState.user.userId,
|
||||||
});
|
})
|
||||||
resolve("文件上传成功");
|
resolve('文件上传成功')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error);
|
reject(error)
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error);
|
reject(error)
|
||||||
});
|
})
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "正在上传文件...",
|
loading: '正在上传文件...',
|
||||||
success: () => {
|
success: () => {
|
||||||
isDialogOpen.value = false;
|
isDialogOpen.value = false
|
||||||
return "文件上传成功";
|
return '文件上传成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
return error.message || "文件上传失败,请稍后重试";
|
return error.message || '文件上传失败,请稍后重试'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
loading.value = false;
|
loading.value = false
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -120,7 +121,8 @@ const onUpload = async () => {
|
|||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
selectedFile = files[0];
|
selectedFile = files[0];
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
selectedFile = null;
|
selectedFile = null;
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@ -129,7 +131,10 @@ const onUpload = async () => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<input type="hidden" name="courseId" />
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="courseId"
|
||||||
|
/>
|
||||||
<div class="text-xs text-muted-foreground space-y-2">
|
<div class="text-xs text-muted-foreground space-y-2">
|
||||||
<p>
|
<p>
|
||||||
根据国家《出版管理条例》《网络出版服务管理规定》及教育部《职业教育专业教学资源库建设工作手册》等相关规定,上传的资源必须符合以下要求:
|
根据国家《出版管理条例》《网络出版服务管理规定》及教育部《职业教育专业教学资源库建设工作手册》等相关规定,上传的资源必须符合以下要求:
|
||||||
@ -155,7 +160,10 @@ const onUpload = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button :disabled="loading" @click="onUpload">
|
<Button
|
||||||
|
:disabled="loading"
|
||||||
|
@click="onUpload"
|
||||||
|
>
|
||||||
{{ loading ? "上传中..." : "上传" }}
|
{{ loading ? "上传中..." : "上传" }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
@ -1,27 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-vue-next";
|
import { ChevronRight, type LucideIcon } from 'lucide-vue-next'
|
||||||
import type { RouteLocationRaw } from "vue-router";
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
nav: {
|
nav: {
|
||||||
label?: string;
|
label?: string
|
||||||
items: {
|
items: {
|
||||||
title: string;
|
title: string
|
||||||
url?: RouteLocationRaw | string;
|
url?: RouteLocationRaw | string
|
||||||
icon: LucideIcon | string;
|
icon: LucideIcon | string
|
||||||
isActive?: boolean;
|
isActive?: boolean
|
||||||
items?: {
|
items?: {
|
||||||
title: string;
|
title: string
|
||||||
url: string;
|
url: string
|
||||||
}[];
|
}[]
|
||||||
}[];
|
}[]
|
||||||
}[];
|
}[]
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarGroup v-for="group in nav" :key="group.label">
|
<SidebarGroup
|
||||||
<SidebarGroupLabel v-if="group.label">{{ group.label }}</SidebarGroupLabel>
|
v-for="group in nav"
|
||||||
|
:key="group.label"
|
||||||
|
>
|
||||||
|
<SidebarGroupLabel v-if="group.label">
|
||||||
|
{{ group.label }}
|
||||||
|
</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
v-for="item in group.items"
|
v-for="item in group.items"
|
||||||
@ -55,7 +60,11 @@ defineProps<{
|
|||||||
class="!size-6"
|
class="!size-6"
|
||||||
/>
|
/>
|
||||||
<!-- 图标组件 -->
|
<!-- 图标组件 -->
|
||||||
<component :is="item.icon" v-else class="!size-6" />
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
v-else
|
||||||
|
class="!size-6"
|
||||||
|
/>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
<!-- 有子项目 -->
|
<!-- 有子项目 -->
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
@ -65,7 +74,10 @@ defineProps<{
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<!-- 无跳转链接 -->
|
<!-- 无跳转链接 -->
|
||||||
<SidebarMenuButton v-else :tooltip="item.title">
|
<SidebarMenuButton
|
||||||
|
v-else
|
||||||
|
:tooltip="item.title"
|
||||||
|
>
|
||||||
<!-- 图标名 -->
|
<!-- 图标名 -->
|
||||||
<Icon
|
<Icon
|
||||||
v-if="item.icon && typeof item.icon === 'string'"
|
v-if="item.icon && typeof item.icon === 'string'"
|
||||||
@ -73,7 +85,10 @@ defineProps<{
|
|||||||
size="16px"
|
size="16px"
|
||||||
/>
|
/>
|
||||||
<!-- 图标组件 -->
|
<!-- 图标组件 -->
|
||||||
<component :is="item.icon" v-else />
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
v-else
|
||||||
|
/>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
v-if="item.items"
|
v-if="item.items"
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronsUpDown } from "lucide-vue-next";
|
import { ChevronsUpDown } from 'lucide-vue-next'
|
||||||
import { useSidebar } from "../ui/sidebar";
|
import { useSidebar } from '../ui/sidebar'
|
||||||
import type { IUser } from "~/types";
|
import type { IUser } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: IUser;
|
user: IUser
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar()
|
||||||
const { logout } = useLoginState();
|
const { logout } = useLoginState()
|
||||||
|
|
||||||
const displayName = computed(() => {
|
const displayName = computed(() => {
|
||||||
return props.user?.nickName || props.user?.userName;
|
return props.user?.nickName || props.user?.userName
|
||||||
});
|
})
|
||||||
|
|
||||||
const compactUserLabel = computed(() => {
|
const compactUserLabel = computed(() => {
|
||||||
const name = displayName.value;
|
const name = displayName.value
|
||||||
if (name?.length > 2) {
|
if (name?.length > 2) {
|
||||||
return name.slice(0, 2);
|
return name.slice(0, 2)
|
||||||
}
|
}
|
||||||
return name || "User";
|
return name || 'User'
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
@ -32,7 +33,10 @@ const compactUserLabel = computed(() => {
|
|||||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar class="h-8 w-8 rounded-lg">
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage :src="user.avatar || ''" :alt="user.userName" />
|
<AvatarImage
|
||||||
|
:src="user.avatar || ''"
|
||||||
|
:alt="user.userName"
|
||||||
|
/>
|
||||||
<AvatarFallback class="rounded-lg">
|
<AvatarFallback class="rounded-lg">
|
||||||
{{ compactUserLabel }}
|
{{ compactUserLabel }}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@ -55,7 +59,10 @@ const compactUserLabel = computed(() => {
|
|||||||
<DropdownMenuLabel class="p-0 font-normal">
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<Avatar class="h-8 w-8 rounded-lg">
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage :src="user.avatar || ''" :alt="user.userName" />
|
<AvatarImage
|
||||||
|
:src="user.avatar || ''"
|
||||||
|
:alt="user.userName"
|
||||||
|
/>
|
||||||
<AvatarFallback class="rounded-lg">
|
<AvatarFallback class="rounded-lg">
|
||||||
{{ compactUserLabel }}
|
{{ compactUserLabel }}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@ -69,7 +76,10 @@ const compactUserLabel = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem class="text-red-500" @click="logout">
|
<DropdownMenuItem
|
||||||
|
class="text-red-500"
|
||||||
|
@click="logout"
|
||||||
|
>
|
||||||
<Icon name="tabler:logout" />
|
<Icon name="tabler:logout" />
|
||||||
退出账号
|
退出账号
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SidebarNavGroup } from "./Sidebar.vue";
|
import type { SidebarNavGroup } from './Sidebar.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sidebarNav: SidebarNavGroup[];
|
sidebarNav: SidebarNavGroup[]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
props,
|
props,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider style="--sidebar-width: 200px">
|
<SidebarProvider style="--sidebar-width: 200px">
|
||||||
<slot name="sidebar" :sidebar-nav="sidebarNav">
|
<slot
|
||||||
|
name="sidebar"
|
||||||
|
:sidebar-nav="sidebarNav"
|
||||||
|
>
|
||||||
<AppSidebar :nav="sidebarNav" />
|
<AppSidebar :nav="sidebarNav" />
|
||||||
</slot>
|
</slot>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { LucideIcon } from "lucide-vue-next";
|
import type { LucideIcon } from 'lucide-vue-next'
|
||||||
import type { SidebarProps } from "../ui/sidebar";
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import type { RouteLocationRaw } from "vue-router";
|
import type { SidebarProps } from '../ui/sidebar'
|
||||||
|
|
||||||
export interface SidebarNavItem {
|
export interface SidebarNavItem {
|
||||||
title: string;
|
title: string
|
||||||
url?: string | RouteLocationRaw;
|
url?: string | RouteLocationRaw
|
||||||
icon: LucideIcon | string;
|
icon: LucideIcon | string
|
||||||
isActive?: boolean;
|
isActive?: boolean
|
||||||
items?: {
|
items?: {
|
||||||
title: string;
|
title: string
|
||||||
url: string;
|
url: string
|
||||||
}[];
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarNavGroup {
|
export interface SidebarNavGroup {
|
||||||
label?: string;
|
label?: string
|
||||||
items: SidebarNavItem[];
|
items: SidebarNavItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<
|
defineProps<
|
||||||
SidebarProps & {
|
SidebarProps & {
|
||||||
nav: SidebarNavGroup[];
|
nav: SidebarNavGroup[]
|
||||||
}
|
}
|
||||||
>(),
|
>(),
|
||||||
{
|
{
|
||||||
collapsible: "offcanvas",
|
collapsible: 'offcanvas',
|
||||||
variant: "sidebar",
|
variant: 'sidebar',
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -45,7 +45,9 @@ const loginState = useLoginState();
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
class="w-9 max-w-9 aspect-square group-has-[[data-collapsible=icon]]/sidebar-wrapper:w-full transition-all duration-200 ease-in-out"
|
class="w-9 max-w-9 aspect-square group-has-[[data-collapsible=icon]]/sidebar-wrapper:w-full transition-all duration-200 ease-in-out"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-lg font-medium">智课教学平台</h1>
|
<h1 class="text-lg font-medium">
|
||||||
|
智课教学平台
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
import { navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
hideTrigger: {
|
hideTrigger: {
|
||||||
@ -10,40 +10,40 @@ defineProps({
|
|||||||
type: Array as () => TopbarNavItem[],
|
type: Array as () => TopbarNavItem[],
|
||||||
default: () => topbarNavDefaults,
|
default: () => topbarNavDefaults,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const colorMode = useColorMode();
|
const colorMode = useColorMode()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export interface TopbarNavItem {
|
export interface TopbarNavItem {
|
||||||
title: string;
|
title: string
|
||||||
to: string;
|
to: string
|
||||||
icon?: string;
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const topbarNavDefaults = [
|
export const topbarNavDefaults = [
|
||||||
{
|
{
|
||||||
title: "课程中心",
|
title: '课程中心',
|
||||||
to: "/course",
|
to: '/course',
|
||||||
icon: "tabler:home",
|
icon: 'tabler:home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 备课",
|
title: 'AI 备课',
|
||||||
to: "/course/prepare",
|
to: '/course/prepare',
|
||||||
icon: "tabler:clipboard-list",
|
icon: 'tabler:clipboard-list',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 教科研",
|
title: 'AI 教科研',
|
||||||
to: "/course/research",
|
to: '/course/research',
|
||||||
icon: "tabler:report-search",
|
icon: 'tabler:report-search',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "课程资源库",
|
title: '课程资源库',
|
||||||
to: "/course/resources",
|
to: '/course/resources',
|
||||||
icon: "tabler:books",
|
icon: 'tabler:books',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -52,12 +52,18 @@ export const topbarNavDefaults = [
|
|||||||
>
|
>
|
||||||
<!-- group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 -->
|
<!-- group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 -->
|
||||||
<div class="flex items-center gap-2 px-4 w-full">
|
<div class="flex items-center gap-2 px-4 w-full">
|
||||||
<SidebarTrigger v-if="!hideTrigger" class="-ml-1" />
|
<SidebarTrigger
|
||||||
|
v-if="!hideTrigger"
|
||||||
|
class="-ml-1"
|
||||||
|
/>
|
||||||
<slot name="title-area" />
|
<slot name="title-area" />
|
||||||
<div class="flex-1 flex justify-center">
|
<div class="flex-1 flex justify-center">
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
<NavigationMenuItem v-for="item in nav" :key="item.title">
|
<NavigationMenuItem
|
||||||
|
v-for="item in nav"
|
||||||
|
:key="item.title"
|
||||||
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-slot="{ isActive, href, navigate }"
|
v-slot="{ isActive, href, navigate }"
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
@ -84,7 +90,10 @@ export const topbarNavDefaults = [
|
|||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost" size="icon">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name="tabler:moon"
|
name="tabler:moon"
|
||||||
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||||
|
@ -1,65 +1,65 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { ChevronLeft } from "lucide-vue-next";
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
import { useForm } from "vee-validate";
|
import { useForm } from 'vee-validate'
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
import { createCourseSection, editCourseChapter } from "~/api/course";
|
import { createCourseSection, editCourseChapter } from '~/api/course'
|
||||||
import type { ICourseChapter } from "~/types";
|
import type { ICourseChapter } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tag?: string;
|
tag?: string
|
||||||
chapter: ICourseChapter;
|
chapter: ICourseChapter
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
refresh: [];
|
'refresh': []
|
||||||
"delete-chapter": [chapterId: number];
|
'delete-chapter': [chapterId: number]
|
||||||
"delete-section": [sectionId: number];
|
'delete-section': [sectionId: number]
|
||||||
"delete-resource": [resourceId: number];
|
'delete-resource': [resourceId: number]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const createSectionDialogOpen = ref(false);
|
const createSectionDialogOpen = ref(false)
|
||||||
|
|
||||||
const createSectionSchema = toTypedSchema(
|
const createSectionSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().min(2, "小节名称至少2个字符").max(32, "最大长度32个字符"),
|
title: z.string().min(2, '小节名称至少2个字符').max(32, '最大长度32个字符'),
|
||||||
chapterId: z.number().min(1, "章节ID不能为空"),
|
chapterId: z.number().min(1, '章节ID不能为空'),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const createSectionForm = useForm({
|
const createSectionForm = useForm({
|
||||||
validationSchema: createSectionSchema,
|
validationSchema: createSectionSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
title: "",
|
title: '',
|
||||||
chapterId: props.chapter.id,
|
chapterId: props.chapter.id,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const onCreateSectionSubmit = createSectionForm.handleSubmit((values) => {
|
const onCreateSectionSubmit = createSectionForm.handleSubmit((values) => {
|
||||||
toast.promise(createCourseSection(values), {
|
toast.promise(createCourseSection(values), {
|
||||||
loading: "正在创建小节...",
|
loading: '正在创建小节...',
|
||||||
success: () => {
|
success: () => {
|
||||||
createSectionForm.resetForm();
|
createSectionForm.resetForm()
|
||||||
createSectionDialogOpen.value = false;
|
createSectionDialogOpen.value = false
|
||||||
emit("refresh");
|
emit('refresh')
|
||||||
return "创建小节成功";
|
return '创建小节成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "创建小节失败";
|
return '创建小节失败'
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleDeleteChapter = () => {
|
const handleDeleteChapter = () => {
|
||||||
if (props.chapter.sections.length > 0) {
|
if (props.chapter.sections.length > 0) {
|
||||||
const confirmDelete = confirm(
|
const confirmDelete = confirm(
|
||||||
"该章节下有小节,删除后将无法恢复,是否继续?"
|
'该章节下有小节,删除后将无法恢复,是否继续?',
|
||||||
);
|
)
|
||||||
if (!confirmDelete) return;
|
if (!confirmDelete) return
|
||||||
}
|
}
|
||||||
emit("delete-chapter", props.chapter.id);
|
emit('delete-chapter', props.chapter.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onIsPublishedSwitch = () => {
|
const onIsPublishedSwitch = () => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -68,19 +68,19 @@ const onIsPublishedSwitch = () => {
|
|||||||
isPublished: !props.chapter.isPublished,
|
isPublished: !props.chapter.isPublished,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "正在修改章节发布状态...",
|
loading: '正在修改章节发布状态...',
|
||||||
success: () => {
|
success: () => {
|
||||||
return `已${props.chapter.isPublished ? "取消" : ""}发布章节`;
|
return `已${props.chapter.isPublished ? '取消' : ''}发布章节`
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "修改章节发布状态失败";
|
return '修改章节发布状态失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
emit("refresh");
|
emit('refresh')
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -89,13 +89,19 @@ const onIsPublishedSwitch = () => {
|
|||||||
v-if="chapter.sections.length > 0"
|
v-if="chapter.sections.length > 0"
|
||||||
class="absolute inset-y-0 left-9 bottom-6 w-[1px] bg-gray-300 dark:bg-gray-700 z-0"
|
class="absolute inset-y-0 left-9 bottom-6 w-[1px] bg-gray-300 dark:bg-gray-700 z-0"
|
||||||
/>
|
/>
|
||||||
<Collapsible class="group/collapsible z-10" :default-open="true">
|
<Collapsible
|
||||||
|
class="group/collapsible z-10"
|
||||||
|
:default-open="true"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full px-4 py-3 rounded-md bg-indigo-50 dark:bg-muted flex justify-between items-center"
|
class="w-full px-4 py-3 rounded-md bg-indigo-50 dark:bg-muted flex justify-between items-center"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center">
|
||||||
<Badge variant="secondary" class="text-xs text-white bg-indigo-400">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs text-white bg-indigo-400"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
tag || chapter.sections.length > 0
|
tag || chapter.sections.length > 0
|
||||||
@ -142,7 +148,10 @@ const onIsPublishedSwitch = () => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
class="flex items-center gap-1 text-muted-foreground"
|
class="flex items-center gap-1 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:automation" size="16px" />
|
<Icon
|
||||||
|
name="tabler:automation"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>章节检测</span>
|
<span>章节检测</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -153,7 +162,10 @@ const onIsPublishedSwitch = () => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
class="flex items-center gap-1 text-muted-foreground"
|
class="flex items-center gap-1 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加小节</span>
|
<span>添加小节</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -168,7 +180,10 @@ const onIsPublishedSwitch = () => {
|
|||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
@submit="onCreateSectionSubmit"
|
@submit="onCreateSectionSubmit"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="title">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="title"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>小节名称</FormLabel>
|
<FormLabel>小节名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -181,11 +196,19 @@ const onIsPublishedSwitch = () => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<input type="hidden" name="chapterId" />
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="chapterId"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" form="create-section-form">创建</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="create-section-form"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -196,7 +219,10 @@ const onIsPublishedSwitch = () => {
|
|||||||
class="flex items-center gap-1 text-red-500"
|
class="flex items-center gap-1 text-red-500"
|
||||||
@click="handleDeleteChapter"
|
@click="handleDeleteChapter"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:trash" size="16px" />
|
<Icon
|
||||||
|
name="tabler:trash"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -210,7 +236,10 @@ const onIsPublishedSwitch = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CollapsibleContent class="pt-4">
|
<CollapsibleContent class="pt-4">
|
||||||
<div v-if="chapter.sections.length > 0" class="flex flex-col gap-4">
|
<div
|
||||||
|
v-if="chapter.sections.length > 0"
|
||||||
|
class="flex flex-col gap-4"
|
||||||
|
>
|
||||||
<!-- Section -->
|
<!-- Section -->
|
||||||
<CourseSection
|
<CourseSection
|
||||||
v-for="section in chapter.sections"
|
v-for="section in chapter.sections"
|
||||||
|
@ -1,45 +1,45 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { editResource } from "~/api/course";
|
import { editResource } from '~/api/course'
|
||||||
import type { IResource } from "~/types";
|
import type { IResource } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
resource: IResource;
|
resource: IResource
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
refresh: [];
|
'refresh': []
|
||||||
"delete-resource": [resourceId: number];
|
'delete-resource': [resourceId: number]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const resourceIcon = computed(() => {
|
const resourceIcon = computed(() => {
|
||||||
switch (props.resource.resourceName?.split(".").pop()) {
|
switch (props.resource.resourceName?.split('.').pop()) {
|
||||||
case "mp4":
|
case 'mp4':
|
||||||
case "avi":
|
case 'avi':
|
||||||
case "mov":
|
case 'mov':
|
||||||
return "tabler:video";
|
return 'tabler:video'
|
||||||
case "jpg":
|
case 'jpg':
|
||||||
case "jpeg":
|
case 'jpeg':
|
||||||
case "png":
|
case 'png':
|
||||||
case "gif":
|
case 'gif':
|
||||||
case "webp":
|
case 'webp':
|
||||||
return "tabler:photo";
|
return 'tabler:photo'
|
||||||
case "ppt":
|
case 'ppt':
|
||||||
case "pptx":
|
case 'pptx':
|
||||||
return "tabler:file-type-ppt";
|
return 'tabler:file-type-ppt'
|
||||||
case "doc":
|
case 'doc':
|
||||||
case "docx":
|
case 'docx':
|
||||||
case "txt":
|
case 'txt':
|
||||||
case "pdf":
|
case 'pdf':
|
||||||
case "xls":
|
case 'xls':
|
||||||
case "xlsx":
|
case 'xlsx':
|
||||||
case "csv":
|
case 'csv':
|
||||||
return "tabler:file-type-doc";
|
return 'tabler:file-type-doc'
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "tabler:file";
|
return 'tabler:file'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const onAllowDownloadSwitch = () => {
|
const onAllowDownloadSwitch = () => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -48,31 +48,31 @@ const onAllowDownloadSwitch = () => {
|
|||||||
allowDownload: !props.resource.allowDownload,
|
allowDownload: !props.resource.allowDownload,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "正在修改资源下载权限...",
|
loading: '正在修改资源下载权限...',
|
||||||
success: () => {
|
success: () => {
|
||||||
return `已${props.resource.allowDownload ? "禁止" : "允许"}下载资源`;
|
return `已${props.resource.allowDownload ? '禁止' : '允许'}下载资源`
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "修改资源下载权限失败";
|
return '修改资源下载权限失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
emit("refresh");
|
emit('refresh')
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteResource = () => {
|
const onDeleteResource = () => {
|
||||||
const confirmDelete = confirm(
|
const confirmDelete = confirm(
|
||||||
"将从课程中移除该资源,文件仍可在资源库中找到,是否继续?"
|
'将从课程中移除该资源,文件仍可在资源库中找到,是否继续?',
|
||||||
);
|
)
|
||||||
if (!confirmDelete) return;
|
if (!confirmDelete) return
|
||||||
emit("delete-resource", props.resource.id);
|
emit('delete-resource', props.resource.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onPreviewResource = (url: string) => {
|
const onPreviewResource = (url: string) => {
|
||||||
window.open(`/preview/${btoa(url)}`, "xmts_resource_preview");
|
window.open(`/preview/${btoa(url)}`, 'xmts_resource_preview')
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -84,7 +84,11 @@ const onPreviewResource = (url: string) => {
|
|||||||
class="absolute inset-y-0 top-3 left-1.5 w-4 h-[1px] bg-gray-300 dark:bg-gray-700 z-0"
|
class="absolute inset-y-0 top-3 left-1.5 w-4 h-[1px] bg-gray-300 dark:bg-gray-700 z-0"
|
||||||
/>
|
/>
|
||||||
<div class="w-[7px] h-[7px] rounded-full bg-foreground/50 z-10" />
|
<div class="w-[7px] h-[7px] rounded-full bg-foreground/50 z-10" />
|
||||||
<Icon :name="resourceIcon" class="ml-6" size="20px" />
|
<Icon
|
||||||
|
:name="resourceIcon"
|
||||||
|
class="ml-6"
|
||||||
|
size="20px"
|
||||||
|
/>
|
||||||
<span class="text-ellipsis line-clamp-1 text-xs font-medium">
|
<span class="text-ellipsis line-clamp-1 text-xs font-medium">
|
||||||
{{ resource.resourceName }}
|
{{ resource.resourceName }}
|
||||||
</span>
|
</span>
|
||||||
@ -98,7 +102,10 @@ const onPreviewResource = (url: string) => {
|
|||||||
class="flex items-center gap-1 text-muted-foreground"
|
class="flex items-center gap-1 text-muted-foreground"
|
||||||
@click="onPreviewResource(resource.resourceUrl)"
|
@click="onPreviewResource(resource.resourceUrl)"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:eye" size="16px" />
|
<Icon
|
||||||
|
name="tabler:eye"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>预览</span>
|
<span>预览</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -122,7 +129,7 @@ const onPreviewResource = (url: string) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<!-- <Tooltip :delay-duration="0">
|
<!-- <Tooltip :delay-duration="0">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{{ `当前${resource.allow_download ? "允许" : "禁止"}下载` }}
|
{{ `当前${resource.allow_download ? "允许" : "禁止"}下载` }}
|
||||||
@ -134,7 +141,10 @@ const onPreviewResource = (url: string) => {
|
|||||||
class="flex items-center gap-1 text-red-500"
|
class="flex items-center gap-1 text-red-500"
|
||||||
@click="onDeleteResource"
|
@click="onDeleteResource"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:trash" size="16px" />
|
<Icon
|
||||||
|
name="tabler:trash"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { addResourceToSection } from "~/api/course";
|
import { addResourceToSection } from '~/api/course'
|
||||||
import type { FetchError, ICourseSection, IResource } from "~/types";
|
import type { FetchError, ICourseSection, IResource } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tag?: string;
|
tag?: string
|
||||||
section: ICourseSection;
|
section: ICourseSection
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
refresh: [];
|
'refresh': []
|
||||||
"delete-section": [sectionId: number];
|
'delete-section': [sectionId: number]
|
||||||
"delete-resource": [resourceId: number];
|
'delete-resource': [resourceId: number]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isUploadOpen = ref(false);
|
const isUploadOpen = ref(false)
|
||||||
|
|
||||||
const handleDeleteSection = () => {
|
const handleDeleteSection = () => {
|
||||||
if (props.section.resources.length > 0) {
|
if (props.section.resources.length > 0) {
|
||||||
const confirmDelete = confirm(
|
const confirmDelete = confirm(
|
||||||
"该小节下有资源,删除后将无法恢复,是否继续?"
|
'该小节下有资源,删除后将无法恢复,是否继续?',
|
||||||
);
|
)
|
||||||
if (!confirmDelete) return;
|
if (!confirmDelete) return
|
||||||
}
|
}
|
||||||
emit("delete-section", props.section.id);
|
emit('delete-section', props.section.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onCreateResource = (resource: IResource) => {
|
const onCreateResource = (resource: IResource) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -33,20 +33,20 @@ const onCreateResource = (resource: IResource) => {
|
|||||||
resourceId: resource.id,
|
resourceId: resource.id,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "添加资源中...",
|
loading: '添加资源中...',
|
||||||
success: () => {
|
success: () => {
|
||||||
isUploadOpen.value = false;
|
isUploadOpen.value = false
|
||||||
return "添加资源成功";
|
return '添加资源成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
return `添加资源失败: ${error.message}`;
|
return `添加资源失败: ${error.message}`
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
emit("refresh");
|
emit('refresh')
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -56,7 +56,10 @@ const onCreateResource = (resource: IResource) => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center">
|
||||||
<Badge variant="outline" class="text-xs bg-background">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs bg-background"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
tag || section.resources.length > 0
|
tag || section.resources.length > 0
|
||||||
@ -80,7 +83,10 @@ const onCreateResource = (resource: IResource) => {
|
|||||||
class="flex items-center gap-1 text-muted-foreground"
|
class="flex items-center gap-1 text-muted-foreground"
|
||||||
@click="isUploadOpen = true"
|
@click="isUploadOpen = true"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加资源</span>
|
<span>添加资源</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -89,12 +95,18 @@ const onCreateResource = (resource: IResource) => {
|
|||||||
class="flex items-center gap-1 text-red-500"
|
class="flex items-center gap-1 text-red-500"
|
||||||
@click="handleDeleteSection"
|
@click="handleDeleteSection"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:trash" size="16px" />
|
<Icon
|
||||||
|
name="tabler:trash"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="section.resources.length > 0" class="flex flex-col gap-2 py-2">
|
<div
|
||||||
|
v-if="section.resources.length > 0"
|
||||||
|
class="flex flex-col gap-2 py-2"
|
||||||
|
>
|
||||||
<!-- Resource -->
|
<!-- Resource -->
|
||||||
<CourseResource
|
<CourseResource
|
||||||
v-for="resource in section.resources"
|
v-for="resource in section.resources"
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { userSearch } from "~/api";
|
import { userSearch } from '~/api'
|
||||||
import {
|
import {
|
||||||
addStudentToClass,
|
addStudentToClass,
|
||||||
deleteStudentClassRecord,
|
deleteStudentClassRecord,
|
||||||
getStudentListByClass,
|
getStudentListByClass,
|
||||||
type ICourseClass,
|
type ICourseClass,
|
||||||
} from "~/api/course";
|
} from '~/api/course'
|
||||||
import type { FetchError } from "~/types";
|
import type { FetchError } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
classItem: ICourseClass;
|
classItem: ICourseClass
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const { data: students, refresh: refreshStudents } = useAsyncData(
|
const { data: students, refresh: refreshStudents } = useAsyncData(
|
||||||
`students-${props.classItem.classId}`,
|
`students-${props.classItem.classId}`,
|
||||||
@ -20,18 +20,18 @@ const { data: students, refresh: refreshStudents } = useAsyncData(
|
|||||||
{
|
{
|
||||||
immediate: false,
|
immediate: false,
|
||||||
watch: [() => props.classItem.classId],
|
watch: [() => props.classItem.classId],
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const studentsSheetOpen = ref(false);
|
const studentsSheetOpen = ref(false)
|
||||||
|
|
||||||
watch(studentsSheetOpen, (isOpen) => {
|
watch(studentsSheetOpen, (isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
refreshStudents();
|
refreshStudents()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const searchKeyword = ref("");
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: searchResults,
|
data: searchResults,
|
||||||
@ -40,31 +40,32 @@ const {
|
|||||||
} = useAsyncData(
|
} = useAsyncData(
|
||||||
() =>
|
() =>
|
||||||
userSearch({
|
userSearch({
|
||||||
searchType: "student",
|
searchType: 'student',
|
||||||
keyword: searchKeyword.value,
|
keyword: searchKeyword.value,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
immediate: false,
|
immediate: false,
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const triggerSearch = useDebounceFn(() => {
|
const triggerSearch = useDebounceFn(() => {
|
||||||
if (searchKeyword.value.length > 0) {
|
if (searchKeyword.value.length > 0) {
|
||||||
refreshSearch();
|
refreshSearch()
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
}
|
}
|
||||||
}, 500);
|
else {
|
||||||
|
clearSearch()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
watch(searchKeyword, (newValue) => {
|
watch(searchKeyword, (newValue) => {
|
||||||
if (newValue.length > 0) {
|
if (newValue.length > 0) {
|
||||||
triggerSearch();
|
triggerSearch()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const isInClass = (userId: number) => {
|
const isInClass = (userId: number) => {
|
||||||
return students.value?.data?.some((item) => item.studentId === userId);
|
return students.value?.data?.some(item => item.studentId === userId)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onAddStudent = async (userId: number) => {
|
const onAddStudent = async (userId: number) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -73,46 +74,48 @@ const onAddStudent = async (userId: number) => {
|
|||||||
studentId: userId,
|
studentId: userId,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "正在添加学生...",
|
loading: '正在添加学生...',
|
||||||
success: () => {
|
success: () => {
|
||||||
return "添加学生成功";
|
return '添加学生成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
if (error.status === 409) {
|
if (error.status === 409) {
|
||||||
return "该学生已在班级中";
|
return '该学生已在班级中'
|
||||||
}
|
}
|
||||||
return "添加学生失败";
|
return '添加学生失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
refreshStudents();
|
refreshStudents()
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteRecord = async (recordId: number) => {
|
const onDeleteRecord = async (recordId: number) => {
|
||||||
toast.promise(deleteStudentClassRecord(recordId), {
|
toast.promise(deleteStudentClassRecord(recordId), {
|
||||||
loading: "正在移除学生...",
|
loading: '正在移除学生...',
|
||||||
success: () => {
|
success: () => {
|
||||||
return "移除学生成功";
|
return '移除学生成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "移除学生失败";
|
return '移除学生失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
refreshStudents();
|
refreshStudents()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Card v-bind="props">
|
<Card v-bind="props">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-xl">{{
|
<CardTitle class="text-xl">
|
||||||
classItem.className || "未命名班级"
|
{{
|
||||||
}}</CardTitle>
|
classItem.className || "未命名班级"
|
||||||
|
}}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{{ classItem.notes || "没有描述" }}
|
{{ classItem.notes || "没有描述" }}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@ -137,7 +140,10 @@ const onDeleteRecord = async (recordId: number) => {
|
|||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
@click="studentsSheetOpen = true"
|
@click="studentsSheetOpen = true"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:chevron-right" size="16px" />
|
<Icon
|
||||||
|
name="tabler:chevron-right"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>班级详情</span>
|
<span>班级详情</span>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@ -156,13 +162,22 @@ const onDeleteRecord = async (recordId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加学生</span>
|
<span>添加学生</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-96" :align="'center'">
|
<PopoverContent
|
||||||
|
class="w-96"
|
||||||
|
:align="'center'"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<FormField v-slot="{ componentField }" name="keyword">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="keyword"
|
||||||
|
>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>搜索学生</FormLabel>
|
<FormLabel>搜索学生</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -183,7 +198,9 @@ const onDeleteRecord = async (recordId: number) => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm text-muted-foreground">搜索结果</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
搜索结果
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="searchResults?.data && searchResults.data.length > 0"
|
v-if="searchResults?.data && searchResults.data.length > 0"
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
@ -250,9 +267,13 @@ const onDeleteRecord = async (recordId: number) => {
|
|||||||
<Table v-if="students?.data && students.data.length > 0">
|
<Table v-if="students?.data && students.data.length > 0">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-[100px]">学号</TableHead>
|
<TableHead class="w-[100px]">
|
||||||
|
学号
|
||||||
|
</TableHead>
|
||||||
<TableHead>姓名</TableHead>
|
<TableHead>姓名</TableHead>
|
||||||
<TableHead class="text-right">操作</TableHead>
|
<TableHead class="text-right">
|
||||||
|
操作
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -273,15 +294,23 @@ const onDeleteRecord = async (recordId: number) => {
|
|||||||
class="p-0 text-red-500"
|
class="p-0 text-red-500"
|
||||||
@click="onDeleteRecord(student.id)"
|
@click="onDeleteRecord(student.id)"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:trash" size="16px" />
|
<Icon
|
||||||
|
name="tabler:trash"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
移出
|
移出
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<TableEmpty v-else class="flex justify-center items-center">
|
<TableEmpty
|
||||||
<p class="text-sm text-muted-foreground">该班级暂无成员</p>
|
v-else
|
||||||
|
class="flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
该班级暂无成员
|
||||||
|
</p>
|
||||||
</TableEmpty>
|
</TableEmpty>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { IPerson, ITeacher } from "~/api/course";
|
import type { IPerson, ITeacher } from '~/api/course'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
member: IPerson<ITeacher>;
|
member: IPerson<ITeacher>
|
||||||
isCurrentUser?: boolean;
|
isCurrentUser?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [recordId: number];
|
delete: [recordId: number]
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -22,7 +22,11 @@ const emit = defineEmits<{
|
|||||||
size="icon"
|
size="icon"
|
||||||
@click="emit('delete', member.id)"
|
@click="emit('delete', member.id)"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:logout" size="20px" class="text-red-500" />
|
<Icon
|
||||||
|
name="tabler:logout"
|
||||||
|
size="20px"
|
||||||
|
class="text-red-500"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
export default withNuxt(
|
export default withNuxt(
|
||||||
// Your custom configs here
|
// Your custom configs here
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
"vue/html-self-closing": "off",
|
'vue/html-self-closing': 'off',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
.override('nuxt/vue/rules', {
|
||||||
|
ignores: ['components/ui/**'],
|
||||||
|
})
|
||||||
|
.override('nuxt/typescript/rules', {
|
||||||
|
ignores: ['components/ui/**'],
|
||||||
|
})
|
||||||
|
.override('nuxt/disables/routes', {
|
||||||
|
ignores: ['components/ui/**'],
|
||||||
|
})
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 w-[calc(var(--sidebar-width)+24px)] overflow-hidden"
|
class="flex items-center gap-2 w-[calc(var(--sidebar-width)+24px)] overflow-hidden"
|
||||||
>
|
>
|
||||||
<h1 class="text-lg font-medium">智课教学平台</h1>
|
<h1 class="text-lg font-medium">
|
||||||
|
智课教学平台
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</AppTopbar>
|
</AppTopbar>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useLoginState } from "~/stores/loginState";
|
import { useLoginState } from '~/stores/loginState'
|
||||||
|
|
||||||
export default defineNuxtRouteMiddleware((to, from) => {
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
if (import.meta.server) return;
|
if (import.meta.server) return
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !loginState.isLoggedIn) {
|
if (to.meta.requiresAuth && !loginState.isLoggedIn) {
|
||||||
// let queries = {
|
// let queries = {
|
||||||
@ -11,10 +11,10 @@ export default defineNuxtRouteMiddleware((to, from) => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return navigateTo({
|
return navigateTo({
|
||||||
path: "/user/authenticate",
|
path: '/user/authenticate',
|
||||||
query: {
|
query: {
|
||||||
redirect: to.fullPath || from.fullPath,
|
redirect: to.fullPath || from.fullPath,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
@ -1,48 +1,57 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-11-01",
|
modules: [
|
||||||
devtools: { enabled: true },
|
'@nuxt/eslint',
|
||||||
|
'@nuxt/icon',
|
||||||
|
'@nuxt/fonts',
|
||||||
|
'@nuxt/image',
|
||||||
|
'@nuxt/test-utils',
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'shadcn-nuxt',
|
||||||
|
'@nuxtjs/color-mode',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'pinia-plugin-persistedstate',
|
||||||
|
'dayjs-nuxt',
|
||||||
|
'@formkit/auto-animate',
|
||||||
|
'@vueuse/nuxt',
|
||||||
|
],
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
colorMode: {
|
||||||
|
classSuffix: '',
|
||||||
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
baseURL: "https://service5.fenshenzhike.com:1219/",
|
baseURL: 'https://service5.fenshenzhike.com:1219/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compatibilityDate: '2024-11-01',
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
stylistic: {
|
||||||
|
indent: 2,
|
||||||
|
quotes: 'single',
|
||||||
|
semi: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
|
||||||
"@nuxt/eslint",
|
|
||||||
"@nuxt/icon",
|
|
||||||
"@nuxt/fonts",
|
|
||||||
"@nuxt/image",
|
|
||||||
"@nuxt/test-utils",
|
|
||||||
"@nuxtjs/tailwindcss",
|
|
||||||
"shadcn-nuxt",
|
|
||||||
"@nuxtjs/color-mode",
|
|
||||||
"@pinia/nuxt",
|
|
||||||
"pinia-plugin-persistedstate",
|
|
||||||
"dayjs-nuxt",
|
|
||||||
"@formkit/auto-animate",
|
|
||||||
"@vueuse/nuxt",
|
|
||||||
],
|
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
mode: "svg",
|
mode: 'svg',
|
||||||
},
|
|
||||||
|
|
||||||
colorMode: {
|
|
||||||
classSuffix: "",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shadcn: {
|
shadcn: {
|
||||||
/**
|
/**
|
||||||
* Prefix for all the imported component
|
* Prefix for all the imported component
|
||||||
*/
|
*/
|
||||||
prefix: "",
|
prefix: '',
|
||||||
/**
|
/**
|
||||||
* Directory that the component lives in.
|
* Directory that the component lives in.
|
||||||
* @default "./components/ui"
|
* @default "./components/ui"
|
||||||
*/
|
*/
|
||||||
componentDir: "./components/ui",
|
componentDir: './components/ui',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { IResponse } from "~/api";
|
import type { IResponse } from '~/api'
|
||||||
import { getCourseDetail } from "~/api/course";
|
import { getCourseDetail } from '~/api/course'
|
||||||
import type { SidebarNavGroup } from "~/components/app/Sidebar.vue";
|
import type { SidebarNavGroup } from '~/components/app/Sidebar.vue'
|
||||||
import { topbarNavDefaults } from "~/components/app/Topbar.vue";
|
import { topbarNavDefaults } from '~/components/app/Topbar.vue'
|
||||||
import type { ICourse } from "~/types";
|
import type { ICourse } from '~/types'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fullPath,
|
fullPath,
|
||||||
params: { id },
|
params: { id },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
// const course = await getCourseDetail(id as string);
|
// const course = await getCourseDetail(id as string);
|
||||||
|
|
||||||
@ -19,60 +19,60 @@ const {
|
|||||||
error: courseError,
|
error: courseError,
|
||||||
} = await useAsyncData<
|
} = await useAsyncData<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: ICourse;
|
data: ICourse
|
||||||
}>,
|
}>,
|
||||||
IResponse
|
IResponse
|
||||||
>(() => getCourseDetail(id as string));
|
>(() => getCourseDetail(id as string))
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: `${course.value?.data.courseName || '课程不存在'} - 课程管理`,
|
title: `${course.value?.data.courseName || '课程不存在'} - 课程管理`,
|
||||||
});
|
})
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const sideNav: SidebarNavGroup[] = [
|
const sideNav: SidebarNavGroup[] = [
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "课程章节",
|
title: '课程章节',
|
||||||
url: `/course/${id}/chapters`,
|
url: `/course/${id}/chapters`,
|
||||||
icon: "tabler:books",
|
icon: 'tabler:books',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "教师团队",
|
title: '教师团队',
|
||||||
url: `/course/${id}/team`,
|
url: `/course/${id}/team`,
|
||||||
icon: "tabler:users-group",
|
icon: 'tabler:users-group',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "学生班级",
|
title: '学生班级',
|
||||||
url: `/course/${id}/classes`,
|
url: `/course/${id}/classes`,
|
||||||
icon: "tabler:school",
|
icon: 'tabler:school',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "学生评价",
|
title: '学生评价',
|
||||||
url: `/course/${id}/evaluation`,
|
url: `/course/${id}/evaluation`,
|
||||||
icon: "tabler:mood-smile",
|
icon: 'tabler:mood-smile',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const topNav = [
|
const topNav = [
|
||||||
{
|
{
|
||||||
title: "课程管理",
|
title: '课程管理',
|
||||||
to: `/course/${id}`,
|
to: `/course/${id}`,
|
||||||
icon: "tabler:layout-dashboard",
|
icon: 'tabler:layout-dashboard',
|
||||||
},
|
},
|
||||||
...topbarNavDefaults.slice(1),
|
...topbarNavDefaults.slice(1),
|
||||||
];
|
]
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fullPath === `/course/${id}`) {
|
if (fullPath === `/course/${id}`) {
|
||||||
router.replace(`/course/${id}/chapters`);
|
router.replace(`/course/${id}/chapters`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -116,7 +116,10 @@ onMounted(() => {
|
|||||||
title="加载中..."
|
title="加载中..."
|
||||||
icon="svg-spinners:90-ring-with-bg"
|
icon="svg-spinners:90-ring-with-bg"
|
||||||
/>
|
/>
|
||||||
<NuxtPage v-else :page-key="fullPath" />
|
<NuxtPage
|
||||||
|
v-else
|
||||||
|
:page-key="fullPath"
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</AppPageWithSidebar>
|
</AppPageWithSidebar>
|
||||||
|
@ -1,104 +1,106 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
import {
|
import {
|
||||||
createCourseChatper,
|
createCourseChatper,
|
||||||
deleteCourseChatper,
|
deleteCourseChatper,
|
||||||
deleteCourseSection,
|
deleteCourseSection,
|
||||||
deleteResource,
|
deleteResource,
|
||||||
getCourseChatpers,
|
getCourseChatpers,
|
||||||
} from "~/api/course";
|
} from '~/api/course'
|
||||||
import * as z from "zod";
|
|
||||||
import { useForm } from "vee-validate";
|
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params: { id: courseId },
|
params: { id: courseId },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
|
|
||||||
const { data: chapters, refresh: refreshChapters } = useAsyncData(() =>
|
const { data: chapters, refresh: refreshChapters } = useAsyncData(() =>
|
||||||
getCourseChatpers(parseInt(courseId as string))
|
getCourseChatpers(parseInt(courseId as string)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const createChatperDialogOpen = ref(false);
|
const createChatperDialogOpen = ref(false)
|
||||||
|
|
||||||
const createChatperSchema = toTypedSchema(
|
const createChatperSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().min(2, "章节名称至少2个字符").max(32, "最大长度32个字符"),
|
title: z.string().min(2, '章节名称至少2个字符').max(32, '最大长度32个字符'),
|
||||||
courseId: z.number().min(1, "课程ID不能为空"),
|
courseId: z.number().min(1, '课程ID不能为空'),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const createChatperForm = useForm({
|
const createChatperForm = useForm({
|
||||||
validationSchema: createChatperSchema,
|
validationSchema: createChatperSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
title: "",
|
title: '',
|
||||||
courseId: Number(courseId),
|
courseId: Number(courseId),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const onCreateChapterSubmit = createChatperForm.handleSubmit((values) => {
|
const onCreateChapterSubmit = createChatperForm.handleSubmit((values) => {
|
||||||
toast.promise(createCourseChatper(values), {
|
toast.promise(createCourseChatper(values), {
|
||||||
loading: "正在创建章节...",
|
loading: '正在创建章节...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshChapters();
|
refreshChapters()
|
||||||
createChatperForm.resetForm();
|
createChatperForm.resetForm()
|
||||||
createChatperDialogOpen.value = false;
|
createChatperDialogOpen.value = false
|
||||||
return "创建章节成功";
|
return '创建章节成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "创建章节失败";
|
return '创建章节失败'
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const onDeleteChatper = (chapterId: number) => {
|
const onDeleteChatper = (chapterId: number) => {
|
||||||
toast.promise(deleteCourseChatper(chapterId), {
|
toast.promise(deleteCourseChatper(chapterId), {
|
||||||
loading: "正在删除章节...",
|
loading: '正在删除章节...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshChapters();
|
refreshChapters()
|
||||||
return "删除章节成功";
|
return '删除章节成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "删除章节失败";
|
return '删除章节失败'
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteSection = (sectionId: number) => {
|
const onDeleteSection = (sectionId: number) => {
|
||||||
toast.promise(deleteCourseSection(sectionId), {
|
toast.promise(deleteCourseSection(sectionId), {
|
||||||
loading: "正在删除小节...",
|
loading: '正在删除小节...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshChapters();
|
refreshChapters()
|
||||||
return "删除小节成功";
|
return '删除小节成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "删除小节失败";
|
return '删除小节失败'
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteResource = (resourceId: number) => {
|
const onDeleteResource = (resourceId: number) => {
|
||||||
toast.promise(deleteResource(resourceId), {
|
toast.promise(deleteResource(resourceId), {
|
||||||
loading: "正在删除资源...",
|
loading: '正在删除资源...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshChapters();
|
refreshChapters()
|
||||||
return "删除资源成功";
|
return '删除资源成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "删除资源失败";
|
return '删除资源失败'
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 px-4 py-2">
|
<div class="flex flex-col gap-4 px-4 py-2">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<h1 class="text-xl font-medium">课程章节管理</h1>
|
<h1 class="text-xl font-medium">
|
||||||
|
课程章节管理
|
||||||
|
</h1>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Tooltip :delay-duration="0">
|
<Tooltip :delay-duration="0">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@ -121,7 +123,10 @@ const onDeleteResource = (resourceId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加章节</span>
|
<span>添加章节</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -136,7 +141,10 @@ const onDeleteResource = (resourceId: number) => {
|
|||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
@submit="onCreateChapterSubmit"
|
@submit="onCreateChapterSubmit"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="title">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="title"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>章节名称</FormLabel>
|
<FormLabel>章节名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -149,11 +157,19 @@ const onDeleteResource = (resourceId: number) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<input type="hidden" name="courseId" />
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="courseId"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" form="create-chapter-form">创建</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="create-chapter-form"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -176,7 +192,11 @@ const onDeleteResource = (resourceId: number) => {
|
|||||||
@delete-resource="onDeleteResource"
|
@delete-resource="onDeleteResource"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EmptyScreen v-else title="暂无章节" icon="fluent-color:document-add-24">
|
<EmptyScreen
|
||||||
|
v-else
|
||||||
|
title="暂无章节"
|
||||||
|
icon="fluent-color:document-add-24"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
课程章节列表为空,先创建章节吧
|
课程章节列表为空,先创建章节吧
|
||||||
@ -186,7 +206,10 @@ const onDeleteResource = (resourceId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
@click="createChatperDialogOpen = true"
|
@click="createChatperDialogOpen = true"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加章节</span>
|
<span>添加章节</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,67 +1,67 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { useForm } from "vee-validate";
|
import { useForm } from 'vee-validate'
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
import {
|
import {
|
||||||
createClass,
|
createClass,
|
||||||
getClassListByCourse,
|
getClassListByCourse,
|
||||||
getCourseDetail,
|
getCourseDetail,
|
||||||
} from "~/api/course";
|
} from '~/api/course'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params: { id: courseId },
|
params: { id: courseId },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
|
|
||||||
// const loginState = useLoginState();
|
// const loginState = useLoginState();
|
||||||
const course = await getCourseDetail(courseId as string);
|
const course = await getCourseDetail(courseId as string)
|
||||||
|
|
||||||
const { data: classes, refresh: refreshClasses } = useAsyncData(() =>
|
const { data: classes, refresh: refreshClasses } = useAsyncData(() =>
|
||||||
getClassListByCourse(parseInt(courseId as string))
|
getClassListByCourse(parseInt(courseId as string)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const createClassDialogOpen = ref(false);
|
const createClassDialogOpen = ref(false)
|
||||||
|
|
||||||
const createClassSchema = toTypedSchema(
|
const createClassSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
className: z
|
className: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, "班级名称至少2个字符")
|
.min(2, '班级名称至少2个字符')
|
||||||
.max(32, "最大长度32个字符"),
|
.max(32, '最大长度32个字符'),
|
||||||
notes: z.string().max(200, "班级介绍最大长度200个字符"),
|
notes: z.string().max(200, '班级介绍最大长度200个字符'),
|
||||||
courseId: z.number().min(1, "课程ID不能为空"),
|
courseId: z.number().min(1, '课程ID不能为空'),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const createClassForm = useForm({
|
const createClassForm = useForm({
|
||||||
validationSchema: createClassSchema,
|
validationSchema: createClassSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
className: "",
|
className: '',
|
||||||
notes: "",
|
notes: '',
|
||||||
courseId: Number(courseId),
|
courseId: Number(courseId),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
||||||
toast.promise(createClass(values), {
|
toast.promise(createClass(values), {
|
||||||
loading: "正在创建班级...",
|
loading: '正在创建班级...',
|
||||||
success: () => {
|
success: () => {
|
||||||
createClassForm.resetForm();
|
createClassForm.resetForm()
|
||||||
createClassDialogOpen.value = false;
|
createClassDialogOpen.value = false
|
||||||
return "创建班级成功";
|
return '创建班级成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "创建班级失败";
|
return '创建班级失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
refreshClasses();
|
refreshClasses()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
// const onDeleteClass = (classId: number) => {
|
// const onDeleteClass = (classId: number) => {
|
||||||
// toast.promise(deleteCourseClass(classId), {
|
// toast.promise(deleteCourseClass(classId), {
|
||||||
@ -94,7 +94,10 @@ const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>创建班级</span>
|
<span>创建班级</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -109,7 +112,10 @@ const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
|||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
@submit="onCreateClassSubmit"
|
@submit="onCreateClassSubmit"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="className">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="className"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>班级名称</FormLabel>
|
<FormLabel>班级名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -122,7 +128,10 @@ const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="{ componentField }" name="notes">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="notes"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>班级介绍</FormLabel>
|
<FormLabel>班级介绍</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -134,11 +143,19 @@ const onCreateClassSubmit = createClassForm.handleSubmit((values) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<input type="hidden" name="courseId" />
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="courseId"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" form="create-class-form">创建</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="create-class-form"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { userSearch } from "~/api";
|
import { userSearch } from '~/api'
|
||||||
import {
|
import {
|
||||||
addTeacherToCourse,
|
addTeacherToCourse,
|
||||||
deleteTeacherTeamRecord,
|
deleteTeacherTeamRecord,
|
||||||
getCourseDetail,
|
getCourseDetail,
|
||||||
getTeacherTeamByCourse,
|
getTeacherTeamByCourse,
|
||||||
} from "~/api/course";
|
} from '~/api/course'
|
||||||
import type { FetchError } from "~/types";
|
import type { FetchError } from '~/types'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params: { id: courseId },
|
params: { id: courseId },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
const course = await getCourseDetail(courseId as string);
|
const course = await getCourseDetail(courseId as string)
|
||||||
|
|
||||||
const { data: teacherTeam, refresh: refreshTeacherTeam } = useAsyncData(() =>
|
const { data: teacherTeam, refresh: refreshTeacherTeam } = useAsyncData(() =>
|
||||||
getTeacherTeamByCourse(parseInt(courseId as string))
|
getTeacherTeamByCourse(parseInt(courseId as string)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const searchKeyword = ref("");
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: searchResults,
|
data: searchResults,
|
||||||
@ -33,32 +33,33 @@ const {
|
|||||||
} = useAsyncData(
|
} = useAsyncData(
|
||||||
() =>
|
() =>
|
||||||
userSearch({
|
userSearch({
|
||||||
searchType: "teacher",
|
searchType: 'teacher',
|
||||||
keyword: searchKeyword.value,
|
keyword: searchKeyword.value,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
immediate: false,
|
immediate: false,
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
// watch searchKeyword and refresh search results, with debounce
|
// watch searchKeyword and refresh search results, with debounce
|
||||||
const triggerSearch = useDebounceFn(() => {
|
const triggerSearch = useDebounceFn(() => {
|
||||||
if (searchKeyword.value.length > 0) {
|
if (searchKeyword.value.length > 0) {
|
||||||
refreshSearch();
|
refreshSearch()
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
}
|
}
|
||||||
}, 500);
|
else {
|
||||||
|
clearSearch()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
watch(searchKeyword, (newValue) => {
|
watch(searchKeyword, (newValue) => {
|
||||||
if (newValue.length > 0) {
|
if (newValue.length > 0) {
|
||||||
triggerSearch();
|
triggerSearch()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const isInTeam = (userId: number) => {
|
const isInTeam = (userId: number) => {
|
||||||
return teacherTeam?.value?.data?.some((item) => item.teacherId === userId);
|
return teacherTeam?.value?.data?.some(item => item.teacherId === userId)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onAddTeacherToCourse = (teacherId: number) => {
|
const onAddTeacherToCourse = (teacherId: number) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -67,33 +68,33 @@ const onAddTeacherToCourse = (teacherId: number) => {
|
|||||||
teacherId,
|
teacherId,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "正在添加教师...",
|
loading: '正在添加教师...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshTeacherTeam();
|
refreshTeacherTeam()
|
||||||
return "添加教师成功";
|
return '添加教师成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
if (error.statusCode === 409) {
|
if (error.statusCode === 409) {
|
||||||
return "该教师已在团队中";
|
return '该教师已在团队中'
|
||||||
}
|
}
|
||||||
return `添加教师失败:${error.message}`;
|
return `添加教师失败:${error.message}`
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteTeacher = (recordId: number) => {
|
const onDeleteTeacher = (recordId: number) => {
|
||||||
toast.promise(deleteTeacherTeamRecord(recordId), {
|
toast.promise(deleteTeacherTeamRecord(recordId), {
|
||||||
loading: "正在移出教师...",
|
loading: '正在移出教师...',
|
||||||
success: () => {
|
success: () => {
|
||||||
refreshTeacherTeam();
|
refreshTeacherTeam()
|
||||||
return "移出教师成功";
|
return '移出教师成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
return `移出教师失败:${error.message}`;
|
return `移出教师失败:${error.message}`
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -113,13 +114,22 @@ const onDeleteTeacher = (recordId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
<span>添加教师</span>
|
<span>添加教师</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-96" :align="'end'">
|
<PopoverContent
|
||||||
|
class="w-96"
|
||||||
|
:align="'end'"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<FormField v-slot="{ componentField }" name="keyword">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="keyword"
|
||||||
|
>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>搜索教师</FormLabel>
|
<FormLabel>搜索教师</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -140,7 +150,9 @@ const onDeleteTeacher = (recordId: number) => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm text-muted-foreground">搜索结果</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
搜索结果
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="searchResults?.data && searchResults.data.length > 0"
|
v-if="searchResults?.data && searchResults.data.length > 0"
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import * as z from "zod";
|
import * as z from 'zod'
|
||||||
import { createCourse, deleteCourse, listUserCourses } from "~/api/course";
|
import type { FetchError } from 'ofetch'
|
||||||
import type { FetchError } from "ofetch";
|
import { createCourse, deleteCourse, listUserCourses } from '~/api/course'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "no-sidebar",
|
layout: 'no-sidebar',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "课程中心",
|
title: '课程中心',
|
||||||
});
|
})
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
const deleteMode = ref(false);
|
const deleteMode = ref(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: coursesList,
|
data: coursesList,
|
||||||
refresh: refreshCoursesList,
|
refresh: refreshCoursesList,
|
||||||
status: _,
|
status: _,
|
||||||
} = useAsyncData(() => listUserCourses(loginState.user.userId));
|
} = useAsyncData(() => listUserCourses(loginState.user.userId))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成学期列表
|
* 生成学期列表
|
||||||
@ -29,74 +29,74 @@ const {
|
|||||||
* @returns 学期列表
|
* @returns 学期列表
|
||||||
*/
|
*/
|
||||||
const getSemesters = (years: number) => {
|
const getSemesters = (years: number) => {
|
||||||
const currentYear = new Date().getFullYear() - 1;
|
const currentYear = new Date().getFullYear() - 1
|
||||||
const semesters = [];
|
const semesters = []
|
||||||
for (let i = 0; i < years + 1; i++) {
|
for (let i = 0; i < years + 1; i++) {
|
||||||
const year = currentYear + i;
|
const year = currentYear + i
|
||||||
semesters.push(`${year}-${year + 1}-1`, `${year}-${year + 1}-2`);
|
semesters.push(`${year}-${year + 1}-1`, `${year}-${year + 1}-2`)
|
||||||
}
|
}
|
||||||
return semesters;
|
return semesters
|
||||||
};
|
}
|
||||||
|
|
||||||
const createCourseDialogOpen = ref(false);
|
const createCourseDialogOpen = ref(false)
|
||||||
|
|
||||||
const courseFormSchema = toTypedSchema(
|
const courseFormSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
courseName: z
|
courseName: z
|
||||||
.string()
|
.string()
|
||||||
.min(4, "课程名称不能为空")
|
.min(4, '课程名称不能为空')
|
||||||
.max(32, "最大长度32个字符"),
|
.max(32, '最大长度32个字符'),
|
||||||
profile: z.string().optional(),
|
profile: z.string().optional(),
|
||||||
schoolName: z.string().min(4).max(32),
|
schoolName: z.string().min(4).max(32),
|
||||||
teacherName: z.string().optional(),
|
teacherName: z.string().optional(),
|
||||||
semester: z.enum([...getSemesters(3)] as [string, ...string[]]),
|
semester: z.enum([...getSemesters(3)] as [string, ...string[]]),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const folderFormSchema = toTypedSchema(
|
const folderFormSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
folderName: z.string().min(2).max(32),
|
folderName: z.string().min(2).max(32),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onCourseSubmit = (values: any) => {
|
const onCourseSubmit = (values: any) => {
|
||||||
toast.promise(createCourse(values), {
|
toast.promise(createCourse(values), {
|
||||||
loading: "正在创建课程...",
|
loading: '正在创建课程...',
|
||||||
success: () => {
|
success: () => {
|
||||||
createCourseDialogOpen.value = false;
|
createCourseDialogOpen.value = false
|
||||||
return "创建课程成功";
|
return '创建课程成功'
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
return `创建课程失败:${error.data?.msg || error.message}`;
|
return `创建课程失败:${error.data?.msg || error.message}`
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
refreshCoursesList();
|
refreshCoursesList()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onFolderSubmit = (values: any) => {
|
const onFolderSubmit = (values: any) => {
|
||||||
toast("submit data:", {
|
toast('submit data:', {
|
||||||
description: JSON.stringify(values, null, 2),
|
description: JSON.stringify(values, null, 2),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDeleteCourse = (courseId: number) => {
|
const onDeleteCourse = (courseId: number) => {
|
||||||
toast.promise(deleteCourse(courseId), {
|
toast.promise(deleteCourse(courseId), {
|
||||||
loading: "正在删除课程...",
|
loading: '正在删除课程...',
|
||||||
success: () => {
|
success: () => {
|
||||||
return "删除课程成功";
|
return '删除课程成功'
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
return "删除课程失败";
|
return '删除课程失败'
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
refreshCoursesList();
|
refreshCoursesList()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -110,8 +110,14 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
>
|
>
|
||||||
<Dialog v-model:open="createCourseDialogOpen">
|
<Dialog v-model:open="createCourseDialogOpen">
|
||||||
<DialogTrigger as-child>
|
<DialogTrigger as-child>
|
||||||
<Button variant="secondary" size="sm">
|
<Button
|
||||||
<Icon name="tabler:plus" size="16px" />
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
新建课程
|
新建课程
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -129,7 +135,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
@submit="handleSubmit($event, onCourseSubmit)"
|
@submit="handleSubmit($event, onCourseSubmit)"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="courseName">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="courseName"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>课程名称</FormLabel>
|
<FormLabel>课程名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -142,7 +151,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="{ componentField }" name="profile">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="profile"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>课程介绍</FormLabel>
|
<FormLabel>课程介绍</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -155,7 +167,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="{ componentField }" name="schoolName">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="schoolName"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>学校名称</FormLabel>
|
<FormLabel>学校名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -173,7 +188,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
name="teacherName"
|
name="teacherName"
|
||||||
:value="loginState.user.nickName"
|
:value="loginState.user.nickName"
|
||||||
/>
|
/>
|
||||||
<FormField v-slot="{ componentField }" name="semester">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="semester"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>学期</FormLabel>
|
<FormLabel>学期</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -202,7 +220,12 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" form="createCourseForm">创建</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="createCourseForm"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -217,8 +240,15 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger as-child>
|
<DialogTrigger as-child>
|
||||||
<!-- TODO: disable temporarily -->
|
<!-- TODO: disable temporarily -->
|
||||||
<Button variant="secondary" size="sm" class="hidden">
|
<Button
|
||||||
<Icon name="tabler:folder-plus" size="16px" />
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="tabler:folder-plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
新建文件夹
|
新建文件夹
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -236,7 +266,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
@submit="handleSubmit($event, onFolderSubmit)"
|
@submit="handleSubmit($event, onFolderSubmit)"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="folderName">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="folderName"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>文件夹名称</FormLabel>
|
<FormLabel>文件夹名称</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -252,7 +285,12 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" form="createCourseForm">创建</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="createCourseForm"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -280,7 +318,11 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
@delete-course="onDeleteCourse"
|
@delete-course="onDeleteCourse"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EmptyScreen v-else title="暂无课程" icon="fluent-color:people-list-24">
|
<EmptyScreen
|
||||||
|
v-else
|
||||||
|
title="暂无课程"
|
||||||
|
icon="fluent-color:people-list-24"
|
||||||
|
>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
还没有创建或加入课程
|
还没有创建或加入课程
|
||||||
</p>
|
</p>
|
||||||
@ -289,7 +331,10 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
@click="createCourseDialogOpen = true"
|
@click="createCourseDialogOpen = true"
|
||||||
>
|
>
|
||||||
<Icon name="tabler:plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
新建课程
|
新建课程
|
||||||
</Button>
|
</Button>
|
||||||
</EmptyScreen>
|
</EmptyScreen>
|
||||||
|
@ -1,41 +1,41 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Settings } from "lucide-vue-next";
|
import { Settings } from 'lucide-vue-next'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "AI 教案设计",
|
title: 'AI 教案设计',
|
||||||
url: "/test",
|
url: '/test',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 案例设计",
|
title: 'AI 案例设计',
|
||||||
url: "/test",
|
url: '/test',
|
||||||
icon: "tabler:settings",
|
icon: 'tabler:settings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 课件设计",
|
title: 'AI 课件设计',
|
||||||
url: "/test",
|
url: '/test',
|
||||||
icon: "tabler:settings",
|
icon: 'tabler:settings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 出题",
|
title: 'AI 出题',
|
||||||
url: "/test",
|
url: '/test',
|
||||||
icon: "tabler:settings",
|
icon: 'tabler:settings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "微视频制作",
|
title: '微视频制作',
|
||||||
url: "/test",
|
url: '/test',
|
||||||
icon: "tabler:settings",
|
icon: 'tabler:settings',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "no-sidebar",
|
layout: 'no-sidebar',
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "no-sidebar",
|
layout: 'no-sidebar',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "blank",
|
layout: 'blank',
|
||||||
});
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "AI 智慧课程平台",
|
title: 'AI 智慧课程平台',
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -23,19 +23,28 @@ useHead({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="fn-block border-blue-300/80 from-blue-300/5 via-blue-300/40 to-blue-400/20"
|
class="fn-block border-blue-300/80 from-blue-300/5 via-blue-300/40 to-blue-400/20"
|
||||||
>
|
>
|
||||||
<Icon name="fluent-color:chat-multiple-24" class="text-5xl" />
|
<Icon
|
||||||
|
name="fluent-color:chat-multiple-24"
|
||||||
|
class="text-5xl"
|
||||||
|
/>
|
||||||
<h2 class="text-lg font-medium text-blue-500">AI 助教</h2>
|
<h2 class="text-lg font-medium text-blue-500">AI 助教</h2>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="fn-block border-teal-300/80 from-teal-300/5 via-teal-300/40 to-teal-400/20"
|
class="fn-block border-teal-300/80 from-teal-300/5 via-teal-300/40 to-teal-400/20"
|
||||||
>
|
>
|
||||||
<Icon name="fluent-color:book-open-24" class="text-5xl" />
|
<Icon
|
||||||
|
name="fluent-color:book-open-24"
|
||||||
|
class="text-5xl"
|
||||||
|
/>
|
||||||
<h2 class="text-lg font-medium text-teal-500">AI 助学</h2>
|
<h2 class="text-lg font-medium text-teal-500">AI 助学</h2>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="fn-block border-violet-300/80 from-violet-300/5 via-violet-300/40 to-violet-400/20"
|
class="fn-block border-violet-300/80 from-violet-300/5 via-violet-300/40 to-violet-400/20"
|
||||||
>
|
>
|
||||||
<Icon name="fluent-color:star-settings-24" class="text-5xl" />
|
<Icon
|
||||||
|
name="fluent-color:star-settings-24"
|
||||||
|
class="text-5xl"
|
||||||
|
/>
|
||||||
<h2 class="text-lg font-medium text-violet-500">AI 助管</h2>
|
<h2 class="text-lg font-medium text-violet-500">AI 助管</h2>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,43 +1,48 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import VueOfficePptx from "@vue-office/pptx";
|
import VueOfficePptx from '@vue-office/pptx'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params: { resource_url },
|
params: { resource_url },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
|
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
return atob(resource_url as string);
|
return atob(resource_url as string)
|
||||||
});
|
})
|
||||||
|
|
||||||
const fileExtension = computed(() => {
|
const fileExtension = computed(() => {
|
||||||
const lastDotIndex = url.value.lastIndexOf(".");
|
const lastDotIndex = url.value.lastIndexOf('.')
|
||||||
if (lastDotIndex === -1) return "";
|
if (lastDotIndex === -1) return ''
|
||||||
return url.value.substring(lastDotIndex + 1).toLowerCase();
|
return url.value.substring(lastDotIndex + 1).toLowerCase()
|
||||||
});
|
})
|
||||||
|
|
||||||
const fileType = computed(() => {
|
const fileType = computed(() => {
|
||||||
const ext = fileExtension.value;
|
const ext = fileExtension.value
|
||||||
if (ext === "pdf") return "pdf";
|
if (ext === 'pdf') return 'pdf'
|
||||||
if (ext === "doc" || ext === "docx") return "word";
|
if (ext === 'doc' || ext === 'docx') return 'word'
|
||||||
if (ext === "ppt" || ext === "pptx") return "ppt";
|
if (ext === 'ppt' || ext === 'pptx') return 'ppt'
|
||||||
if (ext === "xls" || ext === "xlsx") return "excel";
|
if (ext === 'xls' || ext === 'xlsx') return 'excel'
|
||||||
if (ext === "txt") return "txt";
|
if (ext === 'txt') return 'txt'
|
||||||
if (ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "gif")
|
if (ext === 'jpg' || ext === 'jpeg' || ext === 'png' || ext === 'gif')
|
||||||
return "image";
|
return 'image'
|
||||||
if (ext === "mp4" || ext === "avi" || ext === "mov") return "video";
|
if (ext === 'mp4' || ext === 'avi' || ext === 'mov') return 'video'
|
||||||
if (ext === "mp3" || ext === "wav") return "audio";
|
if (ext === 'mp3' || ext === 'wav') return 'audio'
|
||||||
return "";
|
return ''
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-screen">
|
<div class="w-full h-screen">
|
||||||
<div v-if="!url">
|
<div v-if="!url">
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex items-center justify-center h-full">
|
||||||
<p class="text-muted-foreground">资源链接无效</p>
|
<p class="text-muted-foreground">
|
||||||
|
资源链接无效
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full h-full">
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full h-full"
|
||||||
|
>
|
||||||
<VueOfficePptx
|
<VueOfficePptx
|
||||||
v-if="fileType === 'ppt'"
|
v-if="fileType === 'ppt'"
|
||||||
:src="url"
|
:src="url"
|
||||||
|
@ -1,74 +1,74 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { useForm } from "vee-validate";
|
import { useForm } from 'vee-validate'
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from 'vue-sonner'
|
||||||
import * as z from "zod";
|
import * as z from 'zod'
|
||||||
import { userLogin, type LoginResponse } from "~/api";
|
import type { FetchError } from 'ofetch'
|
||||||
import type { FetchError } from "ofetch";
|
import { userLogin, type LoginResponse } from '~/api'
|
||||||
|
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
const {
|
const {
|
||||||
query: { redirect },
|
query: { redirect },
|
||||||
} = useRoute();
|
} = useRoute()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const redirectBack = () => {
|
const redirectBack = () => {
|
||||||
router.replace(redirect ? (redirect as string) : "/");
|
router.replace(redirect ? (redirect as string) : '/')
|
||||||
};
|
}
|
||||||
|
|
||||||
const pending = ref(false);
|
const pending = ref(false)
|
||||||
|
|
||||||
const passwordLoginSchema = toTypedSchema(
|
const passwordLoginSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
username: z.string().nonempty("请输入用户名"),
|
username: z.string().nonempty('请输入用户名'),
|
||||||
password: z.string().min(6, "密码至少6个字符"),
|
password: z.string().min(6, '密码至少6个字符'),
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const passwordLoginForm = useForm({
|
const passwordLoginForm = useForm({
|
||||||
validationSchema: passwordLoginSchema,
|
validationSchema: passwordLoginSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: '',
|
||||||
password: "",
|
password: '',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
||||||
pending.value = true;
|
pending.value = true
|
||||||
toast.promise(
|
toast.promise(
|
||||||
userLogin({
|
userLogin({
|
||||||
account: values.username,
|
account: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
loginType: "teacher",
|
loginType: 'teacher',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "登录中...",
|
loading: '登录中...',
|
||||||
success: async (data: LoginResponse) => {
|
success: async (data: LoginResponse) => {
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
toast.error(`登录失败:${data.msg}`);
|
toast.error(`登录失败:${data.msg}`)
|
||||||
return "登录中...";
|
return '登录中...'
|
||||||
}
|
}
|
||||||
loginState.token = data.token;
|
loginState.token = data.token
|
||||||
const userInfo = await loginState.checkLogin();
|
const userInfo = await loginState.checkLogin()
|
||||||
if (!userInfo) {
|
if (!userInfo) {
|
||||||
toast.error(`获取用户信息失败`);
|
toast.error(`获取用户信息失败`)
|
||||||
return "登录中...";
|
return '登录中...'
|
||||||
}
|
}
|
||||||
redirectBack();
|
redirectBack()
|
||||||
return `登录成功`;
|
return `登录成功`
|
||||||
},
|
},
|
||||||
error: (error: FetchError) => {
|
error: (error: FetchError) => {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
return "用户名或密码错误";
|
return '用户名或密码错误'
|
||||||
}
|
}
|
||||||
return `登录失败:${error.message}`;
|
return `登录失败:${error.message}`
|
||||||
},
|
},
|
||||||
finally: () => {
|
finally: () => {
|
||||||
pending.value = false;
|
pending.value = false
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -86,23 +86,35 @@ const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
|||||||
<h1 class="text-4xl font-medium drop-shadow-xl text-ai-gradient mb-12">
|
<h1 class="text-4xl font-medium drop-shadow-xl text-ai-gradient mb-12">
|
||||||
AI 智慧课程平台
|
AI 智慧课程平台
|
||||||
</h1>
|
</h1>
|
||||||
<Tabs default-value="account" class="w-[480px]">
|
<Tabs
|
||||||
|
default-value="account"
|
||||||
|
class="w-[480px]"
|
||||||
|
>
|
||||||
<TabsList class="grid w-full grid-cols-3">
|
<TabsList class="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="account">
|
<TabsTrigger value="account">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Icon name="tabler:key" size="16px" />
|
<Icon
|
||||||
|
name="tabler:key"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
密码登录
|
密码登录
|
||||||
</div>
|
</div>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="otp">
|
<TabsTrigger value="otp">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Icon name="tabler:password-mobile-phone" size="16px" />
|
<Icon
|
||||||
|
name="tabler:password-mobile-phone"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
验证码登录
|
验证码登录
|
||||||
</div>
|
</div>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="recovery">
|
<TabsTrigger value="recovery">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Icon name="tabler:lock-question" size="16px" />
|
<Icon
|
||||||
|
name="tabler:lock-question"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
找回密码
|
找回密码
|
||||||
</div>
|
</div>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -122,7 +134,10 @@ const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
|||||||
keep-values
|
keep-values
|
||||||
@submit="onPasswordLoginSubmit"
|
@submit="onPasswordLoginSubmit"
|
||||||
>
|
>
|
||||||
<FormField v-slot="{ componentField }" name="username">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="username"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>用户名</FormLabel>
|
<FormLabel>用户名</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -135,7 +150,10 @@ const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField v-slot="{ componentField }" name="password">
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="password"
|
||||||
|
>
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>密码</FormLabel>
|
<FormLabel>密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -169,7 +187,10 @@ const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<div>
|
<div>
|
||||||
<Button variant="link">
|
<Button variant="link">
|
||||||
<Icon name="tabler:user-plus" size="16px" />
|
<Icon
|
||||||
|
name="tabler:user-plus"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
注册新账号
|
注册新账号
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,48 +1,49 @@
|
|||||||
import { userProfile } from "~/api";
|
import { userProfile } from '~/api'
|
||||||
import type { IUser } from "~/types";
|
import type { IUser } from '~/types'
|
||||||
|
|
||||||
export const useLoginState = defineStore(
|
export const useLoginState = defineStore(
|
||||||
"loginState",
|
'loginState',
|
||||||
() => {
|
() => {
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false)
|
||||||
|
|
||||||
const token = ref<string | null>(null);
|
const token = ref<string | null>(null)
|
||||||
const user = ref<IUser>({} as IUser);
|
const user = ref<IUser>({} as IUser)
|
||||||
|
|
||||||
const checkLogin = async (): Promise<IUser | false> => {
|
const checkLogin = async (): Promise<IUser | false> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!token.value) {
|
if (!token.value) {
|
||||||
user.value = {} as IUser;
|
user.value = {} as IUser
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false
|
||||||
return reject(false);
|
return reject(false)
|
||||||
}
|
}
|
||||||
userProfile()
|
userProfile()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
user.value = res.user;
|
user.value = res.user
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true
|
||||||
resolve(res.user);
|
resolve(res.user)
|
||||||
} else {
|
}
|
||||||
user.value = {} as IUser;
|
else {
|
||||||
isLoggedIn.value = false;
|
user.value = {} as IUser
|
||||||
token.value = null;
|
isLoggedIn.value = false
|
||||||
reject(false);
|
token.value = null
|
||||||
|
reject(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((_) => {
|
.catch((_) => {
|
||||||
user.value = {} as IUser;
|
user.value = {} as IUser
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false
|
||||||
token.value = null;
|
token.value = null
|
||||||
reject(false);
|
reject(false)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false
|
||||||
token.value = null;
|
token.value = null
|
||||||
user.value = {} as IUser;
|
user.value = {} as IUser
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@ -50,13 +51,13 @@ export const useLoginState = defineStore(
|
|||||||
user,
|
user,
|
||||||
checkLogin,
|
checkLogin,
|
||||||
logout,
|
logout,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: {
|
persist: {
|
||||||
key: "xshic_user_state",
|
key: 'xshic_user_state',
|
||||||
storage: piniaPluginPersistedstate.localStorage(),
|
storage: piniaPluginPersistedstate.localStorage(),
|
||||||
pick: ["isLoggedIn", "token", "user"],
|
pick: ['isLoggedIn', 'token', 'user'],
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
@ -1,67 +1,67 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class"],
|
darkMode: ['class'],
|
||||||
content: [],
|
content: [],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: 'var(--radius)',
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
},
|
},
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
chart: {
|
chart: {
|
||||||
1: "hsl(var(--chart-1))",
|
1: 'hsl(var(--chart-1))',
|
||||||
2: "hsl(var(--chart-2))",
|
2: 'hsl(var(--chart-2))',
|
||||||
3: "hsl(var(--chart-3))",
|
3: 'hsl(var(--chart-3))',
|
||||||
4: "hsl(var(--chart-4))",
|
4: 'hsl(var(--chart-4))',
|
||||||
5: "hsl(var(--chart-5))",
|
5: 'hsl(var(--chart-5))',
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
DEFAULT: "hsl(var(--sidebar-background))",
|
'DEFAULT': 'hsl(var(--sidebar-background))',
|
||||||
foreground: "hsl(var(--sidebar-foreground))",
|
'foreground': 'hsl(var(--sidebar-foreground))',
|
||||||
primary: "hsl(var(--sidebar-primary))",
|
'primary': 'hsl(var(--sidebar-primary))',
|
||||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
accent: "hsl(var(--sidebar-accent))",
|
'accent': 'hsl(var(--sidebar-accent))',
|
||||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
border: "hsl(var(--sidebar-border))",
|
'border': 'hsl(var(--sidebar-border))',
|
||||||
ring: "hsl(var(--sidebar-ring))",
|
'ring': 'hsl(var(--sidebar-ring))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require('tailwindcss-animate')],
|
||||||
};
|
}
|
||||||
|
@ -3,29 +3,29 @@
|
|||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export type CourseResourceType =
|
export type CourseResourceType =
|
||||||
| "video"
|
| 'video'
|
||||||
| "image"
|
| 'image'
|
||||||
| "doc"
|
| 'doc'
|
||||||
| "ppt"
|
| 'ppt'
|
||||||
| "resource"
|
| 'resource'
|
||||||
| "temp";
|
| 'temp'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 课程资源
|
* 课程资源
|
||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
export interface IResource {
|
export interface IResource {
|
||||||
id: number;
|
id: number
|
||||||
resourceName: string;
|
resourceName: string
|
||||||
resourceSize: number;
|
resourceSize: number
|
||||||
resourceType: CourseResourceType;
|
resourceType: CourseResourceType
|
||||||
resourceUrl: string;
|
resourceUrl: string
|
||||||
allowDownload: boolean;
|
allowDownload: boolean
|
||||||
isRepo: boolean;
|
isRepo: boolean
|
||||||
ownerId: number;
|
ownerId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ICreateResource = Omit<IResource, "id">;
|
export type ICreateResource = Omit<IResource, 'id'>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 课程小节
|
* 课程小节
|
||||||
@ -35,9 +35,9 @@ export type ICreateResource = Omit<IResource, "id">;
|
|||||||
* @property {ICourseResource[]} resources - 章节中的资源数组
|
* @property {ICourseResource[]} resources - 章节中的资源数组
|
||||||
*/
|
*/
|
||||||
export interface ICourseSection {
|
export interface ICourseSection {
|
||||||
id: number;
|
id: number
|
||||||
title: string;
|
title: string
|
||||||
resources: IResource[];
|
resources: IResource[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,11 +50,11 @@ export interface ICourseSection {
|
|||||||
* @property {[]} [detections] - 待定。
|
* @property {[]} [detections] - 待定。
|
||||||
*/
|
*/
|
||||||
export interface ICourseChapter {
|
export interface ICourseChapter {
|
||||||
id: number;
|
id: number
|
||||||
title: string;
|
title: string
|
||||||
isPublished: boolean;
|
isPublished: boolean
|
||||||
sections: ICourseSection[];
|
sections: ICourseSection[]
|
||||||
detections?: [];
|
detections?: []
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,15 +62,15 @@ export interface ICourseChapter {
|
|||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
export interface ICourse {
|
export interface ICourse {
|
||||||
id: number;
|
id: number
|
||||||
courseName: string;
|
courseName: string
|
||||||
profile: string;
|
profile: string
|
||||||
previewUrl: string | null;
|
previewUrl: string | null
|
||||||
schoolName: string;
|
schoolName: string
|
||||||
teacherName: string;
|
teacherName: string
|
||||||
semester: string;
|
semester: string
|
||||||
status: number;
|
status: number
|
||||||
created_at: Date;
|
created_at: Date
|
||||||
updated_at: Date;
|
updated_at: Date
|
||||||
remark: string | null;
|
remark: string | null
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./user";
|
export * from './user'
|
||||||
export * from "./course";
|
export * from './course'
|
||||||
|
|
||||||
export type { FetchError } from "ofetch";
|
export type { FetchError } from 'ofetch'
|
||||||
|
136
types/user.ts
136
types/user.ts
@ -1,77 +1,77 @@
|
|||||||
export type LoginType = "admin" | "teacher" | "student";
|
export type LoginType = 'admin' | 'teacher' | 'student'
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id?: number;
|
id?: number
|
||||||
createBy: number;
|
createBy: number
|
||||||
createTime: Date;
|
createTime: Date
|
||||||
updateBy: string;
|
updateBy: string
|
||||||
updateTime: string;
|
updateTime: string
|
||||||
remark: string;
|
remark: string
|
||||||
userId: number;
|
userId: number
|
||||||
deptId: number;
|
deptId: number
|
||||||
collegeName: string;
|
collegeName: string
|
||||||
schoolName: string;
|
schoolName: string
|
||||||
employeeId: string;
|
employeeId: string
|
||||||
schoolId: number;
|
schoolId: number
|
||||||
collegeId: number;
|
collegeId: number
|
||||||
userName: string;
|
userName: string
|
||||||
nickName: string;
|
nickName: string
|
||||||
email: string;
|
email: string
|
||||||
phonenumber: string;
|
phonenumber: string
|
||||||
sex: string;
|
sex: string
|
||||||
avatar: string | null;
|
avatar: string | null
|
||||||
password: string;
|
password: string
|
||||||
status: string;
|
status: string
|
||||||
delFlag: string;
|
delFlag: string
|
||||||
loginIp: string;
|
loginIp: string
|
||||||
loginDate: Date;
|
loginDate: Date
|
||||||
dept: IUserDept;
|
dept: IUserDept
|
||||||
roles: IUserRole[];
|
roles: IUserRole[]
|
||||||
roleIds: null;
|
roleIds: null
|
||||||
postIds: null;
|
postIds: null
|
||||||
roleId: null;
|
roleId: null
|
||||||
loginType: LoginType;
|
loginType: LoginType
|
||||||
admin: boolean;
|
admin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserDept {
|
export interface IUserDept {
|
||||||
createBy: null;
|
createBy: null
|
||||||
createTime: null;
|
createTime: null
|
||||||
updateBy: null;
|
updateBy: null
|
||||||
updateTime: null;
|
updateTime: null
|
||||||
remark: null;
|
remark: null
|
||||||
deptId: number;
|
deptId: number
|
||||||
parentId: number;
|
parentId: number
|
||||||
ancestors: string;
|
ancestors: string
|
||||||
deptName: string;
|
deptName: string
|
||||||
orderNum: number;
|
orderNum: number
|
||||||
leader: string;
|
leader: string
|
||||||
phone: null;
|
phone: null
|
||||||
email: null;
|
email: null
|
||||||
status: string;
|
status: string
|
||||||
delFlag: null;
|
delFlag: null
|
||||||
parentName: null;
|
parentName: null
|
||||||
children?: [];
|
children?: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserRole {
|
export interface IUserRole {
|
||||||
createBy: null;
|
createBy: null
|
||||||
createTime: null;
|
createTime: null
|
||||||
updateBy: null;
|
updateBy: null
|
||||||
updateTime: null;
|
updateTime: null
|
||||||
remark: null;
|
remark: null
|
||||||
roleId: number;
|
roleId: number
|
||||||
roleName: string;
|
roleName: string
|
||||||
roleKey: string;
|
roleKey: string
|
||||||
roleSort: number;
|
roleSort: number
|
||||||
dataScope: string;
|
dataScope: string
|
||||||
menuCheckStrictly: boolean;
|
menuCheckStrictly: boolean
|
||||||
deptCheckStrictly: boolean;
|
deptCheckStrictly: boolean
|
||||||
status: string;
|
status: string
|
||||||
delFlag: null;
|
delFlag: null
|
||||||
flag: boolean;
|
flag: boolean
|
||||||
menuIds: null;
|
menuIds: null
|
||||||
deptIds: null;
|
deptIds: null
|
||||||
permissions: null;
|
permissions: null
|
||||||
admin: boolean;
|
admin: boolean
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { NitroFetchOptions, NitroFetchRequest } from "nitropack";
|
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack'
|
||||||
import { FetchError } from "ofetch";
|
import { FetchError } from 'ofetch'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封装 HTTP 请求
|
* 封装 HTTP 请求
|
||||||
@ -11,29 +11,31 @@ import { FetchError } from "ofetch";
|
|||||||
*/
|
*/
|
||||||
export const http = async <T>(
|
export const http = async <T>(
|
||||||
url: string,
|
url: string,
|
||||||
options?: NitroFetchOptions<NitroFetchRequest>
|
options?: NitroFetchOptions<NitroFetchRequest>,
|
||||||
) => {
|
) => {
|
||||||
const loginState = useLoginState();
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const baseURL = runtimeConfig.public.baseURL as string;
|
const baseURL = runtimeConfig.public.baseURL as string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<T>(url, {
|
const data = await $fetch<T>(url, {
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${loginState.token}`,
|
'Authorization': `Bearer ${loginState.token}`,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
})
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
} catch (err: unknown) {
|
}
|
||||||
|
catch (err: unknown) {
|
||||||
if (err instanceof FetchError) {
|
if (err instanceof FetchError) {
|
||||||
throw err;
|
throw err
|
||||||
} else {
|
}
|
||||||
throw new FetchError("请求失败");
|
else {
|
||||||
|
throw new FetchError('请求失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user