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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -50,9 +50,10 @@ export interface CreateCourseRequest {
max_enroll?: number | null max_enroll?: number | null
status?: number | null status?: number | null
question?: string | null question?: string | null
pause_exit: string pauseExit: string
allow_speed: string allowSpeed: string
show_subtitle: string showSubtitle: string
categoryId?: number | string | null // 支持单个ID(number)或多个ID的逗号分隔字符串
} }
// 编辑课程请求参数 // 编辑课程请求参数
@ -89,6 +90,37 @@ export interface CourseStudent {
enrollTime?: string 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模块 * 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 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 ids: string; // 逗号分隔的学生id
} }
export interface CreatedStudentsRequest {
realName: string;
studentNumber: string;
password: string;
school: string;
classId: string;
}
export class ClassApi { export class ClassApi {
/** /**
* *
@ -254,6 +389,10 @@ export class ClassApi {
return ApiRequest.post('/aiol/aiolClass/add', data); 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>> { static async deleteClass(id: string): Promise<ApiResponse<any>> {
return ApiRequest.delete('/aiol/aiolClass/delete', { params: { id } }); return ApiRequest.delete('/aiol/aiolClass/delete', { id });
} }
/**
*
*/
static async createdStudents(data: CreatedStudentsRequest): Promise<ApiResponse<any>> {
return ApiRequest.post(`/aiol/aiolClass/create_and_add_student`, data);
}
/** /**
* *
*/ */

View File

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

View File

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

View File

@ -24,6 +24,12 @@
<n-input v-model:value="formData.courseName" placeholder="请输入课程名称" class="form-input" /> <n-input v-model:value="formData.courseName" placeholder="请输入课程名称" class="form-input" />
</div> </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"> <div class="form-item">
<label class="form-label required">主讲老师:</label> <label class="form-label required">主讲老师:</label>
@ -31,14 +37,6 @@
class="form-input" /> class="form-input" />
</div> </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"> <div class="form-item">
<label class="form-label required">参与学员:</label> <label class="form-label required">参与学员:</label>
@ -57,7 +55,8 @@
</div> </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" <n-select v-model:value="formData.selectedClasses" multiple :options="classOptions"
placeholder="选择班级(可多选)" class="form-input" /> placeholder="选择班级(可多选)" class="form-input" />
</div> </div>
@ -68,15 +67,15 @@
<!-- 课程分类 --> <!-- 课程分类 -->
<div class="form-item"> <div class="form-item">
<label class="form-label required">课程分类:</label> <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" /> class="form-input" />
</div> </div>
<!-- 排序 --> <!-- 排序 -->
<div class="form-item"> <!-- <div class="form-item">
<label class="form-label required">排序:</label> <label class="form-label required">排序:</label>
<n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" /> <n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" />
</div> </div> -->
<!-- 课程结束时间 --> <!-- 课程结束时间 -->
<div class="form-item"> <div class="form-item">
@ -173,7 +172,7 @@
</template> </template>
<script setup lang="ts"> <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 { useRouter, useRoute } from 'vue-router'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { ArrowBackOutline } from '@vicons/ionicons5'; import { ArrowBackOutline } from '@vicons/ionicons5';
@ -188,11 +187,15 @@ import {
import '@wangeditor/editor/dist/css/style.css' import '@wangeditor/editor/dist/css/style.css'
// @ts-ignore // @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue' 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 router = useRouter()
const route = useRoute() const route = useRoute()
const message = useMessage() const message = useMessage()
const courseStore = useCourseStore()
// //
const isEditMode = computed(() => !!route.params.id) const isEditMode = computed(() => !!route.params.id)
@ -208,6 +211,9 @@ const editorRef = shallowRef()
const fileInput = ref<HTMLInputElement>() const fileInput = ref<HTMLInputElement>()
const previewUrl = ref('') const previewUrl = ref('')
//
const pendingCourseDescription = ref('')
const toolbarConfig = {} const toolbarConfig = {}
const editorConfig = { placeholder: '请输入内容...' } const editorConfig = { placeholder: '请输入内容...' }
const mode = 'default' const mode = 'default'
@ -221,126 +227,179 @@ onBeforeUnmount(() => {
const handleCreated = (editor: any) => { const handleCreated = (editor: any) => {
editorRef.value = editor // editor editorRef.value = editor // editor
//
if (pendingCourseDescription.value) {
editor.setHtml(pendingCourseDescription.value);
formData.courseDescription = pendingCourseDescription.value;
pendingCourseDescription.value = ''; //
}
} }
// //
const formData = reactive({ const formData = reactive({
courseName: '', courseName: '',
courseCategory: null, // null placeholder courseCategory: [] as string[] | number[], // null placeholder
instructors: [], instructors: [] as string[],
sort: null, // null placeholder // sort: null as string | null, // null placeholder
startTime: null, startTime: null as number | null,
endTime: null, endTime: null as number | null,
studentType: 'all', // 'all' 'partial' studentType: 'all' as 'all' | 'partial', // 'all' 'partial'
selectedClasses: [], selectedClasses: [] as string[],
courseCover: null as File | null, courseCover: null as File | null,
courseDescription: '', courseDescription: '',
// //
stopOnLeave: true, stopOnLeave: false,
videoSpeedControl: false, videoSpeedControl: false,
showVideoText: true, showVideoText: false,
// //
pointsEnabled: true, pointsEnabled: true,
earnPoints: 60, earnPoints: 60,
requiredPoints: 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 { try {
// API // store
await new Promise(resolve => setTimeout(resolve, 500)) const storeCourseData = courseStore.courseEditData;
const courseData = mockCourseData[id as unknown as keyof typeof mockCourseData] if (storeCourseData) {
if (courseData) { //
// formData.courseName = storeCourseData.name || storeCourseData.courseName || '';
Object.assign(formData, courseData) // 使
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 || []);
// URL const tempCourseDescription = storeCourseData.description || storeCourseData.courseDescription || '';
if (courseData.courseCover) { if (tempCourseDescription) {
previewUrl.value = courseData.courseCover if (editorRef.value) {
editorRef.value.setHtml(tempCourseDescription);
formData.courseDescription = tempCourseDescription;
} else {
pendingCourseDescription.value = tempCourseDescription;
}
} }
message.success('课程数据加载成功') // 0/1
} else { formData.stopOnLeave = storeCourseData.pauseExit !== undefined ?
message.error('课程不存在') Boolean(Number(storeCourseData.pauseExit)) : true;
router.back() formData.videoSpeedControl = storeCourseData.allowSpeed !== undefined ?
Boolean(Number(storeCourseData.allowSpeed)) : false;
formData.showVideoText = storeCourseData.showSubtitle !== undefined ?
Boolean(Number(storeCourseData.showSubtitle)) : true;
//
formData.pointsEnabled = storeCourseData.pointsEnabled !== undefined ? storeCourseData.pointsEnabled : true;
formData.earnPoints = storeCourseData.earnPoints || 60;
formData.requiredPoints = storeCourseData.requiredPoints || 60;
if (storeCourseData.cover || storeCourseData.courseCover) {
previewUrl.value = storeCourseData.cover || storeCourseData.courseCover;
formData.courseCover = null;
}
message.success('课程数据加载成功');
return; // storeAPI
} }
// store
const routeCourseData = route.query.courseData;
if (routeCourseData) {
try {
const courseData = JSON.parse(routeCourseData as string);
//
formData.courseName = courseData.name || courseData.courseName || '';
// 使
const categoryData = courseData.categoryId || courseData.courseCategory;
if (typeof categoryData === 'string' && categoryData) {
formData.courseCategory = categoryData.split(',').map(id => Number(id));
} else if (Array.isArray(categoryData)) {
formData.courseCategory = categoryData;
} else {
formData.courseCategory = [];
}
formData.instructors = courseData.instructors || [];
formData.startTime = courseData.start_time || courseData.startTime || null;
formData.endTime = courseData.end_time || courseData.endTime || null;
formData.studentType = (courseData.type === 1 || courseData.studentType === 'partial') ? 'partial' : 'all';
formData.selectedClasses = courseData.target ? courseData.target.split(',') : (courseData.selectedClasses || []);
//
const tempCourseDescription = courseData.description || courseData.courseDescription || '';
if (tempCourseDescription) {
if (editorRef.value) {
//
editorRef.value.setHtml(tempCourseDescription);
formData.courseDescription = tempCourseDescription;
} else {
//
pendingCourseDescription.value = tempCourseDescription;
}
}
// 0/1
formData.stopOnLeave = courseData.pauseExit !== undefined ?
Boolean(Number(courseData.pauseExit)) : true;
formData.videoSpeedControl = courseData.allowSpeed !== undefined ?
Boolean(Number(courseData.allowSpeed)) : false;
formData.showVideoText = courseData.showSubtitle !== undefined ?
Boolean(Number(courseData.showSubtitle)) : true;
// 使
formData.pointsEnabled = courseData.pointsEnabled !== undefined ? courseData.pointsEnabled : true;
formData.earnPoints = courseData.earnPoints || 60;
formData.requiredPoints = courseData.requiredPoints || 60;
// URL
if (courseData.cover || courseData.courseCover) {
previewUrl.value = courseData.cover || courseData.courseCover;
formData.courseCover = null; // URL
}
message.success('课程数据加载成功');
return; // API
} catch (parseError) {
// 使
}
}
// store
} catch (error) { } catch (error) {
message.error('加载课程数据失败') message.error('加载课程数据失败')
console.error('Load course data error:', error)
} }
} }
// //
onMounted(() => { onMounted(() => {
// courseIdstore
if (isEditMode.value && courseId.value) { if (isEditMode.value && courseId.value) {
loadCourseData(courseId.value) loadCourseData()
} else if (route.query.courseData || courseStore.courseEditData) {
// 使IDstore
loadCourseData()
} }
}) })
// //
const categoryOptions = [ const categoryOptions: Ref<{ label: string; value: number }[]> = ref([])
{ 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 instructorOptions = [ const instructorOptions = ref([] as { label: string; value: string }[])
{ label: '李清林', value: '李清林' },
{ label: '刘树光', value: '刘树光' },
{ label: '肖蒙', value: '肖蒙' },
{ label: '张老师', value: '张老师' },
{ label: '王老师', value: '王老师' }
]
// //
const classOptions = [ const classOptions = [
@ -350,20 +409,6 @@ const classOptions = [
{ label: '全栈开发班', value: 'fullstack-class' } { 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 = () => { const triggerFileUpload = () => {
fileInput.value?.click() fileInput.value?.click()
@ -388,15 +433,14 @@ const handleFileChange = (event: Event) => {
// URL // URL
previewUrl.value = URL.createObjectURL(file) previewUrl.value = URL.createObjectURL(file)
//
formData.courseCover = file formData.courseCover = file
console.log('文件上传成功:', file.name)
} }
} }
const clearUpload = () => { const clearUpload = () => {
// URL // previewUrlURL
if (previewUrl.value) { if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value) URL.revokeObjectURL(previewUrl.value)
} }
@ -412,9 +456,25 @@ const clearUpload = () => {
// //
const handleCancel = () => { const handleCancel = () => {
//
courseStore.clearCourseEditData();
router.back() 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 () => { const handleSubmit = async () => {
try { try {
@ -424,7 +484,7 @@ const handleSubmit = async () => {
return return
} }
if (!formData.courseCategory) { if (!formData.courseCategory || formData.courseCategory.length === 0) {
message.error('请选择课程分类') message.error('请选择课程分类')
return return
} }
@ -434,37 +494,155 @@ const handleSubmit = async () => {
return return
} }
if (!formData.startTime) {
message.error('请选择课程开始时间')
return
}
if (!formData.endTime) {
message.error('请选择课程结束时间')
return
}
if (!formData.courseCover && !previewUrl.value) { if (!formData.courseCover && !previewUrl.value) {
message.error('请上传课程封面') message.error('请上传课程封面')
return return
} }
console.log('表单数据:', formData) //
if (formData.studentType === 'partial' && formData.selectedClasses.length === 0) {
message.error('请选择参与的班级')
return
}
// API message.loading('正在保存课程信息...')
await new Promise(resolve => setTimeout(resolve, 1000))
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) { if (isEditMode.value) {
// //
message.success('课程更新成功!') const editData = {
console.log('更新课程ID:', courseId.value) 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 { } else {
// //
message.success('课程创建成功!') 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 || '创建失败,请重试')
}
} }
//
router.push('/teacher/course-management')
} catch (error) { } catch (error) {
console.error('提交失败:', error)
const errorMessage = isEditMode.value ? '更新失败,请重试' : '创建失败,请重试' const errorMessage = isEditMode.value ? '更新失败,请重试' : '创建失败,请重试'
message.error(errorMessage) message.error(errorMessage)
} }
} }
const goBack = () => { const goBack = () => {
//
courseStore.clearCourseEditData();
router.back() 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> </script>
<style scoped> <style scoped>

View File

@ -6,22 +6,29 @@
<h1>我的资源</h1> <h1>我的资源</h1>
</div> </div>
<div class="resources-actions"> <div class="resources-actions">
<button class="upload-btn" @click="handleUpload"> <n-button type="primary" @click="handleUpload">
上传文件 上传文件
</button> </n-button>
<button class="new-folder-btn" @click="handleNewFolder"> <n-button @click="handleNewFolder">
新建文件夹 新建文件夹
</button> </n-button>
<button class="recycle-bin-btn" @click="handleRecycleBin"> <n-button @click="handleRecycleBin">
<img src="/images/teacher/delete2.png" alt="回收站" class="action-icon"> <template #icon>
<img src="/images/teacher/delete2.png" alt="回收站" class="action-icon">
</template>
回收站 回收站
</button> </n-button>
<div class="search-container"> <div class="search-container">
<input type="text" class="search-input" placeholder="请输入关键字" v-model="searchKeyword" <n-input
v-model:value="searchKeyword"
placeholder="请输入关键字"
@keyup.enter="handleSearch"> @keyup.enter="handleSearch">
<button class="search-btn" @click="handleSearch"> <template #suffix>
搜索 <n-icon>
</button> <Search />
</n-icon>
</template>
</n-input>
</div> </div>
</div> </div>
</div> </div>
@ -30,27 +37,19 @@
<!-- 文件网格 --> <!-- 文件网格 -->
<div class="files-grid"> <div class="files-grid">
<div v-for="file in filteredFiles" :key="file.id" class="file-item" <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"> @mouseleave="handleItemMouseLeave">
<!-- 文件操作菜单 --> <!-- 文件操作菜单 -->
<div class="file-menu"> <div class="file-menu">
<button class="file-menu-btn" @click.stop="toggleMenu(file.id)"> <n-popselect
<img src="/images/profile/more.png" alt="更多操作" class="more-icon"> :options="fileMenuOptions"
</button> @update:value="(value: string) => handleFileMenuSelect(file, value)"
<div class="file-menu-dropdown" v-if="showMenuFor === file.id"> trigger="click"
<div class="menu-item" @click="handleEdit(file)"> placement="bottom-end">
<img class="menu-icon" src="/images/teacher/edit.png" alt="编辑"> <n-button text @click.stop>
<span>编辑</span> <img src="/images/profile/more.png" alt="更多操作" class="more-icon">
</div> </n-button>
<div class="menu-item" @click="handleMove(file)"> </n-popselect>
<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>
</div> </div>
<!-- 文件图标 --> <!-- 文件图标 -->
@ -62,8 +61,14 @@
<div v-if="editingId !== file.id" class="file-name" :title="file.name" @click.stop="startEdit(file)"> <div v-if="editingId !== file.id" class="file-name" :title="file.name" @click.stop="startEdit(file)">
{{ file.name }} {{ file.name }}
</div> </div>
<input v-else class="file-name-input" type="text" v-model="editName" @keyup.enter="saveEdit(file)" <n-input v-else
@keyup.esc="cancelEdit" @blur="saveEdit(file)" :maxlength="50" autofocus /> v-model:value="editName"
@keyup.enter="saveEdit(file)"
@keyup.esc="cancelEdit"
@blur="saveEdit(file)"
:maxlength="50"
autofocus
class="file-name-input" />
<!-- 文件详情由全局定位卡片显示 --> <!-- 文件详情由全局定位卡片显示 -->
</div> </div>
@ -80,8 +85,8 @@
<template #body> <template #body>
<div style="display:flex; align-items:center; gap:12px;"> <div style="display:flex; align-items:center; gap:12px;">
<span style="width:90px; text-align:right; color:#333; font-size:14px;">文件夹名称</span> <span style="width:90px; text-align:right; color:#333; font-size:14px;">文件夹名称</span>
<input type="text" v-model="newFolderName" placeholder="请输入文件夹名称" <n-input v-model:value="newFolderName" placeholder="请输入文件夹名称"
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;"> style="flex:1;" />
</div> </div>
</template> </template>
</RecycleConfirmModal> </RecycleConfirmModal>
@ -99,11 +104,8 @@
<template #body> <template #body>
<div style="display:flex; align-items:center; gap:12px;"> <div style="display:flex; align-items:center; gap:12px;">
<span style="width:90px; text-align:right; color:#333; font-size:14px;">选择文件夹</span> <span style="width:90px; text-align:right; color:#333; font-size:14px;">选择文件夹</span>
<select v-model="moveTargetFolder" <n-select v-model:value="moveTargetFolder" placeholder="请选择文件夹"
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;"> :options="folderOptions" style="flex:1;" />
<option value="">请选择文件夹</option>
<option v-for="f in folderFiles" :key="f.id" :value="f.name">{{ f.name }}</option>
</select>
</div> </div>
</template> </template>
</RecycleConfirmModal> </RecycleConfirmModal>
@ -112,10 +114,12 @@
</template> </template>
<script setup lang="ts"> <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 FileInfoCard from '@/components/admin/FileInfoCard.vue'
import UploadFileModal from '@/views/teacher/course/UploadFileModal.vue' import UploadFileModal from '@/views/teacher/course/UploadFileModal.vue'
import RecycleConfirmModal from '@/views/teacher/resource/RecycleConfirmModal.vue' import RecycleConfirmModal from '@/views/teacher/resource/RecycleConfirmModal.vue'
import { Search } from '@vicons/ionicons5'
// //
interface FileItem { interface FileItem {
@ -129,7 +133,6 @@ interface FileItem {
// //
const searchKeyword = ref('') const searchKeyword = ref('')
const showMenuFor = ref<string | null>(null)
const hoveredFile = ref<string | null>(null) const hoveredFile = ref<string | null>(null)
const showNewFolderModal = ref(false) const showNewFolderModal = ref(false)
const showUploadModal = 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 showMoveModal = ref(false)
const moveTargetFolder = ref('') 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[]>([ 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) => { const handleEdit = (file: FileItem) => {
console.log('编辑文件:', file.name) console.log('编辑文件:', file.name)
showMenuFor.value = null
} }
const handleMove = (file: FileItem) => { const handleMove = (file: FileItem) => {
console.log('移动文件:', file.name) console.log('移动文件:', file.name)
showMenuFor.value = null
showMoveModal.value = true showMoveModal.value = true
} }
@ -222,7 +236,21 @@ const handleDelete = (file: FileItem) => {
files.value.splice(index, 1) 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 = () => { const handleSearch = () => {
@ -275,9 +303,7 @@ const closeUploadModal = () => {
// //
onMounted(() => { onMounted(() => {
document.addEventListener('click', () => { // 使 Popselect
showMenuFor.value = null
})
}) })
// //
@ -355,99 +381,14 @@ const handleItemMouseLeave = () => {
gap: 12px; gap: 12px;
} }
.upload-btn, .action-icon {
.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 {
width: 14px; width: 14px;
height: 14px; height: 14px;
display: inline-block; display: inline-block;
} }
.search-container { .search-container {
display: flex; width: 200px;
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;
} }
@ -488,57 +429,11 @@ const handleItemMouseLeave = () => {
z-index: 10; 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 { .more-icon {
width: 4px; width: 4px;
height: 12px; 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 { .menu-icon {
width: 12px; width: 12px;
height: 12px; height: 12px;
@ -564,21 +459,7 @@ const handleItemMouseLeave = () => {
.file-name-input { .file-name-input {
margin-top: -20px; margin-top: -20px;
width: 100%;
max-width: 140px; 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 { .file-details {
position: absolute; position: absolute;
@ -624,90 +505,6 @@ const handleItemMouseLeave = () => {
z-index: 2000; 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) { @media (max-width: 768px) {
.resources-header { .resources-header {

View File

@ -211,11 +211,11 @@
clearable clearable
/> />
</n-form-item> </n-form-item>
<n-form-item label="所在学" path="college"> <n-form-item label="所在学" path="college">
<n-select <n-select
v-model:value="formData.college" v-model:value="formData.college"
:options="collegeOptions" :options="collegeOptions"
placeholder="请选择学" placeholder="请选择学"
clearable clearable
/> />
</n-form-item> </n-form-item>
@ -351,6 +351,14 @@ const props = withDefaults(defineProps<Props>(), {
classId: null classId: null
}) })
//
interface Emits {
(event: 'class-changed'): void
}
// emit
const emit = defineEmits<Emits>()
// //
interface StudentItem { interface StudentItem {
id: string id: string
@ -438,15 +446,15 @@ const rules: FormRules = {
college: [ college: [
{ required: true, message: '请选择所在学院', trigger: 'blur' } { required: true, message: '请选择所在学院', trigger: 'blur' }
], ],
// className: [ className: [
// { {
// required: true, required: true,
// type: 'array', type: 'array',
// min: 1, min: 1,
// message: '', message: '请选择至少一个班级',
// trigger: 'blur' trigger: 'blur'
// } }
// ] ]
} }
// //
@ -457,43 +465,7 @@ const classRules: FormRules = {
} }
// //
const masterClassList = ref<ClassItem[]>([ 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 collegeOptions = ref([ const collegeOptions = ref([
@ -549,7 +521,7 @@ const classOptions = computed(() =>
const classSelectOptions = computed(() => const classSelectOptions = computed(() =>
masterClassList.value.map(item => ({ masterClassList.value.map(item => ({
label: item.className, label: item.className,
value: item.className // 使 value: item.id
})) }))
) )
@ -576,8 +548,21 @@ const columns: DataTableColumns<StudentItem> = [
{ {
title: '班级', title: '班级',
key: 'className', key: 'className',
width: 120, width: 150,
align: 'center' 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: '所在学院', title: '所在学院',
@ -846,23 +831,21 @@ const handleDeleteStudent = (row: StudentItem) => {
negativeText: '取消', negativeText: '取消',
onPositiveClick: async () => { onPositiveClick: async () => {
try { try {
// API if (!props.classId) {
await new Promise(resolve => setTimeout(resolve, 500)) message.error('班级ID不存在无法删除学员')
return
}
// API
await ClassApi.removeStudent(props.classId.toString(), row.id)
const studentName = row.studentName 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}`) message.success(`已删除学员:${studentName}`)
// //
loadData(props.classId) loadData(props.classId)
} catch (error) { } catch (error) {
console.error('删除学员失败:', error)
message.error('删除失败,请重试') message.error('删除失败,请重试')
} }
} }
@ -927,6 +910,30 @@ const isCurrentClass = (classValue: string) => {
return classOption?.id === classValue 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 // ID
const generateInviteCode = (classId: string) => { const generateInviteCode = (classId: string) => {
// ID // ID
@ -1040,34 +1047,20 @@ const handleAddClass = async () => {
const classId = currentEditId.value const classId = currentEditId.value
const className = classFormData.value.className const className = classFormData.value.className
await ClassApi.editClass({ id: classId, name: 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}`) message.success(`已将班级重命名为:${className}`)
//
emit('class-changed')
} else { } else {
// //
const className = classFormData.value.className const className = classFormData.value.className
const res = await ClassApi.createClass({ name: className, course_id: courseId.value }) await ClassApi.createClass({ name: className, course_id: courseId.value })
// idres.data.id
const newId = res.data?.id || (masterClassList.value.length + 1).toString()
const newClass: ClassItem = {
id: newId,
className,
studentCount: 0,
creator: '王建国',
createTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.').replace(',', '')
}
masterClassList.value.push(newClass)
message.success(`已添加班级:${className}`) message.success(`已添加班级:${className}`)
//
emit('class-changed')
} }
//
await loadClassList()
closeAddClassModal() closeAddClassModal()
} catch (error) { } catch (error) {
message.error('请检查表单信息') message.error('请检查表单信息')
@ -1099,11 +1092,11 @@ const handleDeleteClass = (classItem: any) => {
try { try {
await ClassApi.deleteClass(classItem.id) await ClassApi.deleteClass(classItem.id)
message.success(`已删除班级:${classItem.className}`) message.success(`已删除班级:${classItem.className}`)
//
const index = masterClassList.value.findIndex(item => item.id === classItem.id) //
if (index > -1) { await loadClassList()
masterClassList.value.splice(index, 1) //
} emit('class-changed')
} catch (error) { } catch (error) {
message.error('删除失败,请重试') message.error('删除失败,请重试')
} }
@ -1111,98 +1104,107 @@ const handleDeleteClass = (classItem: any) => {
}) })
} }
// //
const loadData = async (classId?: number | null) => { const loadClassList = async () => {
loading.value = true
try { try {
await new Promise(resolve => setTimeout(resolve, 500)) console.log('🚀 开始加载班级列表数据...')
const response = await ClassApi.queryClassList({ course_id: null })
// ID // API
let mockData: StudentItem[] = [] 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(',', '')
}))
if (classId === null || classId === undefined) { masterClassList.value = transformedClassData
// console.log(`✅ 成功加载班级列表,共 ${transformedClassData.length} 个班级`)
mockData = [] } catch (error) {
} else { console.error('❌ 加载班级列表失败:', error)
// ID message.error('加载班级列表失败,请重试')
const classDataMap: Record<number, StudentItem[]> = { masterClassList.value = []
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'
}
]
}
mockData = classDataMap[classId] || [] //
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
} }
data.value = mockData loading.value = true
try {
if (classId === null || classId === undefined) {
//
data.value = []
totalStudents.value = 0
console.log('📝 未选择班级,显示空数据')
} else {
// API
console.log(`📡 正在获取班级 ${classId} 的学生数据...`)
const response = await ClassApi.getClassStudents(classId.toString())
// // API
totalStudents.value = mockData.length 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(',', '') : '未知时间'
}))
console.log(`加载班级 ${classId} 的数据,共 ${mockData.length} 名学员`) data.value = transformedData
} catch (error) { totalStudents.value = transformedData.length
console.error('加载数据失败:', error)
message.error('加载数据失败,请重试') console.log(`✅ 成功加载班级 ${classId} 的数据,共 ${transformedData.length} 名学员`)
} finally { }
loading.value = false } catch (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) => { (newClassId, oldClassId) => {
console.log(`班级ID从 ${oldClassId} 变更为 ${newClassId}`) console.log(`班级ID从 ${oldClassId} 变更为 ${newClassId}`)
if (newClassId !== oldClassId) { if (newClassId !== oldClassId) {
// // watch
selectedDepartment.value = newClassId ? newClassId.toString() : '' selectedDepartment.value = newClassId ? newClassId.toString() : ''
loadData(newClassId) loadData(newClassId)
} }
@ -1235,21 +1237,27 @@ watch(
{ immediate: false } // onMounted { immediate: false } // onMounted
) )
// / // /
// propsclassId使props
watch( watch(
() => selectedDepartment.value, () => selectedDepartment.value,
(newDepartmentId, oldDepartmentId) => { (newDepartmentId, oldDepartmentId) => {
console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`) console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`)
if (newDepartmentId !== oldDepartmentId) { // props.classId
// 使使classId // props.classIdprops
const targetClassId = newDepartmentId ? Number(newDepartmentId) : props.classId const currentPropsClassId = props.classId?.toString()
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
const targetClassId = newDepartmentId ? Number(newDepartmentId) : null
loadData(targetClassId) loadData(targetClassId)
} }
}, },
{ immediate: false } { immediate: false }
) )
onMounted(() => { onMounted(async () => {
//
await loadClassList()
// 使使classId使 // 使使classId使
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value) const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
loadData(initialClassId) loadData(initialClassId)
@ -1267,7 +1275,8 @@ defineExpose({
openAddClassModal, openAddClassModal,
handleRenameClass, handleRenameClass,
handleDeleteClass, handleDeleteClass,
openInviteModal openInviteModal,
loadClassList
}) })
</script> </script>
@ -1622,4 +1631,31 @@ defineExpose({
font-size: 14px; font-size: 14px;
text-align: center; 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> </style>

View File

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

View File

@ -2,53 +2,62 @@
<div class="modal-container flex-col"> <div class="modal-container flex-col">
<h2 class="modal-title">添加课件</h2> <h2 class="modal-title">添加课件</h2>
<n-divider /> <n-divider />
<div class="upload-section flex-row"> <div class="upload-section">
<div class="label-group flex-col justify-between"> <div class="upload-row">
<span class="upload-path-label">上传路径</span> <div class="upload-item">
<span class="upload-file-label">上传文件</span> <label class="upload-label">资源分类</label>
</div> <div class="upload-control">
<div class="button-group flex-col justify-between"> <n-select
<n-popselect :options="categoryOptions"
:options="folderOptions" v-model:value="selectedCategory"
trigger="click" placeholder="请选择分类"
placement="bottom-start" @update:value="handleCategorySelect"
v-model:value="selectedFolder" class="category-select"
@update:value="handleFolderSelect" />
>
<div class="select-path-button flex-col">
<span class="button-text file-input-label">选择路径</span>
</div> </div>
</n-popselect> <div class="upload-display">
<n-popselect <span class="selected-value">{{ getSelectedCategoryName() }}</span>
:options="uploadOptions"
trigger="click"
placement="bottom-start"
@update:value="handleUploadOptionSelect"
:render-label="renderUploadOption"
>
<div class="select-file-button flex-col">
<span class="button-text file-input-label">选择文件</span>
</div> </div>
</n-popselect> </div>
<!-- 隐藏的文件输入框 -->
<input
ref="localFileInput"
type="file"
@change="handleLocalFileUpload"
style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
/>
<input
ref="resourceFileInput"
type="file"
@change="handleResourceFileUpload"
style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
/>
</div> </div>
<div class="folder-display flex-col justify-between">
<span class="folder-name">{{ getSelectedFolderName() }}</span> <div class="upload-row">
<div class="upload-item">
<label class="upload-label">上传文件</label>
<div class="upload-control">
<n-popselect
:options="uploadOptions"
trigger="click"
placement="bottom-start"
@update:value="handleUploadOptionSelect"
:render-label="renderUploadOption"
>
<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> </div>
<!-- 隐藏的文件输入框 -->
<input
ref="localFileInput"
type="file"
@change="handleLocalFileUpload"
style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
/>
<input
ref="resourceFileInput"
type="file"
@change="handleResourceFileUpload"
style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
/>
</div> </div>
<div class="supported-formats flex-row"> <div class="supported-formats flex-row">
<span class="formats-label">支持格式</span> <span class="formats-label">支持格式</span>
@ -76,9 +85,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, h } from 'vue' 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([ const uploadOptions = ref([
@ -90,18 +111,6 @@ const uploadOptions = ref([
const localFileInput = ref<HTMLInputElement>() const localFileInput = ref<HTMLInputElement>()
const resourceFileInput = 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) => { const renderUploadOption = (option: any) => {
return h('span', { return h('span', {
@ -126,19 +135,19 @@ const handleUploadOptionSelect = (value: string) => {
} }
} }
// //
const getSelectedFolderName = () => { const getSelectedCategoryName = () => {
if (!selectedFolder.value) { if (!selectedCategory.value) {
return '请选择文件夹' return '请选择分类'
} }
const folder = folderOptions.value.find(option => option.value === selectedFolder.value) const category = categoryOptions.value.find(option => option.value === selectedCategory.value)
return folder ? folder.label : '未知文件夹' return category ? category.label : '未知分类'
} }
// //
const handleFolderSelect = (value: string) => { const handleCategorySelect = (value: string) => {
console.log('选中的文件夹:', value) console.log('选中的分类:', value)
// //
} }
// //
@ -146,7 +155,9 @@ const handleLocalFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement const target = event.target as HTMLInputElement
const files = target.files const files = target.files
if (files && files.length > 0) { 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 target = event.target as HTMLInputElement
const files = target.files const files = target.files
if (files && files.length > 0) { 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 { .modal-container {
position: relative; position: relative;
width: 1076px; width: 1076px;
height: 623px; /* height: 623px; */
background: #FFFFFF ; background: #FFFFFF ;
background-size: 100% 100%; background-size: 100% 100%;
margin: 0 auto; margin: 0 auto;
@ -205,146 +218,66 @@ const handleConfirm = () => {
.upload-section { .upload-section {
width: 1028px; width: 1028px;
height: 160px; background: #FCFCFC;
background: #FCFCFC ;
background-size: 100% 100%;
border: 1px solid rgb(233, 233, 233); border: 1px solid rgb(233, 233, 233);
border-radius: 8px;
margin: 0 0 0 22px; margin: 0 0 0 22px;
padding: 24px;
} }
.label-group { .upload-row {
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;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; margin-bottom: 24px;
color: white;
}
.button-text1{
color: #0288D1 ;
}
.button-text2{
color: #FFFFFF ;
} }
.select-file-button { .upload-row:last-child {
height: 38px; margin-bottom: 0;
background: #0288D1 ;
background-size: 100% 100%;
margin-top: 22px;
width: 123px;
} }
.upload-item {
display: flex;
.folder-display { align-items: center;
width: 171px; width: 100%;
height: 106px; gap: 20px;
margin: 39px 604px 0 2px;
} }
.folder-name { .upload-label {
width: 72px; width: 90px;
height: 22px;
overflow-wrap: break-word;
color: rgba(6, 35, 51, 1); color: rgba(6, 35, 51, 1);
font-size: 18px; font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif; font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
line-height: 22px; flex-shrink: 0;
margin-left: 12px;
} }
.existing-folders { .upload-control {
width: 171px; width: 200px;
height: 80px; flex-shrink: 0;
background: #FFFFFF;
background-size: 289px 198px;
margin-top: 4px;
} }
.folder-item { .upload-display {
width: 112px; flex: 1;
height: 17px; padding-left: 20px;
overflow-wrap: break-word; }
color: rgba(51, 51, 51, 1);
font-size: 14px; .selected-value,
.file-info {
color: rgba(6, 35, 51, 1);
font-size: 16px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif; font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal; font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 17px;
margin: 13px 0 0 29px;
} }
.folder-item { .category-select {
width: 112px; width: 100%;
height: 17px; }
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1); .upload-button {
font-size: 14px; width: 100%;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif; height: 38px;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 17px;
margin: 14px 0 19px 29px;
} }
.supported-formats { .supported-formats {
@ -468,6 +401,13 @@ const handleConfirm = () => {
line-height: 22px; line-height: 22px;
margin: 0; margin: 0;
} }
.button-text1{
color: #0288D1 ;
}
.button-text2{
color: #FFFFFF ;
}
.flex-col { .flex-col {
display: flex; display: flex;
flex-direction: column; 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 { .file-input-label {
cursor: pointer; cursor: pointer;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,8 @@
<span class="class-title">班级管理</span> <span class="class-title">班级管理</span>
<n-collapse :default-expanded-names="['1']"> <n-collapse :default-expanded-names="['1']">
<template #header-extra> <template #header-extra>
<n-popselect <n-popselect trigger="hover" placement="bottom-start" :options="classMenuOptions"
trigger="hover" @update:value="handleClassMenuSelect">
placement="bottom-start"
:options="classMenuOptions"
@update:value="handleClassMenuSelect"
>
<n-icon style="cursor: pointer;"> <n-icon style="cursor: pointer;">
<EllipsisVertical /> <EllipsisVertical />
</n-icon> </n-icon>
@ -21,20 +17,11 @@
</n-icon> </n-icon>
</template> </template>
<n-collapse-item title="班级管理" name="1"> <n-collapse-item title="班级管理" name="1">
<div <div class="class-item" :class="{ active: activeClassId === value.id }" v-for="value in classList"
class="class-item" :key="value.id" @click="handleClassClick(value.id)">
:class="{ active: activeClassId === value.id }"
v-for="value in classList"
:key="value.id"
@click="handleClassClick(value.id)"
>
<div>{{ value.name }}</div> <div>{{ value.name }}</div>
<n-popselect <n-popselect trigger="hover" placement="bottom-start" :options="getClassItemOptions()"
trigger="hover" @update:value="(selectedValue: string) => handleClassItemMenuSelect(selectedValue, value.id)">
placement="bottom-start"
:options="getClassItemOptions()"
@update:value="(selectedValue: string) => handleClassItemMenuSelect(selectedValue, value.id)"
>
<n-icon style="cursor: pointer;"> <n-icon style="cursor: pointer;">
<EllipsisVertical /> <EllipsisVertical />
</n-icon> </n-icon>
@ -44,7 +31,9 @@
</n-collapse> </n-collapse>
</div> </div>
<div class="class-right"> <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>
</div> </div>
</template> </template>
@ -52,13 +41,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import ClassManagement from '@/components/teacher/ClassManagement.vue' import ClassManagement from '@/components/teacher/ClassManagement.vue'
import { CaretForward, EllipsisVertical } from '@vicons/ionicons5' import { CaretForward, EllipsisVertical } from '@vicons/ionicons5'
import { ref } from "vue" import { onMounted, ref } from "vue"
import { ClassApi } from '@/api/modules/teachCourse'
const classList = ref([ const classList = ref<Array<{ id: number; name: string }>>([])
{ id: 1, name: "班级一" },
{ id: 2, name: "班级二" },
{ id: 3, name: "班级三" },
])
// ID // ID
const activeClassId = ref<number | null>(1) const activeClassId = ref<number | null>(1)
@ -92,7 +78,13 @@ const getClassItemOptions = () => [
// //
const handleClassClick = (classId: number) => { const handleClassClick = (classId: number) => {
activeClassId.value = classId 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?.({ classManagementRef.value.handleDeleteClass?.({
id: classId.toString(), id: classId.toString(),
className: selectedClass.name, className: selectedClass.name,
studentCount: 0,
creator: '王建国',
createTime: '2025.09.02 09:11'
}) })
break 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> </script>
<style scoped> <style scoped>
@ -169,7 +180,7 @@ const handleClassItemMenuSelect = (value: string, classId: number) => {
flex: 1; flex: 1;
} }
.class-item{ .class-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -178,7 +189,7 @@ const handleClassItemMenuSelect = (value: string, classId: number) => {
border-radius: 6px; border-radius: 6px;
} }
.active{ .active {
background-color: #F5F9FC; background-color: #F5F9FC;
color: #0288D1; color: #0288D1;
} }