feat:对接部分班级页面接口,修改了部分课程页面,对接了一部分接口
This commit is contained in:
parent
e645a190dd
commit
155db7a1e4
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@ -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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 班级通过搜索导入学生
|
||||
*/
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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(() => {
|
||||
|
@ -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; // 如果从store获取到数据,就不需要再调用API
|
||||
}
|
||||
|
||||
// 如果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(() => {
|
||||
// 如果是编辑模式(有courseId)、有通过路由传递的课程数据,或者store中有数据
|
||||
if (isEditMode.value && courseId.value) {
|
||||
loadCourseData(courseId.value)
|
||||
loadCourseData()
|
||||
} else if (route.query.courseData || courseStore.courseEditData) {
|
||||
// 即使没有ID,也尝试从路由数据或store加载(用于从列表页面直接编辑的情况)
|
||||
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) {
|
||||
// 只有当previewUrl是本地创建的URL时才需要释放
|
||||
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>
|
||||
|
@ -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 {
|
||||
|
@ -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 })
|
||||
// 假设后端返回新班级id,前端可用res.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重复
|
||||
)
|
||||
|
||||
// 监听部门/班级选择器变化,重新加载数据
|
||||
// 只监听用户手动选择部门/班级选择器的变化(排除程序化更新)
|
||||
// 注意:这个监听器主要用于组件内部选择器的交互,如果外部通过props传入classId则优先使用props
|
||||
watch(
|
||||
() => selectedDepartment.value,
|
||||
(newDepartmentId, oldDepartmentId) => {
|
||||
console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`)
|
||||
if (newDepartmentId !== oldDepartmentId) {
|
||||
// 当选择器有值时,使用选择器的值;否则使用传入的classId
|
||||
const targetClassId = newDepartmentId ? Number(newDepartmentId) : props.classId
|
||||
// 只有当不是props.classId驱动的变化时才加载数据
|
||||
// 如果当前props.classId存在且与选择器值一致,说明是props驱动的变化,不需要重复加载
|
||||
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>
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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%;
|
||||
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
1543
src/views/teacher/course/CoursewareManagementBackup.vue
Normal file
1543
src/views/teacher/course/CoursewareManagementBackup.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user