feat:对接部分班级页面接口,修改了部分课程页面,对接了一部分接口

This commit is contained in:
yuk255 2025-09-12 22:59:14 +08:00
parent e645a190dd
commit 155db7a1e4
13 changed files with 2959 additions and 3010 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -50,9 +50,10 @@ export interface CreateCourseRequest {
max_enroll?: number | null
status?: number | null
question?: string | null
pause_exit: string
allow_speed: string
show_subtitle: string
pauseExit: string
allowSpeed: string
showSubtitle: string
categoryId?: number | string | null // 支持单个ID(number)或多个ID的逗号分隔字符串
}
// 编辑课程请求参数
@ -89,6 +90,37 @@ export interface CourseStudent {
enrollTime?: string
}
// 课程章节类型定义
export interface CourseSection {
id?: string
course_id?: string | null
name?: string | null
type?: number | null
sort_order?: number | null
parent_id?: string | null
level?: number | null
}
// 新建课程章节请求参数
export interface CreateCourseSectionRequest {
course_id?: string | null
name?: string | null
type?: number | null
sort_order?: number | null
parent_id?: string | null
level?: number | null
}
// 编辑课程章节请求参数
export interface EditCourseSectionRequest extends CreateCourseSectionRequest {
id: string
}
// 查询课程章节参数
export interface QueryCourseSectionParams {
keyword?: string // 关键词模糊查询
}
/**
* API模块
*/
@ -124,6 +156,19 @@ export class TeachCourseApi {
}
}
/**
*
*/
static async getTeacherList(): Promise<ApiResponseWithResult<any>> {
try {
const response = await ApiRequest.get<{ result: any[] }>('/aiol/aiolUser/teachers')
return response
} catch (error) {
console.error('❌ 查询教师列表失败:', error)
throw error
}
}
/**
*
*/
@ -160,6 +205,14 @@ export class TeachCourseApi {
}
}
/**
*
*/
// static async deleteCourse(id: string): Promise<ApiResponse<any>> {
// }
/**
*
*/
@ -224,6 +277,80 @@ export class TeachCourseApi {
throw error
}
}
/**
*
*/
static async createCourseSection(data: CreateCourseSectionRequest): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送新建课程章节请求:', { url: '/aiol/aiolCourseSection/add', data })
const response = await ApiRequest.post<any>('/aiol/aiolCourseSection/add', data)
console.log('📚 新建课程章节响应:', response)
return response
} catch (error) {
console.error('❌ 新建课程章节失败:', error)
throw error
}
}
/**
*
*/
static async editCourseSection(data: EditCourseSectionRequest): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送编辑课程章节请求:', { url: '/aiol/aiolCourseSection/edit', data })
const response = await ApiRequest.put<any>('/aiol/aiolCourseSection/edit', data)
console.log('✏️ 编辑课程章节响应:', response)
return response
} catch (error) {
console.error('❌ 编辑课程章节失败:', error)
throw error
}
}
/**
*
*/
static async deleteCourseSection(id: string): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送删除课程章节请求:', { url: '/aiol/aiolCourseSection/delete', id })
const response = await ApiRequest.delete<any>('/aiol/aiolCourseSection/delete', {
params: { id }
})
console.log('🗑️ 删除课程章节响应:', response)
return response
} catch (error) {
console.error('❌ 删除课程章节失败:', error)
throw error
}
}
/**
*
*/
static async getCourseSections(courseId: string, params?: QueryCourseSectionParams): Promise<ApiResponseWithResult<CourseSection[]>> {
try {
console.log('🚀 发送查询课程章节请求:', {
url: `/aiol/aiolCourse/${courseId}/section`,
courseId,
params
})
const response = await ApiRequest.get<{ result: CourseSection[] }>(`/aiol/aiolCourse/${courseId}/section`, params)
console.log('📑 课程章节列表响应:', response)
return response
} catch (error) {
console.error('❌ 查询课程章节列表失败:', error)
throw error
}
}
}
// 默认导出
@ -246,6 +373,14 @@ export interface ImportStudentsRequest {
ids: string; // 逗号分隔的学生id
}
export interface CreatedStudentsRequest {
realName: string;
studentNumber: string;
password: string;
school: string;
classId: string;
}
export class ClassApi {
/**
*
@ -254,6 +389,10 @@ export class ClassApi {
return ApiRequest.post('/aiol/aiolClass/add', data);
}
static async queryClassList(params: { course_id: string|null }): Promise<ApiResponse<any>> {
return ApiRequest.get('/aiol/aiolClass/query_list', params);
}
/**
*
*/
@ -265,9 +404,17 @@ export class ClassApi {
*
*/
static async deleteClass(id: string): Promise<ApiResponse<any>> {
return ApiRequest.delete('/aiol/aiolClass/delete', { params: { id } });
return ApiRequest.delete('/aiol/aiolClass/delete', { id });
}
/**
*
*/
static async createdStudents(data: CreatedStudentsRequest): Promise<ApiResponse<any>> {
return ApiRequest.post(`/aiol/aiolClass/create_and_add_student`, data);
}
/**
*
*/

View File

@ -40,6 +40,8 @@ export class UploadApi {
onProgress?: (progress: number) => void
): Promise<ApiResponse<{
url: string
message: string
success: boolean
filename: string
size: number
}>> {
@ -49,7 +51,7 @@ export class UploadApi {
formData.append('courseId', courseId.toString())
}
return ApiRequest.post('/upload/course-thumbnail', formData, {
return ApiRequest.post('/sys/common/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},

View File

@ -65,7 +65,7 @@
</div>
<!-- 课程列表 -->
<div v-else class="course-grid">
<div v-if="courseList.length > 0" class="course-grid" key="course-list">
<div class="course-card" v-for="course in courseList" :key="course.id">
<div class="course-image-container">
<div class="section-title" :class="{ 'offline': course.status === 0 }">{{ course.statusText }}
@ -123,6 +123,7 @@ import { useRouter } from 'vue-router';
import { EllipsisVerticalSharp, Refresh } from '@vicons/ionicons5';
import { useMessage, useDialog } from 'naive-ui';
import TeachCourseApi, { type TeachCourse } from '@/api/modules/teachCourse';
import { useCourseStore } from '@/stores/course';
//
interface CourseDisplayItem extends TeachCourse {
@ -133,6 +134,7 @@ interface CourseDisplayItem extends TeachCourse {
const router = useRouter();
const message = useMessage();
const dialog = useDialog();
const courseStore = useCourseStore();
//
const originalCourseList = ref<CourseDisplayItem[]>([]);
@ -144,9 +146,11 @@ const loading = ref<boolean>(false);
const error = ref<string>('');
//
const getCourseList = async (forceRefresh: boolean = false) => {
const getCourseList = async (forceRefresh: boolean = false, showLoading: boolean = true) => {
try {
if (showLoading) {
loading.value = true;
}
error.value = ''; //
const params = {
@ -202,8 +206,10 @@ const getCourseList = async (forceRefresh: boolean = false) => {
//
message.error(error.value);
} finally {
if (showLoading) {
loading.value = false;
}
}
};
//
@ -249,8 +255,13 @@ const activeTab = ref<string>('ongoing')
//
watch(activeTab, async (newTab, oldTab) => {
console.log('📋 Tab切换:', oldTab, '->', newTab);
// tab
await getCourseList(true);
//
courseList.value = [];
originalCourseList.value = [];
// tab
await getCourseList(true, false);
});
//
@ -278,14 +289,14 @@ const getOptionsForCourse = (course: CourseDisplayItem) => {
return [
{ label: '下架', value: 'offline', icon: '/images/teacher/下架.png' },
{ label: '编辑', value: 'edit', icon: '/images/teacher/小编辑.png' },
{ label: '移动', value: 'move', icon: '/images/teacher/移动.png' },
// { label: '', value: 'move', icon: '/images/teacher/.png' },
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
];
} else if (course.status === 0) { // /稿
return [
{ label: '发布', value: 'publish', icon: '/images/teacher/加号.png' },
{ label: '编辑', value: 'edit', icon: '/images/teacher/小编辑.png' },
{ label: '移动', value: 'move', icon: '/images/teacher/移动.png' },
// { label: '', value: 'move', icon: '/images/teacher/.png' },
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
];
} else if (course.status === 2) { //
@ -324,7 +335,13 @@ const handleOptionSelect = (value: string, course: any) => {
// value
switch (value) {
case 'edit':
// -
// - store
console.log('✏️ 编辑课程,准备数据:', course);
// store
courseStore.setCourseEditData(course);
// ID
router.push(`/teacher/course-create/${course.id}`);
break;
case 'delete':
@ -339,10 +356,10 @@ const handleOptionSelect = (value: string, course: any) => {
//
handlePublishCourse(course);
break;
case 'move':
//
handleMoveCourse(course);
break;
// case 'move':
// //
// handleMoveCourse(course);
// break;
default:
break;
}
@ -416,9 +433,10 @@ const handleOfflineCourse = (course: CourseDisplayItem) => {
name: course.name,
description: course.description,
status: 2, // 2=
pause_exit: '1',
allow_speed: '1',
show_subtitle: '1'
//
pauseExit: '1',
allowSpeed: '1',
showSubtitle: '1'
};
await TeachCourseApi.editCourse(updatedData);
@ -456,56 +474,56 @@ const handlePublishCourse = (course: CourseDisplayItem) => {
};
//
const handleMoveCourse = (course: any) => {
const currentIndex = courseList.value.findIndex(c => c.id === course.id);
const totalCourses = courseList.value.length;
// const handleMoveCourse = (course: any) => {
// const currentIndex = courseList.value.findIndex(c => c.id === course.id);
// const totalCourses = courseList.value.length;
dialog.create({
title: '移动课程位置',
content: () => h('div', [
h('p', `课程"${course.name}"当前位置:第 ${currentIndex + 1}`),
h('p', { style: 'margin-top: 10px; margin-bottom: 10px;' }, '移动到位置:'),
h('input', {
type: 'number',
min: 1,
max: totalCourses,
value: currentIndex + 1,
style: 'width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
placeholder: `请输入位置 (1-${totalCourses})`,
id: 'movePositionInput'
}),
h('p', {
style: 'margin-top: 8px; font-size: 12px; color: #666;'
}, `提示:输入 1-${totalCourses} 之间的数字`)
]),
positiveText: '确定移动',
negativeText: '取消',
onPositiveClick: () => {
const input = document.getElementById('movePositionInput') as HTMLInputElement;
const newPosition = parseInt(input.value);
// dialog.create({
// title: '',
// content: () => h('div', [
// h('p', `"${course.name}" ${currentIndex + 1} `),
// h('p', { style: 'margin-top: 10px; margin-bottom: 10px;' }, ''),
// h('input', {
// type: 'number',
// min: 1,
// max: totalCourses,
// value: currentIndex + 1,
// style: 'width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
// placeholder: ` (1-${totalCourses})`,
// id: 'movePositionInput'
// }),
// h('p', {
// style: 'margin-top: 8px; font-size: 12px; color: #666;'
// }, ` 1-${totalCourses} `)
// ]),
// positiveText: '',
// negativeText: '',
// onPositiveClick: () => {
// const input = document.getElementById('movePositionInput') as HTMLInputElement;
// const newPosition = parseInt(input.value);
if (isNaN(newPosition) || newPosition < 1 || newPosition > totalCourses) {
message.error(`请输入有效的位置 (1-${totalCourses})`);
return false; //
}
// if (isNaN(newPosition) || newPosition < 1 || newPosition > totalCourses) {
// message.error(` (1-${totalCourses})`);
// return false; //
// }
//
const targetIndex = newPosition - 1;
if (targetIndex !== currentIndex) {
//
const [movedCourse] = courseList.value.splice(currentIndex, 1);
//
courseList.value.splice(targetIndex, 0, movedCourse);
// //
// const targetIndex = newPosition - 1;
// if (targetIndex !== currentIndex) {
// //
// const [movedCourse] = courseList.value.splice(currentIndex, 1);
// //
// courseList.value.splice(targetIndex, 0, movedCourse);
message.success(`课程"${course.name}"已移动到第 ${newPosition}`);
} else {
message.info('位置未发生变化');
}
// message.success(`"${course.name}" ${newPosition} `);
// } else {
// message.info('');
// }
return true; //
}
});
};
// return true; //
// }
// });
// };
onMounted(() => {

View File

@ -24,6 +24,12 @@
<n-input v-model:value="formData.courseName" placeholder="请输入课程名称" class="form-input" />
</div>
<!-- 课程开始时间 -->
<div class="form-item">
<label class="form-label required">课程开始时间:</label>
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="选择时间" class="form-input" />
</div>
<!-- 主讲老师 -->
<div class="form-item">
<label class="form-label required">主讲老师:</label>
@ -31,14 +37,6 @@
class="form-input" />
</div>
<!-- 课程开始时间 -->
<div class="form-item">
<label class="form-label required">课程开始时间:</label>
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="选择时间" class="form-input" />
</div>
<!-- 参与学员 -->
<div class="form-item">
<label class="form-label required">参与学员:</label>
@ -57,7 +55,8 @@
</div>
<!-- 选择班级 -->
<div class="form-item">
<div class="form-item" v-show="formData.studentType === 'partial'">
<label class="form-label required">选择班级:</label>
<n-select v-model:value="formData.selectedClasses" multiple :options="classOptions"
placeholder="选择班级(可多选)" class="form-input" />
</div>
@ -68,15 +67,15 @@
<!-- 课程分类 -->
<div class="form-item">
<label class="form-label required">课程分类:</label>
<n-select v-model:value="formData.courseCategory" :options="categoryOptions" placeholder="分类名称"
<n-select v-model:value="formData.courseCategory" multiple :options="categoryOptions" placeholder="分类名称"
class="form-input" />
</div>
<!-- 排序 -->
<div class="form-item">
<!-- <div class="form-item">
<label class="form-label required">排序:</label>
<n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" />
</div>
</div> -->
<!-- 课程结束时间 -->
<div class="form-item">
@ -173,7 +172,7 @@
</template>
<script setup lang="ts">
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed } from 'vue'
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed, Ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useMessage } from 'naive-ui'
import { ArrowBackOutline } from '@vicons/ionicons5';
@ -188,11 +187,15 @@ import {
import '@wangeditor/editor/dist/css/style.css'
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
// import TeachCourseApi from '@/api/modules/teachCourse'
import TeachCourseApi from '@/api/modules/teachCourse'
import UploadApi from '@/api/modules/upload'
import CourseApi from '@/api/modules/course';
import { useCourseStore } from '@/stores/course'
const router = useRouter()
const route = useRoute()
const message = useMessage()
const courseStore = useCourseStore()
//
const isEditMode = computed(() => !!route.params.id)
@ -208,6 +211,9 @@ const editorRef = shallowRef()
const fileInput = ref<HTMLInputElement>()
const previewUrl = ref('')
//
const pendingCourseDescription = ref('')
const toolbarConfig = {}
const editorConfig = { placeholder: '请输入内容...' }
const mode = 'default'
@ -221,126 +227,179 @@ onBeforeUnmount(() => {
const handleCreated = (editor: any) => {
editorRef.value = editor // editor
//
if (pendingCourseDescription.value) {
editor.setHtml(pendingCourseDescription.value);
formData.courseDescription = pendingCourseDescription.value;
pendingCourseDescription.value = ''; //
}
}
//
const formData = reactive({
courseName: '',
courseCategory: null, // null placeholder
instructors: [],
sort: null, // null placeholder
startTime: null,
endTime: null,
studentType: 'all', // 'all' 'partial'
selectedClasses: [],
courseCategory: [] as string[] | number[], // null placeholder
instructors: [] as string[],
// sort: null as string | null, // null placeholder
startTime: null as number | null,
endTime: null as number | null,
studentType: 'all' as 'all' | 'partial', // 'all' 'partial'
selectedClasses: [] as string[],
courseCover: null as File | null,
courseDescription: '',
//
stopOnLeave: true,
stopOnLeave: false,
videoSpeedControl: false,
showVideoText: true,
showVideoText: false,
//
pointsEnabled: true,
earnPoints: 60,
requiredPoints: 60
})
//
const mockCourseData = {
1: {
courseName: '前端开发基础课程',
courseCategory: 'frontend',
instructors: ['李清林', '张老师'],
sort: '1',
startTime: new Date('2024-01-15 09:00:00').getTime(),
endTime: new Date('2024-03-15 18:00:00').getTime(),
studentType: 'partial',
selectedClasses: ['frontend-class', 'fullstack-class'],
courseDescription: '<p>这是一门全面的前端开发基础课程涵盖HTML、CSS、JavaScript等核心技术。</p><p>课程特色:</p><ul><li>理论与实践相结合</li><li>项目驱动学习</li><li>一对一指导</li></ul>',
stopOnLeave: true,
videoSpeedControl: true,
showVideoText: false,
pointsEnabled: true,
earnPoints: 80,
requiredPoints: 50,
courseCover: '/images/teacher/fj.png'
},
2: {
courseName: 'Vue.js 实战教程',
courseCategory: 'frontend',
instructors: ['刘树光'],
sort: '2',
startTime: new Date('2024-02-01 10:00:00').getTime(),
endTime: new Date('2024-04-01 17:00:00').getTime(),
studentType: 'all',
selectedClasses: [],
courseDescription: '<p>深入学习Vue.js框架掌握现代前端开发技能。</p>',
stopOnLeave: false,
videoSpeedControl: true,
showVideoText: true,
pointsEnabled: true,
earnPoints: 100,
requiredPoints: 60,
courseCover: '/images/teacher/fj.png'
}
}
//
const loadCourseData = async (id: string) => {
const loadCourseData = async () => {
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
// store
const storeCourseData = courseStore.courseEditData;
const courseData = mockCourseData[id as unknown as keyof typeof mockCourseData]
if (courseData) {
//
Object.assign(formData, courseData)
if (storeCourseData) {
//
formData.courseName = storeCourseData.name || storeCourseData.courseName || '';
// 使
const categoryData = storeCourseData.categoryId || storeCourseData.courseCategory;
if (typeof categoryData === 'string' && categoryData) {
formData.courseCategory = categoryData.split(',').map(id => Number(id));
} else if (Array.isArray(categoryData)) {
formData.courseCategory = categoryData;
} else {
formData.courseCategory = [];
}
formData.instructors = storeCourseData.instructors || [];
formData.startTime = storeCourseData.start_time || storeCourseData.startTime || null;
formData.endTime = storeCourseData.end_time || storeCourseData.endTime || null;
formData.studentType = (storeCourseData.type === 1 || storeCourseData.studentType === 'partial') ? 'partial' : 'all';
formData.selectedClasses = storeCourseData.target ? storeCourseData.target.split(',') : (storeCourseData.selectedClasses || []);
const tempCourseDescription = storeCourseData.description || storeCourseData.courseDescription || '';
if (tempCourseDescription) {
if (editorRef.value) {
editorRef.value.setHtml(tempCourseDescription);
formData.courseDescription = tempCourseDescription;
} else {
pendingCourseDescription.value = tempCourseDescription;
}
}
// 0/1
formData.stopOnLeave = storeCourseData.pauseExit !== undefined ?
Boolean(Number(storeCourseData.pauseExit)) : true;
formData.videoSpeedControl = storeCourseData.allowSpeed !== undefined ?
Boolean(Number(storeCourseData.allowSpeed)) : false;
formData.showVideoText = storeCourseData.showSubtitle !== undefined ?
Boolean(Number(storeCourseData.showSubtitle)) : true;
//
formData.pointsEnabled = storeCourseData.pointsEnabled !== undefined ? storeCourseData.pointsEnabled : true;
formData.earnPoints = storeCourseData.earnPoints || 60;
formData.requiredPoints = storeCourseData.requiredPoints || 60;
if (storeCourseData.cover || storeCourseData.courseCover) {
previewUrl.value = storeCourseData.cover || storeCourseData.courseCover;
formData.courseCover = null;
}
message.success('课程数据加载成功');
return; // storeAPI
}
// store
const routeCourseData = route.query.courseData;
if (routeCourseData) {
try {
const courseData = JSON.parse(routeCourseData as string);
//
formData.courseName = courseData.name || courseData.courseName || '';
// 使
const categoryData = courseData.categoryId || courseData.courseCategory;
if (typeof categoryData === 'string' && categoryData) {
formData.courseCategory = categoryData.split(',').map(id => Number(id));
} else if (Array.isArray(categoryData)) {
formData.courseCategory = categoryData;
} else {
formData.courseCategory = [];
}
formData.instructors = courseData.instructors || [];
formData.startTime = courseData.start_time || courseData.startTime || null;
formData.endTime = courseData.end_time || courseData.endTime || null;
formData.studentType = (courseData.type === 1 || courseData.studentType === 'partial') ? 'partial' : 'all';
formData.selectedClasses = courseData.target ? courseData.target.split(',') : (courseData.selectedClasses || []);
//
const tempCourseDescription = courseData.description || courseData.courseDescription || '';
if (tempCourseDescription) {
if (editorRef.value) {
//
editorRef.value.setHtml(tempCourseDescription);
formData.courseDescription = tempCourseDescription;
} else {
//
pendingCourseDescription.value = tempCourseDescription;
}
}
// 0/1
formData.stopOnLeave = courseData.pauseExit !== undefined ?
Boolean(Number(courseData.pauseExit)) : true;
formData.videoSpeedControl = courseData.allowSpeed !== undefined ?
Boolean(Number(courseData.allowSpeed)) : false;
formData.showVideoText = courseData.showSubtitle !== undefined ?
Boolean(Number(courseData.showSubtitle)) : true;
// 使
formData.pointsEnabled = courseData.pointsEnabled !== undefined ? courseData.pointsEnabled : true;
formData.earnPoints = courseData.earnPoints || 60;
formData.requiredPoints = courseData.requiredPoints || 60;
// URL
if (courseData.courseCover) {
previewUrl.value = courseData.courseCover
if (courseData.cover || courseData.courseCover) {
previewUrl.value = courseData.cover || courseData.courseCover;
formData.courseCover = null; // URL
}
message.success('课程数据加载成功')
} else {
message.error('课程不存在')
router.back()
message.success('课程数据加载成功');
return; // API
} catch (parseError) {
// 使
}
}
// store
} catch (error) {
message.error('加载课程数据失败')
console.error('Load course data error:', error)
}
}
//
onMounted(() => {
// courseIdstore
if (isEditMode.value && courseId.value) {
loadCourseData(courseId.value)
loadCourseData()
} else if (route.query.courseData || courseStore.courseEditData) {
// 使IDstore
loadCourseData()
}
})
//
const categoryOptions = [
{ label: '前端开发', value: 'frontend' },
{ label: '后端开发', value: 'backend' },
{ label: '移动开发', value: 'mobile' },
{ label: '人工智能', value: 'ai' },
{ label: '数据库', value: 'database' },
{ label: '运维部署', value: 'devops' },
{ label: '测试', value: 'testing' },
{ label: '产品设计', value: 'design' }
]
const categoryOptions: Ref<{ label: string; value: number }[]> = ref([])
//
const instructorOptions = [
{ label: '李清林', value: '李清林' },
{ label: '刘树光', value: '刘树光' },
{ label: '肖蒙', value: '肖蒙' },
{ label: '张老师', value: '张老师' },
{ label: '王老师', value: '王老师' }
]
const instructorOptions = ref([] as { label: string; value: string }[])
//
const classOptions = [
@ -350,20 +409,6 @@ const classOptions = [
{ label: '全栈开发班', value: 'fullstack-class' }
]
//
// const sortOptions = [
// { label: '1', value: '1' },
// { label: '2', value: '2' },
// { label: '3', value: '3' },
// { label: '4', value: '4' },
// { label: '5', value: '5' },
// { label: '6', value: '6' },
// { label: '7', value: '7' },
// { label: '8', value: '8' },
// { label: '9', value: '9' },
// { label: '10', value: '10' }
// ]
//
const triggerFileUpload = () => {
fileInput.value?.click()
@ -388,15 +433,14 @@ const handleFileChange = (event: Event) => {
// URL
previewUrl.value = URL.createObjectURL(file)
//
formData.courseCover = file
console.log('文件上传成功:', file.name)
}
}
const clearUpload = () => {
// URL
if (previewUrl.value) {
// previewUrlURL
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value)
}
@ -412,9 +456,25 @@ const clearUpload = () => {
//
const handleCancel = () => {
//
courseStore.clearCourseEditData();
router.back()
}
// YYYY-MM-DD HH:mm:ss
const formatDateTime = (timestamp: number): string => {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
const handleSubmit = async () => {
try {
@ -424,7 +484,7 @@ const handleSubmit = async () => {
return
}
if (!formData.courseCategory) {
if (!formData.courseCategory || formData.courseCategory.length === 0) {
message.error('请选择课程分类')
return
}
@ -434,37 +494,155 @@ const handleSubmit = async () => {
return
}
if (!formData.startTime) {
message.error('请选择课程开始时间')
return
}
if (!formData.endTime) {
message.error('请选择课程结束时间')
return
}
if (!formData.courseCover && !previewUrl.value) {
message.error('请上传课程封面')
return
}
console.log('表单数据:', formData)
//
if (formData.studentType === 'partial' && formData.selectedClasses.length === 0) {
message.error('请选择参与的班级')
return
}
// API
await new Promise(resolve => setTimeout(resolve, 1000))
message.loading('正在保存课程信息...')
let coverUrl = previewUrl.value
// File
// courseCover File
// courseCover null previewUrl 使
if (formData.courseCover && formData.courseCover instanceof File) {
try {
const uploadResponse = await UploadApi.uploadCourseThumbnail(formData.courseCover)
if (uploadResponse.data.success) {
coverUrl = uploadResponse.data.message
} else {
message.error('课程封面上传失败')
return
}
} catch (uploadError) {
message.error('课程封面上传失败,请重试')
return
}
}
// API
const createCourseData = {
name: formData.courseName,
cover: coverUrl,
description: formData.courseDescription,
type: formData.studentType === 'all' ? 0 : 1,
categoryId: Array.isArray(formData.courseCategory) ? formData.courseCategory.join(',') : formData.courseCategory,
target: formData.studentType === 'all' ? '' : formData.selectedClasses.join(','), // target
start_time: formData.startTime ? formatDateTime(formData.startTime) : null,
end_time: formData.endTime ? formatDateTime(formData.endTime) : null,
pauseExit: formData.stopOnLeave ? '1' : '0', //
allowSpeed: formData.videoSpeedControl ? '1' : '0', //
showSubtitle: formData.showVideoText ? '1' : '0', //
//
school: null,
video: null,
difficulty: null,
subject: null,
outline: null,
prerequisite: null,
reference: null,
arrangement: null,
enroll_count: null,
max_enroll: null,
status: 1, //
question: null
}
if (isEditMode.value) {
//
message.success('课程更新成功!')
console.log('更新课程ID:', courseId.value)
} else {
//
message.success('课程创建成功!')
const editData = {
id: courseId.value,
...createCourseData
}
const response = await TeachCourseApi.editCourse(editData)
if (response.data.code === 200) {
message.success('课程更新成功!')
//
courseStore.clearCourseEditData();
//
router.push('/teacher/course-management')
} else {
message.error(response.message || '更新失败,请重试')
}
} else {
//
const response = await TeachCourseApi.createCourse(createCourseData)
if (response.data.code === 200) {
message.success('课程创建成功!')
//
courseStore.clearCourseEditData();
//
router.push('/teacher/course-management')
} else {
message.error(response.message || '创建失败,请重试')
}
}
} catch (error) {
console.error('提交失败:', error)
const errorMessage = isEditMode.value ? '更新失败,请重试' : '创建失败,请重试'
message.error(errorMessage)
}
}
const goBack = () => {
//
courseStore.clearCourseEditData();
router.back()
}
//
const getCourseList = () => {
CourseApi.getCategories().then(response => {
categoryOptions.value = response.data.map((category: any) => ({
label: category.name,
value: category.id
}))
//
if (formData.courseCategory.length === 0 && categoryOptions.value.length > 0) {
formData.courseCategory = [];
}
}).catch(error => {
console.error('获取课程列表失败:', error)
})
}
//
const getTeacherList = () => {
TeachCourseApi.getTeacherList().then(response => {
instructorOptions.value = response.data.result.map((teacher: any) => ({
label: teacher.realname,
value: teacher.id
}))
}).catch(error => {
console.error('获取老师列表失败:', error)
})
}
onMounted(() => {
getCourseList()
getTeacherList()
})
</script>
<style scoped>

View File

@ -6,22 +6,29 @@
<h1>我的资源</h1>
</div>
<div class="resources-actions">
<button class="upload-btn" @click="handleUpload">
<n-button type="primary" @click="handleUpload">
上传文件
</button>
<button class="new-folder-btn" @click="handleNewFolder">
</n-button>
<n-button @click="handleNewFolder">
新建文件夹
</button>
<button class="recycle-bin-btn" @click="handleRecycleBin">
</n-button>
<n-button @click="handleRecycleBin">
<template #icon>
<img src="/images/teacher/delete2.png" alt="回收站" class="action-icon">
</template>
回收站
</button>
</n-button>
<div class="search-container">
<input type="text" class="search-input" placeholder="请输入关键字" v-model="searchKeyword"
<n-input
v-model:value="searchKeyword"
placeholder="请输入关键字"
@keyup.enter="handleSearch">
<button class="search-btn" @click="handleSearch">
搜索
</button>
<template #suffix>
<n-icon>
<Search />
</n-icon>
</template>
</n-input>
</div>
</div>
</div>
@ -30,27 +37,19 @@
<!-- 文件网格 -->
<div class="files-grid">
<div v-for="file in filteredFiles" :key="file.id" class="file-item"
@contextmenu.prevent="handleRightClick(file, $event)" @mouseenter="hoveredFile = file.id"
@mouseenter="hoveredFile = file.id"
@mouseleave="handleItemMouseLeave">
<!-- 文件操作菜单 -->
<div class="file-menu">
<button class="file-menu-btn" @click.stop="toggleMenu(file.id)">
<n-popselect
:options="fileMenuOptions"
@update:value="(value: string) => handleFileMenuSelect(file, value)"
trigger="click"
placement="bottom-end">
<n-button text @click.stop>
<img src="/images/profile/more.png" alt="更多操作" class="more-icon">
</button>
<div class="file-menu-dropdown" v-if="showMenuFor === file.id">
<div class="menu-item" @click="handleEdit(file)">
<img class="menu-icon" src="/images/teacher/edit.png" alt="编辑">
<span>编辑</span>
</div>
<div class="menu-item" @click="handleMove(file)">
<img class="menu-icon" src="/images/teacher/移动.png" alt="移动">
<span>移动</span>
</div>
<div class="menu-item delete" @click="handleDelete(file)">
<img class="menu-icon" src="/images/teacher/删除.png.png" alt="删除">
<span>删除</span>
</div>
</div>
</n-button>
</n-popselect>
</div>
<!-- 文件图标 -->
@ -62,8 +61,14 @@
<div v-if="editingId !== file.id" class="file-name" :title="file.name" @click.stop="startEdit(file)">
{{ file.name }}
</div>
<input v-else class="file-name-input" type="text" v-model="editName" @keyup.enter="saveEdit(file)"
@keyup.esc="cancelEdit" @blur="saveEdit(file)" :maxlength="50" autofocus />
<n-input v-else
v-model:value="editName"
@keyup.enter="saveEdit(file)"
@keyup.esc="cancelEdit"
@blur="saveEdit(file)"
:maxlength="50"
autofocus
class="file-name-input" />
<!-- 文件详情由全局定位卡片显示 -->
</div>
@ -80,8 +85,8 @@
<template #body>
<div style="display:flex; align-items:center; gap:12px;">
<span style="width:90px; text-align:right; color:#333; font-size:14px;">文件夹名称</span>
<input type="text" v-model="newFolderName" placeholder="请输入文件夹名称"
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;">
<n-input v-model:value="newFolderName" placeholder="请输入文件夹名称"
style="flex:1;" />
</div>
</template>
</RecycleConfirmModal>
@ -99,11 +104,8 @@
<template #body>
<div style="display:flex; align-items:center; gap:12px;">
<span style="width:90px; text-align:right; color:#333; font-size:14px;">选择文件夹</span>
<select v-model="moveTargetFolder"
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;">
<option value="">请选择文件夹</option>
<option v-for="f in folderFiles" :key="f.id" :value="f.name">{{ f.name }}</option>
</select>
<n-select v-model:value="moveTargetFolder" placeholder="请选择文件夹"
:options="folderOptions" style="flex:1;" />
</div>
</template>
</RecycleConfirmModal>
@ -112,10 +114,12 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, h } from 'vue'
import { NButton, NInput, NSelect, NPopselect } from 'naive-ui'
import FileInfoCard from '@/components/admin/FileInfoCard.vue'
import UploadFileModal from '@/views/teacher/course/UploadFileModal.vue'
import RecycleConfirmModal from '@/views/teacher/resource/RecycleConfirmModal.vue'
import { Search } from '@vicons/ionicons5'
//
interface FileItem {
@ -129,7 +133,6 @@ interface FileItem {
//
const searchKeyword = ref('')
const showMenuFor = ref<string | null>(null)
const hoveredFile = ref<string | null>(null)
const showNewFolderModal = ref(false)
const showUploadModal = ref(false)
@ -143,7 +146,29 @@ const infoCardPosition = ref<{ top: number; left: number }>({ top: 0, left: 0 })
const showMoveModal = ref(false)
const moveTargetFolder = ref('')
const folderFiles = computed(() => files.value.filter((f: FileItem) => f.type === 'folder'))
const folderOptions = computed(() => files.value.filter((f: FileItem) => f.type === 'folder').map(f => ({
label: f.name,
value: f.name
})))
//
const fileMenuOptions = [
{
label: '编辑',
value: 'edit',
icon: () => h('img', { src: '/images/teacher/edit.png', alt: '编辑', class: 'menu-icon' })
},
{
label: '移动',
value: 'move',
icon: () => h('img', { src: '/images/teacher/移动.png', alt: '移动', class: 'menu-icon' })
},
{
label: '删除',
value: 'delete',
icon: () => h('img', { src: '/images/teacher/删除.png.png', alt: '删除', class: 'menu-icon' })
}
]
//
const files = ref<FileItem[]>([
@ -195,23 +220,12 @@ const formatDate = (dateString: string) => {
//
const handleRightClick = (file: FileItem, event: MouseEvent) => {
event.preventDefault()
showMenuFor.value = file.id
}
const toggleMenu = (fileId: string) => {
showMenuFor.value = showMenuFor.value === fileId ? null : fileId
}
const handleEdit = (file: FileItem) => {
console.log('编辑文件:', file.name)
showMenuFor.value = null
}
const handleMove = (file: FileItem) => {
console.log('移动文件:', file.name)
showMenuFor.value = null
showMoveModal.value = true
}
@ -222,7 +236,21 @@ const handleDelete = (file: FileItem) => {
files.value.splice(index, 1)
}
}
showMenuFor.value = null
}
//
const handleFileMenuSelect = (file: FileItem, value: string) => {
switch (value) {
case 'edit':
handleEdit(file)
break
case 'move':
handleMove(file)
break
case 'delete':
handleDelete(file)
break
}
}
const handleSearch = () => {
@ -275,9 +303,7 @@ const closeUploadModal = () => {
//
onMounted(() => {
document.addEventListener('click', () => {
showMenuFor.value = null
})
// 使 Popselect
})
//
@ -355,99 +381,14 @@ const handleItemMouseLeave = () => {
gap: 12px;
}
.upload-btn,
.recycle-bin-btn {
padding: 6px 16px;
border: 1px solid #999999;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
background: white;
color: #999999;
min-width: 100px;
text-align: center;
height: 32px;
}
.new-folder-btn {
padding: 6px 16px;
border: 1px solid #0288D1;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
background: white;
color: #0288D1;
min-width: 100px;
text-align: center;
height: 32px;
}
.upload-btn {
background: #0288D1;
color: white;
}
.upload-btn:hover {
background: #0277BD;
}
.recycle-bin-btn:hover {
background-color: #f5f8fb;
}
.new-folder-btn:hover {
background: #F5F8FB;
color: #0288D1;
}
/* 回收站按钮图标与间距 */
.recycle-bin-btn {
display: inline-flex;
align-items: center;
gap: 6px;
}
.recycle-bin-btn .action-icon {
.action-icon {
width: 14px;
height: 14px;
display: inline-block;
}
.search-container {
display: flex;
align-items: center;
background: white;
border: 1px solid #D9D9D9;
border-radius: 4px;
overflow: hidden;
height: 32px;
}
.search-input {
padding: 6px 12px;
border: none;
outline: none;
width: 180px;
font-size: 14px;
height: 30px;
}
.search-btn {
padding: 6px 16px;
background: #0288D1;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s ease;
font-size: 14px;
height: 32px;
}
.search-btn:hover {
background: #0277BD;
width: 200px;
}
@ -488,57 +429,11 @@ const handleItemMouseLeave = () => {
z-index: 10;
}
.file-menu-btn {
width: 6px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.more-icon {
width: 4px;
height: 12px;
}
.file-menu-dropdown {
position: absolute;
top: calc(100% + 6px);
right: -20px;
background: white;
border-radius: 4px;
box-shadow: 0px 2px 60px 0px rgba(220, 220, 220, 0.74);
width: 60px;
height: 81px;
z-index: 1200;
}
.menu-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 6px 6px;
cursor: pointer;
font-size: 10px;
color: #000;
transition: background 0.2s ease;
}
.menu-item:hover {
background: #F5F5F5;
}
.menu-item.delete {
color: #000;
}
.file-icon {}
.menu-icon {
width: 12px;
height: 12px;
@ -564,21 +459,7 @@ const handleItemMouseLeave = () => {
.file-name-input {
margin-top: -20px;
width: 100%;
max-width: 140px;
height: 36px;
box-sizing: border-box;
padding: 4px 12px;
border: 1.5px solid #D8D8D8;
background: #F5F8FB;
color: #0288D1;
font-size: 14px;
outline: none;
text-align: center;
}
.file-name-input::placeholder {
color: #0288D1;
}
.file-details {
position: absolute;
@ -624,90 +505,6 @@ const handleItemMouseLeave = () => {
z-index: 2000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
min-width: 400px;
max-width: 500px;
}
.modal-content h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
}
.modal-content input {
width: 100%;
padding: 10px 12px;
border: 1px solid #D9D9D9;
border-radius: 6px;
font-size: 14px;
outline: none;
margin-bottom: 20px;
}
.upload-area {
border: 2px dashed #D9D9D9;
border-radius: 6px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s ease;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #0288D1;
}
.upload-icon {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
.upload-area p {
margin: 0;
color: #666;
font-size: 14px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-btn,
.confirm-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.cancel-btn {
background: #F5F5F5;
color: #666;
}
.cancel-btn:hover {
background: #E6E6E6;
}
.confirm-btn {
background: #0288D1;
color: white;
}
.confirm-btn:hover {
background: #0277BD;
}
/* 响应式设计 */
@media (max-width: 768px) {
.resources-header {

View File

@ -211,11 +211,11 @@
clearable
/>
</n-form-item>
<n-form-item label="所在学" path="college">
<n-form-item label="所在学" path="college">
<n-select
v-model:value="formData.college"
:options="collegeOptions"
placeholder="请选择学"
placeholder="请选择学"
clearable
/>
</n-form-item>
@ -351,6 +351,14 @@ const props = withDefaults(defineProps<Props>(), {
classId: null
})
//
interface Emits {
(event: 'class-changed'): void
}
// emit
const emit = defineEmits<Emits>()
//
interface StudentItem {
id: string
@ -438,15 +446,15 @@ const rules: FormRules = {
college: [
{ required: true, message: '请选择所在学院', trigger: 'blur' }
],
// className: [
// {
// required: true,
// type: 'array',
// min: 1,
// message: '',
// trigger: 'blur'
// }
// ]
className: [
{
required: true,
type: 'array',
min: 1,
message: '请选择至少一个班级',
trigger: 'blur'
}
]
}
//
@ -457,43 +465,7 @@ const classRules: FormRules = {
}
//
const masterClassList = ref<ClassItem[]>([
{
id: '1',
className: '软件工程1班',
studentCount: 50,
creator: '王建国',
createTime: '2025.09.02 09:11'
},
{
id: '2',
className: '软件工程2班',
studentCount: 45,
creator: '王建国',
createTime: '2025.09.02 10:15'
},
{
id: '3',
className: '计算机科学1班',
studentCount: 48,
creator: '王建国',
createTime: '2025.09.02 11:20'
},
{
id: '4',
className: '计算机科学2班',
studentCount: 52,
creator: '王建国',
createTime: '2025.09.02 14:30'
},
{
id: '5',
className: '数学与应用数学1班',
studentCount: 30,
creator: '王建国',
createTime: '2025.09.03 08:45'
}
])
const masterClassList = ref<ClassItem[]>([])
//
const collegeOptions = ref([
@ -549,7 +521,7 @@ const classOptions = computed(() =>
const classSelectOptions = computed(() =>
masterClassList.value.map(item => ({
label: item.className,
value: item.className // 使
value: item.id
}))
)
@ -576,8 +548,21 @@ const columns: DataTableColumns<StudentItem> = [
{
title: '班级',
key: 'className',
width: 120,
align: 'center'
width: 150,
align: 'center',
render: (row) => {
// 使
const classNames = formatClassNames(row.className || '')
//
return h('div', {
class: 'class-cell'
}, classNames.map((name, index) =>
h('div', {
key: index,
class: 'class-cell-item'
}, name)
))
}
},
{
title: '所在学院',
@ -846,23 +831,21 @@ const handleDeleteStudent = (row: StudentItem) => {
negativeText: '取消',
onPositiveClick: async () => {
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
if (!props.classId) {
message.error('班级ID不存在无法删除学员')
return
}
// API
await ClassApi.removeStudent(props.classId.toString(), row.id)
const studentName = row.studentName
const studentId = row.id
//
data.value = data.value.filter(student => student.id !== studentId)
//
totalStudents.value = data.value.length
message.success(`已删除学员:${studentName}`)
//
loadData(props.classId)
} catch (error) {
console.error('删除学员失败:', error)
message.error('删除失败,请重试')
}
}
@ -927,6 +910,30 @@ const isCurrentClass = (classValue: string) => {
return classOption?.id === classValue
}
// ID
const getClassNameById = (classId: string): string => {
const classItem = masterClassList.value.find(item => item.id === classId)
console.log('格式化班级信息:', classItem);
return classItem ? classItem.className : classId
}
//
const formatClassNames = (classInfo: string): string[] => {
if (!classInfo) return ['未分配班级']
if (classInfo.includes(',')) {
//
return classInfo.split(',').map(id => id.trim()).map(getClassNameById)
} else {
//
return [getClassNameById(classInfo)]
}
}
// ID
const generateInviteCode = (classId: string) => {
// ID
@ -1040,34 +1047,20 @@ const handleAddClass = async () => {
const classId = currentEditId.value
const className = classFormData.value.className
await ClassApi.editClass({ id: classId, name: className })
//
const classIndex = masterClassList.value.findIndex(item => item.id === classId)
if (classIndex > -1) {
masterClassList.value[classIndex].className = className
}
message.success(`已将班级重命名为:${className}`)
//
emit('class-changed')
} else {
//
const className = classFormData.value.className
const res = await ClassApi.createClass({ name: className, course_id: courseId.value })
// idres.data.id
const newId = res.data?.id || (masterClassList.value.length + 1).toString()
const newClass: ClassItem = {
id: newId,
className,
studentCount: 0,
creator: '王建国',
createTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '')
}
masterClassList.value.push(newClass)
await ClassApi.createClass({ name: className, course_id: courseId.value })
message.success(`已添加班级:${className}`)
//
emit('class-changed')
}
//
await loadClassList()
closeAddClassModal()
} catch (error) {
message.error('请检查表单信息')
@ -1099,11 +1092,11 @@ const handleDeleteClass = (classItem: any) => {
try {
await ClassApi.deleteClass(classItem.id)
message.success(`已删除班级:${classItem.className}`)
//
const index = masterClassList.value.findIndex(item => item.id === classItem.id)
if (index > -1) {
masterClassList.value.splice(index, 1)
}
//
await loadClassList()
//
emit('class-changed')
} catch (error) {
message.error('删除失败,请重试')
}
@ -1111,98 +1104,107 @@ const handleDeleteClass = (classItem: any) => {
})
}
//
//
const loadClassList = async () => {
try {
console.log('🚀 开始加载班级列表数据...')
const response = await ClassApi.queryClassList({ course_id: null })
// API
const classListData = response.data.result || []
const transformedClassData: ClassItem[] = classListData.map((classItem: any) => ({
id: classItem.id?.toString() || '',
className: classItem.name || '未知班级',
studentCount: classItem.studentCount || 0,
creator: classItem.createBy || '未知创建者',
createTime: classItem.createTime ? new Date(classItem.createTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '') : new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '')
}))
masterClassList.value = transformedClassData
console.log(`✅ 成功加载班级列表,共 ${transformedClassData.length} 个班级`)
} catch (error) {
console.error('❌ 加载班级列表失败:', error)
message.error('加载班级列表失败,请重试')
masterClassList.value = []
}
}
//
let loadDataTimer: NodeJS.Timeout | null = null
// API
const loadData = async (classId?: number | null) => {
//
if (loadDataTimer) {
clearTimeout(loadDataTimer)
}
loadDataTimer = setTimeout(async () => {
console.log(`🚀 开始加载班级数据 - classId: ${classId}, 调用栈:`, new Error().stack?.split('\n')[2]?.trim())
//
if (loading.value) {
console.log('⚠️ 数据正在加载中,跳过重复请求')
return
}
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
// ID
let mockData: StudentItem[] = []
if (classId === null || classId === undefined) {
//
mockData = []
//
data.value = []
totalStudents.value = 0
console.log('📝 未选择班级,显示空数据')
} else {
// ID
const classDataMap: Record<number, StudentItem[]> = {
1: [
{
id: 'student_1_1',
studentName: '张华',
accountNumber: '1660341',
className: '软件工程1班',
college: '计算机学院',
loginName: '1660341',
joinTime: '2025.07.25 08:20'
},
{
id: 'student_1_2',
studentName: '李明',
accountNumber: '1660342',
className: '软件工程1班',
college: '计算机学院',
loginName: '1660342',
joinTime: '2025.07.25 09:15'
}
],
2: [
{
id: 'student_2_1',
studentName: '王丽',
accountNumber: '1660343',
className: '软件工程2班',
college: '软件学院',
loginName: '1660343',
joinTime: '2025.07.26 10:30'
},
{
id: 'student_2_2',
studentName: '赵强',
accountNumber: '1660344',
className: '软件工程2班',
college: '软件学院',
loginName: '1660344',
joinTime: '2025.07.26 11:45'
},
{
id: 'student_2_3',
studentName: '孙美',
accountNumber: '1660345',
className: '软件工程2班',
college: '软件学院',
loginName: '1660345',
joinTime: '2025.07.26 14:20'
}
],
3: [
{
id: 'student_3_1',
studentName: '周杰',
accountNumber: '1660346',
className: '计算机科学1班',
college: '数学学院',
loginName: '1660346',
joinTime: '2025.07.27 08:50'
}
]
}
// API
console.log(`📡 正在获取班级 ${classId} 的学生数据...`)
const response = await ClassApi.getClassStudents(classId.toString())
mockData = classDataMap[classId] || []
// API
const studentsData = response.data.result || []
const transformedData: StudentItem[] = studentsData.map((student: any) => ({
id: student.id || '',
studentName: student.realname || student.username || '未知姓名',
accountNumber: student.studentId || student.username || '',
className: student.classId || '未分配班级', // ID
college: student.college || student.department || '未分配学院',
loginName: student.username || '',
joinTime: student.createTime ? new Date(student.createTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '') : '未知时间'
}))
data.value = transformedData
totalStudents.value = transformedData.length
console.log(`✅ 成功加载班级 ${classId} 的数据,共 ${transformedData.length} 名学员`)
}
data.value = mockData
//
totalStudents.value = mockData.length
console.log(`加载班级 ${classId} 的数据,共 ${mockData.length} 名学员`)
} catch (error) {
console.error('加载数据失败:', error)
message.error('加载数据失败,请重试')
console.error('❌ 加载班级学生数据失败:', error)
message.error('加载学生数据失败,请重试')
data.value = []
totalStudents.value = 0
} finally {
loading.value = false
loadDataTimer = null
}
}, 100) // 100ms
}
//
@ -1227,7 +1229,7 @@ watch(
(newClassId, oldClassId) => {
console.log(`班级ID从 ${oldClassId} 变更为 ${newClassId}`)
if (newClassId !== oldClassId) {
//
// watch
selectedDepartment.value = newClassId ? newClassId.toString() : ''
loadData(newClassId)
}
@ -1235,21 +1237,27 @@ watch(
{ immediate: false } // onMounted
)
// /
// /
// propsclassId使props
watch(
() => selectedDepartment.value,
(newDepartmentId, oldDepartmentId) => {
console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`)
if (newDepartmentId !== oldDepartmentId) {
// 使使classId
const targetClassId = newDepartmentId ? Number(newDepartmentId) : props.classId
// props.classId
// props.classIdprops
const currentPropsClassId = props.classId?.toString()
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
const targetClassId = newDepartmentId ? Number(newDepartmentId) : null
loadData(targetClassId)
}
},
{ immediate: false }
)
onMounted(() => {
onMounted(async () => {
//
await loadClassList()
// 使使classId使
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
loadData(initialClassId)
@ -1267,7 +1275,8 @@ defineExpose({
openAddClassModal,
handleRenameClass,
handleDeleteClass,
openInviteModal
openInviteModal,
loadClassList
})
</script>
@ -1622,4 +1631,31 @@ defineExpose({
font-size: 14px;
text-align: center;
}
/* 班级列样式 */
.class-cell {
line-height: 1.4;
word-break: break-all;
white-space: normal;
max-width: 150px;
min-height: 20px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 4px 8px;
}
.class-cell-item {
padding: 1px 0;
font-size: 13px;
color: #333;
border-radius: 2px;
margin: 1px 0;
}
.class-cell-item:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 2px;
margin-bottom: 2px;
}
</style>

View File

@ -17,7 +17,39 @@ export interface Lesson {
isFree?: boolean
}
// 课程编辑数据接口 - 保存完整的后端数据,不做字段限制
export interface CourseEditData {
[key: string]: any // 允许任意字段,直接保存后端返回的所有数据
}
export const useCourseStore = defineStore('course', () => {
// 持久化存储的key
const COURSE_EDIT_DATA_KEY = 'courseEditData'
// 从localStorage加载课程编辑数据
const loadCourseEditDataFromStorage = (): CourseEditData | null => {
try {
const stored = localStorage.getItem(COURSE_EDIT_DATA_KEY)
return stored ? JSON.parse(stored) : null
} catch (error) {
console.error('从localStorage加载课程编辑数据失败:', error)
return null
}
}
// 保存课程编辑数据到localStorage
const saveCourseEditDataToStorage = (data: CourseEditData | null) => {
try {
if (data) {
localStorage.setItem(COURSE_EDIT_DATA_KEY, JSON.stringify(data))
} else {
localStorage.removeItem(COURSE_EDIT_DATA_KEY)
}
} catch (error) {
console.error('保存课程编辑数据到localStorage失败:', error)
}
}
// 状态
const courses = ref<Course[]>([])
const currentCourse = ref<Course | null>(null)
@ -28,6 +60,9 @@ export const useCourseStore = defineStore('course', () => {
const selectedCategory = ref('')
const selectedLevel = ref('')
// 课程编辑数据 - 初始化时从localStorage加载
const courseEditData = ref<CourseEditData | null>(loadCourseEditDataFromStorage())
// 计算属性
const filteredCourses = computed(() => {
let filtered = courses.value
@ -60,151 +95,149 @@ export const useCourseStore = defineStore('course', () => {
const fetchCourses = async () => {
isLoading.value = true
try {
console.log('尝试从API获取课程数据...')
const response = await CourseApi.getCourses()
console.log('API响应:', response)
courses.value = response.data
} catch (error) {
console.error('API调用失败使用模拟数据:', error)
// 如果API调用失败使用模拟数据作为后备
const mockCourses: Course[] = [
{
id: "1",
title: 'Vue.js 3 完整教程',
description: '从零开始学习Vue.js 3包括Composition API、TypeScript集成等现代开发技术',
content: '详细的Vue.js 3课程内容',
instructor: {
id: 1,
name: '李老师',
title: '前端开发专家',
bio: '资深前端开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.8,
studentsCount: 1234,
coursesCount: 5,
experience: '5年前端开发经验',
education: ['计算机科学学士'],
certifications: ['Vue.js认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 299,
originalPrice: 399,
currency: 'CNY',
rating: 4.8,
ratingCount: 100,
studentsCount: 1234,
duration: '12小时',
level: 'intermediate',
category: {
id: 1,
name: '前端开发',
slug: 'frontend',
description: '前端开发相关课程'
},
tags: ['Vue.js', 'JavaScript', 'TypeScript'],
totalLessons: 20,
language: 'zh-CN',
skills: ['Vue.js', 'TypeScript', 'Composition API'],
requirements: ['JavaScript基础', 'HTML/CSS基础'],
objectives: ['掌握Vue.js 3核心概念', '学会使用Composition API', '理解TypeScript集成'],
status: 'published',
createdAt: '2024-01-01',
updatedAt: '2024-01-15',
publishedAt: '2024-01-01'
},
{
id: "2",
title: 'React 18 实战开发',
description: '掌握React 18的新特性包括并发渲染、Suspense等高级功能',
content: '详细的React 18课程内容',
instructor: {
id: 2,
name: '王老师',
title: 'React专家',
bio: '资深React开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.9,
studentsCount: 2156,
coursesCount: 8,
experience: '6年React开发经验',
education: ['软件工程硕士'],
certifications: ['React认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 399,
originalPrice: 499,
currency: 'CNY',
rating: 4.9,
ratingCount: 200,
studentsCount: 2156,
duration: '15小时',
level: 'advanced',
category: {
id: 1,
name: '前端开发',
slug: 'frontend',
description: '前端开发相关课程'
},
tags: ['React', 'JavaScript', 'Hooks'],
totalLessons: 25,
language: 'zh-CN',
skills: ['React 18', 'Hooks', '并发渲染'],
requirements: ['JavaScript基础', 'React基础'],
objectives: ['掌握React 18新特性', '学会并发渲染', '理解Suspense'],
status: 'published',
createdAt: '2024-01-05',
updatedAt: '2024-01-20',
publishedAt: '2024-01-05'
},
{
id: "3",
title: 'Node.js 后端开发',
description: '学习Node.js后端开发包括Express、数据库操作、API设计等',
content: '详细的Node.js课程内容',
instructor: {
id: 3,
name: '张老师',
title: 'Node.js专家',
bio: '资深后端开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.7,
studentsCount: 987,
coursesCount: 6,
experience: '7年后端开发经验',
education: ['计算机科学硕士'],
certifications: ['Node.js认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 349,
originalPrice: 449,
currency: 'CNY',
rating: 4.7,
ratingCount: 150,
studentsCount: 987,
duration: '18小时',
level: 'intermediate',
category: {
id: 2,
name: '后端开发',
slug: 'backend',
description: '后端开发相关课程'
},
tags: ['Node.js', 'Express', 'MongoDB'],
totalLessons: 30,
language: 'zh-CN',
skills: ['Node.js', 'Express', 'MongoDB', 'API设计'],
requirements: ['JavaScript基础', '编程基础'],
objectives: ['掌握Node.js后端开发', '学会Express框架', '理解数据库操作'],
status: 'published',
createdAt: '2024-01-10',
updatedAt: '2024-01-25',
publishedAt: '2024-01-10'
}
]
courses.value = mockCourses
// const mockCourses: Course[] = [
// {
// id: "1",
// title: 'Vue.js 3 完整教程',
// description: '从零开始学习Vue.js 3包括Composition API、TypeScript集成等现代开发技术',
// content: '详细的Vue.js 3课程内容',
// instructor: {
// id: 1,
// name: '李老师',
// title: '前端开发专家',
// bio: '资深前端开发工程师',
// avatar: 'https://via.placeholder.com/50',
// rating: 4.8,
// studentsCount: 1234,
// coursesCount: 5,
// experience: '5年前端开发经验',
// education: ['计算机科学学士'],
// certifications: ['Vue.js认证']
// },
// thumbnail: 'https://via.placeholder.com/300x200',
// coverImage: 'https://via.placeholder.com/300x200',
// price: 299,
// originalPrice: 399,
// currency: 'CNY',
// rating: 4.8,
// ratingCount: 100,
// studentsCount: 1234,
// duration: '12小时',
// level: 'intermediate',
// category: {
// id: 1,
// name: '前端开发',
// slug: 'frontend',
// description: '前端开发相关课程'
// },
// tags: ['Vue.js', 'JavaScript', 'TypeScript'],
// totalLessons: 20,
// language: 'zh-CN',
// skills: ['Vue.js', 'TypeScript', 'Composition API'],
// requirements: ['JavaScript基础', 'HTML/CSS基础'],
// objectives: ['掌握Vue.js 3核心概念', '学会使用Composition API', '理解TypeScript集成'],
// status: 'published',
// createdAt: '2024-01-01',
// updatedAt: '2024-01-15',
// publishedAt: '2024-01-01'
// },
// {
// id: "2",
// title: 'React 18 实战开发',
// description: '掌握React 18的新特性包括并发渲染、Suspense等高级功能',
// content: '详细的React 18课程内容',
// instructor: {
// id: 2,
// name: '王老师',
// title: 'React专家',
// bio: '资深React开发工程师',
// avatar: 'https://via.placeholder.com/50',
// rating: 4.9,
// studentsCount: 2156,
// coursesCount: 8,
// experience: '6年React开发经验',
// education: ['软件工程硕士'],
// certifications: ['React认证']
// },
// thumbnail: 'https://via.placeholder.com/300x200',
// coverImage: 'https://via.placeholder.com/300x200',
// price: 399,
// originalPrice: 499,
// currency: 'CNY',
// rating: 4.9,
// ratingCount: 200,
// studentsCount: 2156,
// duration: '15小时',
// level: 'advanced',
// category: {
// id: 1,
// name: '前端开发',
// slug: 'frontend',
// description: '前端开发相关课程'
// },
// tags: ['React', 'JavaScript', 'Hooks'],
// totalLessons: 25,
// language: 'zh-CN',
// skills: ['React 18', 'Hooks', '并发渲染'],
// requirements: ['JavaScript基础', 'React基础'],
// objectives: ['掌握React 18新特性', '学会并发渲染', '理解Suspense'],
// status: 'published',
// createdAt: '2024-01-05',
// updatedAt: '2024-01-20',
// publishedAt: '2024-01-05'
// },
// {
// id: "3",
// title: 'Node.js 后端开发',
// description: '学习Node.js后端开发包括Express、数据库操作、API设计等',
// content: '详细的Node.js课程内容',
// instructor: {
// id: 3,
// name: '张老师',
// title: 'Node.js专家',
// bio: '资深后端开发工程师',
// avatar: 'https://via.placeholder.com/50',
// rating: 4.7,
// studentsCount: 987,
// coursesCount: 6,
// experience: '7年后端开发经验',
// education: ['计算机科学硕士'],
// certifications: ['Node.js认证']
// },
// thumbnail: 'https://via.placeholder.com/300x200',
// coverImage: 'https://via.placeholder.com/300x200',
// price: 349,
// originalPrice: 449,
// currency: 'CNY',
// rating: 4.7,
// ratingCount: 150,
// studentsCount: 987,
// duration: '18小时',
// level: 'intermediate',
// category: {
// id: 2,
// name: '后端开发',
// slug: 'backend',
// description: '后端开发相关课程'
// },
// tags: ['Node.js', 'Express', 'MongoDB'],
// totalLessons: 30,
// language: 'zh-CN',
// skills: ['Node.js', 'Express', 'MongoDB', 'API设计'],
// requirements: ['JavaScript基础', '编程基础'],
// objectives: ['掌握Node.js后端开发', '学会Express框架', '理解数据库操作'],
// status: 'published',
// createdAt: '2024-01-10',
// updatedAt: '2024-01-25',
// publishedAt: '2024-01-10'
// }
// ]
courses.value = []
} finally {
isLoading.value = false
}
@ -299,6 +332,21 @@ export const useCourseStore = defineStore('course', () => {
}
}
// 课程编辑数据管理方法
const setCourseEditData = (data: CourseEditData) => {
// 清空旧数据再保存新数据
courseEditData.value = null
courseEditData.value = data
// 持久化到localStorage
saveCourseEditDataToStorage(data)
}
const clearCourseEditData = () => {
courseEditData.value = null
// 从localStorage清除
saveCourseEditDataToStorage(null)
}
return {
// 状态
courses,
@ -309,6 +357,7 @@ export const useCourseStore = defineStore('course', () => {
searchQuery,
selectedCategory,
selectedLevel,
courseEditData,
// 计算属性
filteredCourses,
categories,
@ -317,6 +366,8 @@ export const useCourseStore = defineStore('course', () => {
fetchCourseById,
fetchLessons,
enrollCourse,
updateProgress
updateProgress,
setCourseEditData,
clearCourseEditData
}
})

View File

@ -2,23 +2,29 @@
<div class="modal-container flex-col">
<h2 class="modal-title">添加课件</h2>
<n-divider />
<div class="upload-section flex-row">
<div class="label-group flex-col justify-between">
<span class="upload-path-label">上传路径</span>
<span class="upload-file-label">上传文件</span>
<div class="upload-section">
<div class="upload-row">
<div class="upload-item">
<label class="upload-label">资源分类</label>
<div class="upload-control">
<n-select
:options="categoryOptions"
v-model:value="selectedCategory"
placeholder="请选择分类"
@update:value="handleCategorySelect"
class="category-select"
/>
</div>
<div class="button-group flex-col justify-between">
<n-popselect
:options="folderOptions"
trigger="click"
placement="bottom-start"
v-model:value="selectedFolder"
@update:value="handleFolderSelect"
>
<div class="select-path-button flex-col">
<span class="button-text file-input-label">选择路径</span>
<div class="upload-display">
<span class="selected-value">{{ getSelectedCategoryName() }}</span>
</div>
</n-popselect>
</div>
</div>
<div class="upload-row">
<div class="upload-item">
<label class="upload-label">上传文件</label>
<div class="upload-control">
<n-popselect
:options="uploadOptions"
trigger="click"
@ -26,10 +32,17 @@
@update:value="handleUploadOptionSelect"
:render-label="renderUploadOption"
>
<div class="select-file-button flex-col">
<span class="button-text file-input-label">选择文件</span>
</div>
<n-button type="primary" class="upload-button">
选择文件
</n-button>
</n-popselect>
</div>
<div class="upload-display">
<span class="file-info">{{ selectedFileInfo }}</span>
</div>
</div>
</div>
<!-- 隐藏的文件输入框 -->
<input
ref="localFileInput"
@ -46,10 +59,6 @@
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
/>
</div>
<div class="folder-display flex-col justify-between">
<span class="folder-name">{{ getSelectedFolderName() }}</span>
</div>
</div>
<div class="supported-formats flex-row">
<span class="formats-label">支持格式</span>
<span class="document-formats">文本文.doc.docx.pdf表格文件.xls.xlsx<br />演示文稿.ppt.pptx</span>
@ -76,9 +85,21 @@
<script setup lang="ts">
import { ref, h } from 'vue'
import { NSelect, NButton } from 'naive-ui'
//
const selectedFolder = ref<string>('')
//
const selectedCategory = ref<string>('')
//
const selectedFileInfo = ref<string>('未选择文件')
//
const categoryOptions = ref([
{ label: '文档', value: 'document' },
{ label: '视频', value: 'video' },
{ label: '图片', value: 'image' },
{ label: '音频', value: 'audio' }
])
//
const uploadOptions = ref([
@ -90,18 +111,6 @@ const uploadOptions = ref([
const localFileInput = ref<HTMLInputElement>()
const resourceFileInput = ref<HTMLInputElement>()
//
const folderOptions = ref([
{ label: '文件夹一', value: 'folder1' },
{ label: '文件夹二', value: 'folder2' },
{ label: '文件夹三', value: 'folder3' },
{ label: '文档资料', value: 'documents' },
{ label: '视频课件', value: 'videos' },
{ label: '音频文件', value: 'audios' },
{ label: '演示文稿', value: 'presentations' },
{ label: '其他资源', value: 'others' }
])
//
const renderUploadOption = (option: any) => {
return h('span', {
@ -126,19 +135,19 @@ const handleUploadOptionSelect = (value: string) => {
}
}
//
const getSelectedFolderName = () => {
if (!selectedFolder.value) {
return '请选择文件夹'
//
const getSelectedCategoryName = () => {
if (!selectedCategory.value) {
return '请选择分类'
}
const folder = folderOptions.value.find(option => option.value === selectedFolder.value)
return folder ? folder.label : '未知文件夹'
const category = categoryOptions.value.find(option => option.value === selectedCategory.value)
return category ? category.label : '未知分类'
}
//
const handleFolderSelect = (value: string) => {
console.log('选中的文件夹:', value)
//
//
const handleCategorySelect = (value: string) => {
console.log('选中的分类:', value)
//
}
//
@ -146,7 +155,9 @@ const handleLocalFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
console.log('本地上传文件:', files[0])
const file = files[0]
console.log('本地上传文件:', file)
selectedFileInfo.value = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`
//
}
}
@ -156,7 +167,9 @@ const handleResourceFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
console.log('资源上传文件:', files[0])
const file = files[0]
console.log('资源上传文件:', file)
selectedFileInfo.value = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`
//
}
}
@ -181,7 +194,7 @@ const handleConfirm = () => {
.modal-container {
position: relative;
width: 1076px;
height: 623px;
/* height: 623px; */
background: #FFFFFF ;
background-size: 100% 100%;
margin: 0 auto;
@ -205,146 +218,66 @@ const handleConfirm = () => {
.upload-section {
width: 1028px;
height: 160px;
background: #FCFCFC ;
background-size: 100% 100%;
background: #FCFCFC;
border: 1px solid rgb(233, 233, 233);
border-radius: 8px;
margin: 0 0 0 22px;
padding: 24px;
}
.label-group {
width: 90px;
height: 82px;
margin: 39px 0 0 35px;
}
.upload-path-label {
width: 90px;
height: 22px;
overflow-wrap: break-word;
color: rgba(6, 35, 51, 1);
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 22px;
}
.upload-file-label {
width: 90px;
height: 22px;
overflow-wrap: break-word;
color: rgba(6, 35, 51, 1);
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 22px;
margin-top: 38px;
}
.button-group {
width: 123px;
height: 98px;
margin: 30px 0 0 3px;
}
.select-path-button {
height: 38px;
background: #0288D1;
background-size: 100% 100%;
width: 123px;
}
.button-text {
width: 100%;
height: 21px;
overflow-wrap: break-word;
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: center;
white-space: nowrap;
line-height: 21px;
margin: 6px 0 0 0;
.upload-row {
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.button-text1{
color: #0288D1 ;
}
.button-text2{
color: #FFFFFF ;
margin-bottom: 24px;
}
.select-file-button {
height: 38px;
background: #0288D1 ;
background-size: 100% 100%;
margin-top: 22px;
width: 123px;
.upload-row:last-child {
margin-bottom: 0;
}
.folder-display {
width: 171px;
height: 106px;
margin: 39px 604px 0 2px;
.upload-item {
display: flex;
align-items: center;
width: 100%;
gap: 20px;
}
.folder-name {
width: 72px;
height: 22px;
overflow-wrap: break-word;
.upload-label {
width: 90px;
color: rgba(6, 35, 51, 1);
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 22px;
margin-left: 12px;
flex-shrink: 0;
}
.existing-folders {
width: 171px;
height: 80px;
background: #FFFFFF;
background-size: 289px 198px;
margin-top: 4px;
.upload-control {
width: 200px;
flex-shrink: 0;
}
.folder-item {
width: 112px;
height: 17px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 14px;
.upload-display {
flex: 1;
padding-left: 20px;
}
.selected-value,
.file-info {
color: rgba(6, 35, 51, 1);
font-size: 16px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 17px;
margin: 13px 0 0 29px;
}
.folder-item {
width: 112px;
height: 17px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 14px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 17px;
margin: 14px 0 19px 29px;
.category-select {
width: 100%;
}
.upload-button {
width: 100%;
height: 38px;
}
.supported-formats {
@ -468,6 +401,13 @@ const handleConfirm = () => {
line-height: 22px;
margin: 0;
}
.button-text1{
color: #0288D1 ;
}
.button-text2{
color: #FFFFFF ;
}
.flex-col {
display: flex;
flex-direction: column;
@ -501,6 +441,42 @@ const handleConfirm = () => {
/* NSelect组件样式 */
:deep(.n-base-selection) {
height: 38px !important;
border-radius: 4px !important;
border: 1px solid #D9D9D9 !important;
transition: border-color 0.3s ease !important;
}
:deep(.n-base-selection:hover) {
border-color: #0288D1 !important;
}
:deep(.n-base-selection.n-base-selection--focused) {
border-color: #0288D1 !important;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.1) !important;
}
:deep(.n-base-selection .n-base-selection-input) {
height: 36px !important;
}
:deep(.n-base-selection .n-base-selection-placeholder) {
color: #999 !important;
font-size: 14px !important;
}
:deep(.n-base-selection .n-base-selection-label) {
font-size: 14px !important;
color: #333 !important;
}
/* NButton组件样式 */
:deep(.n-button) {
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif !important;
}
/* 文件输入框标签样式 */
.file-input-label {
cursor: pointer;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,8 @@
<span class="class-title">班级管理</span>
<n-collapse :default-expanded-names="['1']">
<template #header-extra>
<n-popselect
trigger="hover"
placement="bottom-start"
:options="classMenuOptions"
@update:value="handleClassMenuSelect"
>
<n-popselect trigger="hover" placement="bottom-start" :options="classMenuOptions"
@update:value="handleClassMenuSelect">
<n-icon style="cursor: pointer;">
<EllipsisVertical />
</n-icon>
@ -21,20 +17,11 @@
</n-icon>
</template>
<n-collapse-item title="班级管理" name="1">
<div
class="class-item"
:class="{ active: activeClassId === value.id }"
v-for="value in classList"
:key="value.id"
@click="handleClassClick(value.id)"
>
<div class="class-item" :class="{ active: activeClassId === value.id }" v-for="value in classList"
:key="value.id" @click="handleClassClick(value.id)">
<div>{{ value.name }}</div>
<n-popselect
trigger="hover"
placement="bottom-start"
:options="getClassItemOptions()"
@update:value="(selectedValue: string) => handleClassItemMenuSelect(selectedValue, value.id)"
>
<n-popselect trigger="hover" placement="bottom-start" :options="getClassItemOptions()"
@update:value="(selectedValue: string) => handleClassItemMenuSelect(selectedValue, value.id)">
<n-icon style="cursor: pointer;">
<EllipsisVertical />
</n-icon>
@ -44,7 +31,9 @@
</n-collapse>
</div>
<div class="class-right">
<ClassManagement ref="classManagementRef" :class-id="activeClassId" :class-name="classList.find(item => item.id === activeClassId)?.name" />
<ClassManagement ref="classManagementRef" :class-id="activeClassId"
:class-name="classList.find(item => item.id === activeClassId)?.name"
@class-changed="handleClassChanged" />
</div>
</div>
</template>
@ -52,13 +41,10 @@
<script lang="ts" setup>
import ClassManagement from '@/components/teacher/ClassManagement.vue'
import { CaretForward, EllipsisVertical } from '@vicons/ionicons5'
import { ref } from "vue"
import { onMounted, ref } from "vue"
import { ClassApi } from '@/api/modules/teachCourse'
const classList = ref([
{ id: 1, name: "班级一" },
{ id: 2, name: "班级二" },
{ id: 3, name: "班级三" },
])
const classList = ref<Array<{ id: number; name: string }>>([])
// ID
const activeClassId = ref<number | null>(1)
@ -92,7 +78,13 @@ const getClassItemOptions = () => [
//
const handleClassClick = (classId: number) => {
console.log(`🖱️ 用户点击班级: ${classId}, 当前激活班级: ${activeClassId.value}`)
if (activeClassId.value !== classId) {
activeClassId.value = classId
console.log(`✅ 班级切换完成: ${activeClassId.value}`)
} else {
console.log('⚠️ 点击的是当前已激活的班级,无需切换')
}
}
//
@ -132,14 +124,33 @@ const handleClassItemMenuSelect = (value: string, classId: number) => {
classManagementRef.value.handleDeleteClass?.({
id: classId.toString(),
className: selectedClass.name,
studentCount: 0,
creator: '王建国',
createTime: '2025.09.02 09:11'
})
break
}
}
//
const handleClassChanged = () => {
queryClassList(false)
}
const queryClassList = (refresh: boolean = false) => {
ClassApi.queryClassList({ course_id: null }).then(res => {
classList.value = res.data.result || []
if(refresh){
activeClassId.value = classList.value.length > 0 ? classList.value[0].id : null
}
}).catch(err => {
console.error('获取班级列表失败:', err)
})
}
onMounted(() => {
queryClassList(true)
})
</script>
<style scoped>
@ -169,7 +180,7 @@ const handleClassItemMenuSelect = (value: string, classId: number) => {
flex: 1;
}
.class-item{
.class-item {
display: flex;
justify-content: space-between;
align-items: center;
@ -178,7 +189,7 @@ const handleClassItemMenuSelect = (value: string, classId: number) => {
border-radius: 6px;
}
.active{
.active {
background-color: #F5F9FC;
color: #0288D1;
}