feat:对接部分课程和班级接口

This commit is contained in:
yuk255 2025-09-11 14:34:30 +08:00
parent 7fb049d31d
commit 7911565249
16 changed files with 1497 additions and 257 deletions

View File

@ -0,0 +1,309 @@
// 教师端课程相关API接口
import { ApiRequest } from '../request'
import type {
ApiResponse,
ApiResponseWithResult,
} from '../types'
// 课程基础类型定义
export interface TeachCourse {
id?: string
name?: string | null
cover?: string | null
video?: string | null
school?: string | null
description?: string | null
type?: number | null
target?: string | null
difficulty?: number | null
subject?: string | null
outline?: string | null
prerequisite?: string | null
reference?: string | null
arrangement?: string | null
start_time?: string | null
end_time?: string | null
enroll_count?: number | null
max_enroll?: number | null
status?: number | null
question?: string | null
}
// 新建课程请求参数
export interface CreateCourseRequest {
name?: string | null
cover?: string | null
video?: string | null
school?: string | null
description?: string | null
type?: number | null
target?: string | null
difficulty?: number | null
subject?: string | null
outline?: string | null
prerequisite?: string | null
reference?: string | null
arrangement?: string | null
start_time?: string | null
end_time?: string | null
enroll_count?: number | null
max_enroll?: number | null
status?: number | null
question?: string | null
pause_exit: string
allow_speed: string
show_subtitle: string
}
// 编辑课程请求参数
export interface EditCourseRequest extends CreateCourseRequest {
id: string
}
// 查询教师课程列表参数
export interface TeacherCourseListParams {
keyword?: string // 课程名关键词
status?: string // 课程状态0 未开始1进行中2已结束
}
// 批量添加学生请求参数
export interface AddStudentsRequest {
ids: string // 用户id多个用英文逗号拼接例如1955366202649014274,3d464b4ea0d2491aab8a7bde74c57e95
}
// 文件上传响应类型
export interface UploadResponse {
url: string
filename: string
size: number
}
// 学生信息类型
export interface CourseStudent {
id: string
username: string
realname: string
phone?: string
email?: string
avatar?: string
enrollTime?: string
}
/**
* API模块
*/
export class TeachCourseApi {
/**
*
*/
static async createCourse(data: CreateCourseRequest): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送新建课程请求:', { url: '/aiol/aiolCourse/add', data })
const response = await ApiRequest.post<any>('/aiol/aiolCourse/add', data)
console.log('📝 新建课程响应:', response)
return response
} catch (error) {
console.error('❌ 新建课程失败:', error)
throw error
}
}
/**
*
*/
static async getTeacherCourseList(params?: TeacherCourseListParams): Promise<ApiResponseWithResult<TeachCourse[]>> {
try {
const response = await ApiRequest.get<{ result: TeachCourse[] }>('/aiol/aiolCourse/teacher_list', params)
return response
} catch (error) {
console.error('❌ 查询教师课程列表失败:', error)
throw error
}
}
/**
*
*/
static async editCourse(data: EditCourseRequest): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送编辑课程请求:', { url: '/aiol/aiolCourse/edit', data })
const response = await ApiRequest.put<any>('/aiol/aiolCourse/edit', data)
console.log('✏️ 编辑课程响应:', response)
return response
} catch (error) {
console.error('❌ 编辑课程失败:', error)
throw error
}
}
/**
*
*/
static async deleteCourse(id: string): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送删除课程请求:', { url: '/aiol/aiolCourse/delete', id })
const response = await ApiRequest.delete<any>('/aiol/aiolCourse/delete', {
params: { id }
})
console.log('🗑️ 删除课程响应:', response)
return response
} catch (error) {
console.error('❌ 删除课程失败:', error)
throw error
}
}
/**
*
*/
static async uploadVideo(file: File): Promise<ApiResponse<UploadResponse>> {
try {
console.log('🚀 发送视频上传请求:', { url: '/aiol/aiolResource/upload', fileName: file.name })
const formData = new FormData()
formData.append('file', file)
const response = await ApiRequest.post<UploadResponse>('/aiol/aiolResource/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
console.log('📹 视频上传响应:', response)
return response
} catch (error) {
console.error('❌ 视频上传失败:', error)
throw error
}
}
/**
*
*/
static async addStudentsToCourse(courseId: string, data: AddStudentsRequest): Promise<ApiResponse<any>> {
try {
console.log('🚀 发送批量导入学生请求:', {
url: `/aiol/aiolCourse/${courseId}/add_students`,
courseId,
data
})
const response = await ApiRequest.post<any>(`/aiol/aiolCourse/${courseId}/add_students`, data)
console.log('👥 批量导入学生响应:', response)
return response
} catch (error) {
console.error('❌ 批量导入学生失败:', error)
throw error
}
}
/**
*
*/
static async getCourseStudents(courseId: string): Promise<ApiResponse<CourseStudent[]>> {
try {
console.log('🚀 发送查询课程学生列表请求:', {
url: `/aiol/aiolCourse/${courseId}/get_students`,
courseId
})
const response = await ApiRequest.get<CourseStudent[]>(`/aiol/aiolCourse/${courseId}/get_students`)
console.log('👨‍🎓 课程学生列表响应:', response)
return response
} catch (error) {
console.error('❌ 查询课程学生列表失败:', error)
throw error
}
}
}
// 默认导出
export default TeachCourseApi
/**
* API
*/
export interface ClassInfo {
id?: string;
name: string;
course_id?: string;
}
export interface EditClassRequest {
id: string;
name: string;
}
export interface ImportStudentsRequest {
ids: string; // 逗号分隔的学生id
}
export class ClassApi {
/**
*
*/
static async createClass(data: { name: string; course_id: string|null }): Promise<ApiResponse<any>> {
return ApiRequest.post('/aiol/aiolClass/add', data);
}
/**
*
*/
static async editClass(data: EditClassRequest): Promise<ApiResponse<any>> {
return ApiRequest.put('/aiol/aiolClass/edit', data);
}
/**
*
*/
static async deleteClass(id: string): Promise<ApiResponse<any>> {
return ApiRequest.delete('/aiol/aiolClass/delete', { params: { id } });
}
/**
*
*/
static async importStudents(classId: string, data: ImportStudentsRequest): Promise<ApiResponse<any>> {
return ApiRequest.post(`/aiol/aiolClass/${classId}/import_students`, data);
}
/**
*
*/
static async getClassStudents(classId: string): Promise<ApiResponse<any>> {
return ApiRequest.get(`/aiol/aiolClass/${classId}/student_list`);
}
/**
*
*/
static async removeStudent(classId: string, studentId: string): Promise<ApiResponse<any>> {
return ApiRequest.delete(`/aiol/aiolClass/${classId}/remove_student/${studentId}`);
}
/**
*
*/
static async getCourseClasses(courseId: string): Promise<ApiResponse<any>> {
return ApiRequest.get(`/aiol/aiolCourse/${courseId}/class_list`);
}
/**
* excel导入学生TODO
*/
static async importStudentsExcel(classId: string, file: File): Promise<ApiResponse<any>> {
const formData = new FormData();
formData.append('file', file);
return ApiRequest.post(`/aiol/aiolClass/${classId}/import_students_excel`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
}

View File

@ -0,0 +1,223 @@
<template>
<n-modal v-model:show="showModal" preset="card" style="width: 800px;" title="资源库">
<div class="resource-library-container">
<div class="resource-list">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-item"
:class="{ 'selected': selectedResources.includes(resource.id) }"
@click="toggleResourceSelection(resource.id)"
>
<div class="resource-info">
<div class="resource-icon">
<img v-if="resource.type === 'ppt'" src="/images/teacher/课件.png" alt="PPT" />
<img v-else-if="resource.type === 'video'" src="/images/teacher/Image.png" alt="视频" />
<img v-else src="/images/teacher/文件格式.png" alt="文件" />
</div>
<div class="resource-details">
<div class="resource-name">{{ resource.name }}</div>
<div class="resource-meta">
<span class="resource-size">{{ resource.size }}</span>
<span class="resource-date">{{ resource.uploadDate }}</span>
</div>
</div>
</div>
<div class="selection-indicator" v-if="selectedResources.includes(resource.id)">
</div>
</div>
</div>
</div>
<template #footer>
<div class="modal-footer">
<n-button @click="handleCancel">取消</n-button>
<n-button type="primary" @click="handleConfirm" :disabled="selectedResources.length === 0">
确认选择 ({{ selectedResources.length }})
</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
interface Resource {
id: number;
name: string;
type: 'ppt' | 'video' | 'document';
size: string;
uploadDate: string;
url: string;
}
interface Props {
show: boolean;
}
interface Emits {
(e: 'update:show', value: boolean): void;
(e: 'confirm', selectedResources: Resource[]): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const showModal = computed({
get: () => props.show,
set: (value) => emit('update:show', value)
});
const selectedResources = ref<number[]>([]);
//
const resources = ref<Resource[]>([
{
id: 1,
name: '第一章课程介绍.pptx',
type: 'ppt',
size: '2.3 MB',
uploadDate: '2024-01-15',
url: '/resources/chapter1-intro.pptx'
},
{
id: 2,
name: '基础知识讲解视频.mp4',
type: 'video',
size: '156 MB',
uploadDate: '2024-01-14',
url: '/resources/basic-knowledge.mp4'
},
{
id: 3,
name: '实践操作演示.pptx',
type: 'ppt',
size: '4.1 MB',
uploadDate: '2024-01-13',
url: '/resources/practice-demo.pptx'
},
{
id: 4,
name: '高级技能培训视频.mp4',
type: 'video',
size: '298 MB',
uploadDate: '2024-01-12',
url: '/resources/advanced-skills.mp4'
}
]);
const toggleResourceSelection = (resourceId: number) => {
const index = selectedResources.value.indexOf(resourceId);
if (index > -1) {
selectedResources.value.splice(index, 1);
} else {
selectedResources.value.push(resourceId);
}
};
const handleConfirm = () => {
const selected = resources.value.filter(resource =>
selectedResources.value.includes(resource.id)
);
emit('confirm', selected);
handleCancel();
};
const handleCancel = () => {
selectedResources.value = [];
showModal.value = false;
};
//
watch(() => props.show, (newValue) => {
if (newValue) {
selectedResources.value = [];
}
});
</script>
<style scoped>
.resource-library-container {
max-height: 500px;
overflow-y: auto;
}
.resource-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.resource-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.resource-item:hover {
background: #f5f5f5;
border-color: #0288D1;
}
.resource-item.selected {
background: #e3f2fd;
border-color: #0288D1;
}
.resource-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.resource-icon img {
width: 32px;
height: 32px;
object-fit: contain;
}
.resource-details {
flex: 1;
}
.resource-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.resource-meta {
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.selection-indicator {
width: 24px;
height: 24px;
border-radius: 50%;
background: #0288D1;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -4,25 +4,71 @@
<div class="top">
<n-tabs v-model:value="activeTab" size="large">
<n-tab-pane name="ongoing" tab="进行中" />
<n-tab-pane name="finished" tab="已结束" />
<n-tab-pane name="draft" tab="草稿箱" />
<n-tab-pane name="finished" tab="已结束" />
</n-tabs>
<div class="actions">
<n-button type="primary" @click="navigateToCreateCourse">创建课程</n-button>
<div class="search-container">
<n-input v-model:value="searchValue" type="text" placeholder="请输入想要搜索的内容" />
<n-button type="primary" class="search-btn">搜索</n-button>
<n-button type="primary" class="search-btn" @click="handleSearch">搜索</n-button>
</div>
</div>
</div>
<!-- 主体 -->
<div class="course-container">
<div class="course-grid">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<n-spin size="large">
<template #description>
正在加载课程数据...
</template>
</n-spin>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<n-result
status="error"
title="加载失败"
:description="error"
>
<template #footer>
<n-space justify="center">
<n-button type="primary" @click="getCourseList(true)">
<template #icon>
<n-icon>
<Refresh />
</n-icon>
</template>
重新加载
</n-button>
</n-space>
</template>
</n-result>
</div>
<!-- 空状态 -->
<div v-else-if="courseList.length === 0" class="empty-container">
<n-empty
description="暂无课程数据"
size="large"
>
<template #extra>
<n-button type="primary" @click="navigateToCreateCourse">
创建课程
</n-button>
</template>
</n-empty>
</div>
<!-- 课程列表 -->
<div v-else class="course-grid">
<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 === '下架中' }">{{ course.status }}
<div class="section-title" :class="{ 'offline': course.status === 0 }">{{ course.statusText }}
</div>
<n-popselect
:options="getOptionsForCourse(course)"
@ -50,7 +96,7 @@
<!-- 底部翻页按钮 -->
<div class="pagination">
<!-- <div class="pagination">
<div class="pagination-content">
<div class="page-numbers">
<a href="#" class="page-number">首页</a>
@ -67,60 +113,186 @@
<a href="#" class="page-number">尾页</a>
</div>
</div>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { ref, h } from 'vue';
import { ref, h, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { EllipsisVerticalSharp } from '@vicons/ionicons5';
import { EllipsisVerticalSharp, Refresh } from '@vicons/ionicons5';
import { useMessage, useDialog } from 'naive-ui';
import TeachCourseApi, { type TeachCourse } from '@/api/modules/teachCourse';
//
interface CourseDisplayItem extends TeachCourse {
statusText?: string;
image?: string;
}
const router = useRouter();
const message = useMessage();
const dialog = useDialog();
//
const courseList = ref([
{ id: 1, name: '前端开发基础课程', status: '发布中', image: '/images/teacher/fj.png', students: 120 },
{ id: 2, name: 'Vue.js 实战教程', status: '发布中', image: '/images/teacher/fj.png', students: 95 },
{ id: 3, name: 'React 入门到精通', status: '发布中', image: '/images/teacher/fj.png', students: 87 },
{ id: 4, name: 'Node.js 后端开发', status: '下架中', image: '/images/teacher/fj.png', students: 65 },
{ id: 5, name: 'TypeScript 高级教程', status: '发布中', image: '/images/teacher/fj.png', students: 73 },
{ id: 6, name: 'JavaScript 设计模式', status: '发布中', image: '/images/teacher/fj.png', students: 56 },
{ id: 7, name: 'CSS 动画与特效', status: '下架中', image: '/images/teacher/fj.png', students: 42 },
{ id: 8, name: 'HTML5 新特性详解', status: '发布中', image: '/images/teacher/fj.png', students: 89 },
{ id: 9, name: 'Web 性能优化指南', status: '发布中', image: '/images/teacher/fj.png', students: 67 },
{ id: 10, name: '移动端适配实战', status: '发布中', image: '/images/teacher/fj.png', students: 54 },
{ id: 11, name: '微信小程序开发', status: '下架中', image: '/images/teacher/fj.png', students: 38 },
{ id: 12, name: 'Flutter 跨平台开发', status: '发布中', image: '/images/teacher/fj.png', students: 29 },
]);
//
const originalCourseList = ref<CourseDisplayItem[]>([]);
//
const courseList = ref<CourseDisplayItem[]>([]);
//
const loading = ref<boolean>(false);
//
const error = ref<string>('');
//
const getCourseList = async (forceRefresh: boolean = false) => {
try {
loading.value = true;
error.value = ''; //
const params = {
keyword: searchValue.value,
status: getStatusByTab(activeTab.value)
};
console.log('🔄 获取课程列表 - Tab:', activeTab.value, 'Status:', params.status, 'Keyword:', params.keyword);
const response = await TeachCourseApi.getTeacherCourseList(params);
if (response && response.data) {
// API
const courseData = response.data.result.map((course: TeachCourse): CourseDisplayItem => ({
...course,
//
statusText: getStatusText(course.status),
//
image: course.cover || '/images/teacher/fj.png'
}));
//
if (forceRefresh || originalCourseList.value.length === 0) {
originalCourseList.value = courseData;
courseList.value = courseData; // API
} else {
//
originalCourseList.value = courseData;
filterCourseList();
}
console.log('✅ 课程列表加载成功,共', courseData.length, '门课程');
}
} catch (err: any) {
console.error('获取课程列表失败:', err);
//
if (err.message?.includes('Network Error') || err.code === 'NETWORK_ERROR') {
error.value = '网络连接失败,请检查网络设置后重试';
} else if (err.response?.status === 401) {
error.value = '登录已过期,请重新登录';
} else if (err.response?.status === 403) {
error.value = '暂无权限访问此功能';
} else if (err.response?.status >= 500) {
error.value = '服务器内部错误,请稍后重试';
} else {
error.value = err.message || '加载课程数据失败,请稍后重试';
}
//
originalCourseList.value = [];
courseList.value = [];
//
message.error(error.value);
} finally {
loading.value = false;
}
};
//
const getStatusText = (status: number | null | undefined): string => {
switch (status) {
case 0: return '未开始';
case 1: return '进行中';
case 2: return '已结束';
default: return '未知';
}
};
//
const getStatusByTab = (tab: string): string => {
switch (tab) {
case 'ongoing': return '1'; //
case 'draft': return '0'; // 稿/
case 'finished': return '2'; //
default: return '';
}
};
//
const filterCourseList = () => {
let filtered = [...originalCourseList.value];
// API
if (searchValue.value?.trim()) {
const keyword = searchValue.value.trim().toLowerCase();
filtered = filtered.filter(course =>
course.name?.toLowerCase().includes(keyword) ||
course.description?.toLowerCase().includes(keyword)
);
}
courseList.value = filtered;
console.log('📊 过滤后的课程数量:', filtered.length);
};
const searchValue = ref<string>('')
const activeTab = ref<string>('ongoing')
//
watch(activeTab, async (newTab, oldTab) => {
console.log('📋 Tab切换:', oldTab, '->', newTab);
// tab
await getCourseList(true);
});
//
watch(searchValue, () => {
console.log('🔍 搜索关键词变化:', searchValue.value);
//
filterCourseList();
});
//
const navigateToCreateCourse = () => {
router.push('/teacher/course-create');
};
//
const handleSearch = async () => {
console.log('🔍 执行搜索:', searchValue.value);
//
await getCourseList(true);
};
//
const getOptionsForCourse = (course: any) => {
if (course.status === '发布中') {
const getOptionsForCourse = (course: CourseDisplayItem) => {
if (course.status === 1) { //
return [
{ label: '下架', value: 'offline', icon: '/images/teacher/下架.png' },
{ label: '编辑', value: 'edit', icon: '/images/teacher/小编辑.png' },
{ label: '移动', value: 'move', icon: '/images/teacher/移动.png' },
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
];
} else if (course.status === '下架中') {
} 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: 'delete', icon: '/images/teacher/删除.png' }
];
} else if (course.status === 2) { //
return [
{ label: '查看', value: 'view', icon: '/images/teacher/查看.png' },
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
];
}
return [];
};
@ -178,35 +350,104 @@ const handleOptionSelect = (value: string, course: any) => {
//
const handleDeleteCourse = (course: any) => {
//
if (course.status === 1) {
dialog.warning({
title: '无法删除',
content: `课程"${course.name}"正在进行中,请先下架课程后再删除。`,
positiveText: '先下架课程',
negativeText: '取消',
onPositiveClick: () => {
//
handleOfflineCourse(course);
}
});
return;
}
//
dialog.warning({
title: '确认删除',
content: `确定要删除课程"${course.name}"吗?此操作不可撤销。`,
positiveText: '确定删除',
negativeText: '取消',
onPositiveClick: () => {
onPositiveClick: async () => {
try {
//
await TeachCourseApi.deleteCourse(course.id);
//
const index = courseList.value.findIndex(c => c.id === course.id);
if (index > -1) {
courseList.value.splice(index, 1);
//
const originalIndex = originalCourseList.value.findIndex(c => c.id === course.id);
if (originalIndex > -1) {
originalCourseList.value.splice(originalIndex, 1);
}
}
message.success(`课程"${course.name}"已删除`);
} catch (error) {
console.error('删除课程失败:', error);
message.error('删除课程失败,请稍后重试');
}
}
});
};
//
const handleOfflineCourse = (course: any) => {
const handleOfflineCourse = (course: CourseDisplayItem) => {
if (!course.id) {
message.error('课程ID不存在无法下架');
return;
}
dialog.warning({
title: '确认下架',
content: `确定要下架课程"${course.name}"吗?下架后学员将无法继续学习,但可以重新上架。`,
positiveText: '确定下架',
negativeText: '取消',
onPositiveClick: async () => {
try {
// API -
const updatedData = {
id: course.id!,
name: course.name,
description: course.description,
status: 2 // 2=
};
await TeachCourseApi.editCourse(updatedData);
//
const targetCourse = courseList.value.find(c => c.id === course.id);
if (targetCourse) {
targetCourse.status = '下架中';
message.success(`课程"${course.name}"已下架`);
targetCourse.status = 2;
targetCourse.statusText = '已结束';
}
const originalCourse = originalCourseList.value.find(c => c.id === course.id);
if (originalCourse) {
originalCourse.status = 2;
originalCourse.statusText = '已结束';
}
message.success(`课程"${course.name}"已下架,现在可以删除了`);
} catch (error) {
console.error('下架课程失败:', error);
message.error('下架课程失败,请稍后重试');
}
}
});
};
//
const handlePublishCourse = (course: any) => {
const handlePublishCourse = (course: CourseDisplayItem) => {
const targetCourse = courseList.value.find(c => c.id === course.id);
if (targetCourse) {
targetCourse.status = '发布中';
targetCourse.status = 1; //
targetCourse.statusText = '进行中';
message.success(`课程"${course.name}"已发布`);
}
};
@ -262,6 +503,11 @@ const handleMoveCourse = (course: any) => {
}
});
};
onMounted(() => {
getCourseList();
});
</script>
@ -324,6 +570,32 @@ const handleMoveCourse = (course: any) => {
flex-direction: column;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
width: 100%;
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
width: 100%;
padding: 40px 20px;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
width: 100%;
padding: 40px 20px;
}
.course-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@ -552,9 +824,9 @@ const handleMoveCourse = (course: any) => {
gap: 30px;
}
.course-card {
/* .course-card {
aspect-ratio: 200 / 220;
}
} */
.course-info img {
width: 85%;

View File

@ -75,8 +75,7 @@
<!-- 排序 -->
<div class="form-item">
<label class="form-label required">排序:</label>
<n-select v-model:value="formData.sort" :options="sortOptions" placeholder="数字越小越排序靠前"
class="form-input" />
<n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" />
</div>
<!-- 课程结束时间 -->
@ -147,7 +146,7 @@
</div>
<!-- 积分设置 -->
<div class="form-item form-integral">
<div class="form-item form-integral" v-if="false">
<label class="form-label required">积分设置</label>
<div class="setting-container">
<n-switch v-model:value="formData.pointsEnabled" class="form-toggle" />
@ -189,6 +188,7 @@ import {
import '@wangeditor/editor/dist/css/style.css'
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import TeachCourseApi from '@/api/modules/teachCourse'
const router = useRouter()
const route = useRoute()
@ -351,18 +351,18 @@ const classOptions = [
]
//
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 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 = () => {

View File

@ -314,7 +314,8 @@
<script setup lang="ts">
import { ref, onMounted, h, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ClassApi } from '@/api/modules/teachCourse'
import { useRouter, useRoute } from 'vue-router'
import { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5'
import {
NDataTable,
@ -386,6 +387,7 @@ const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应
const message = useMessage()
const dialog = useDialog()
const router = useRouter()
const route = useRoute()
//
const selectedDepartment = ref('')
@ -405,6 +407,7 @@ const currentEditId = ref('')
const isRenameMode = ref(false)
const showBatchTransferModal = ref(false)
const selectedRowKeys = ref<string[]>([]) // keys
const courseId = ref<string | null>(null) // ID
//
const formData = ref<FormData>({
@ -1028,24 +1031,30 @@ const closeAddClassModal = () => {
classFormData.value.className = ''
}
//
// /API
const handleAddClass = async () => {
try {
await classFormRef.value?.validate()
if (isRenameMode.value) {
//
const classIndex = masterClassList.value.findIndex(item => item.id === currentEditId.value)
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 = classFormData.value.className
masterClassList.value[classIndex].className = className
}
message.success(`已将班级重命名为:${classFormData.value.className}`)
message.success(`已将班级重命名为:${className}`)
} else {
//
const newId = (masterClassList.value.length + 1).toString()
const className = classFormData.value.className
const res = await ClassApi.createClass({ name: className, course_id: courseId.value })
// idres.data.id
const newId = res.data?.id || (masterClassList.value.length + 1).toString()
const newClass: ClassItem = {
id: newId,
className: classFormData.value.className,
className,
studentCount: 0,
creator: '王建国',
createTime: new Date().toLocaleString('zh-CN', {
@ -1057,12 +1066,9 @@ const handleAddClass = async () => {
}).replace(/\//g, '.').replace(',', '')
}
masterClassList.value.push(newClass)
message.success(`已添加班级:${classFormData.value.className}`)
message.success(`已添加班级:${className}`)
}
//
closeAddClassModal()
} catch (error) {
message.error('请检查表单信息')
}
@ -1082,7 +1088,7 @@ const handleRenameClass = (classItem: any) => {
showManageClassModal.value = false
}
//
// API
const handleDeleteClass = (classItem: any) => {
dialog.info({
title: '确认删除',
@ -1091,17 +1097,13 @@ const handleDeleteClass = (classItem: any) => {
negativeText: '取消',
onPositiveClick: async () => {
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
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)
}
} catch (error) {
message.error('删除失败,请重试')
}
@ -1251,6 +1253,13 @@ onMounted(() => {
// 使使classId使
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
loadData(initialClassId)
// id id
if(route.path.includes('/teacher/course-editor')){
console.log('当前路由路径:', route.path)
console.log('课程ID:', router.currentRoute.value.params.id)
courseId.value = router.currentRoute.value.params.id.toString()
}
})
//

View File

@ -36,6 +36,7 @@ import PersonalCenter from '@/components/admin/PersonalCenter.vue'
import CourseManagement from '@/components/admin/CourseManagement.vue'
import MyResources from '@/components/admin/MyResources.vue'
import StudentManagement from '@/components/admin/StudentManagement.vue'
import MessageCenter from '@/views/teacher/message/MessageCenter.vue'
// 课程管理子组件
import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue'
@ -205,26 +206,52 @@ const routes: RouteRecordRaw[] = [
]
},
{
path: 'practice',
name: 'Practice',
redirect: (to) => `/teacher/course-editor/${to.params.id}/practice/exam`,
meta: { title: '考试管理' },
children: [
{
path: 'exam',
path: 'practice/exam',
name: 'PracticeExam',
// component: () => import('../views/teacher/course/PracticeExam.vue'),
component: ExamLibrary,
meta: { title: '试卷' }
meta: { title: '试卷管理' }
},
{
path: 'review',
path: 'practice/exam/add',
name: 'PracticeAddExam',
component: AddExam,
meta: { title: '添加试卷' }
},
{
path: 'practice/exam/edit/:id',
name: 'PracticeEditExam',
component: AddExam,
meta: { title: '编辑试卷' }
},
{
path: 'practice/exam/preview',
name: 'PracticeExamPreview',
component: () => import('../views/teacher/ExamPages/ExamPreview.vue'),
meta: { title: '试卷预览' }
},
{
path: 'practice/exam/analysis',
name: 'PracticeExamAnalysis',
component: () => import('../views/teacher/ExamPages/ExamAnalysis.vue'),
meta: { title: '试卷分析' }
},
{
path: 'practice/review',
name: 'PracticeReview',
// component: () => import('../views/teacher/course/PracticeReview.vue'),
component: MarkingCenter,
meta: { title: '阅卷中心' }
}
]
},
{
path: 'practice/review/student-list/:paperId',
name: 'PracticeStudentList',
component: StudentList,
meta: { title: '阅卷页面' }
},
{
path: 'practice/review/grading/:examId/:studentId',
name: 'PracticeGradingPage',
component: GradingPage,
meta: { title: '批阅试卷' }
},
{
path: 'question-bank',
@ -313,6 +340,12 @@ const routes: RouteRecordRaw[] = [
component: MyResources,
meta: { title: '我的资源' }
},
{
path: 'message-center',
name: 'MessageCenter',
component: MessageCenter,
meta: { title: '消息中心' }
},
{
path: 'recycle-bin',
name: 'RecycleBin',

View File

@ -11,9 +11,9 @@
<div class="sidebar-container" v-if="!hideSidebar">
<!-- 头像 -->
<div class="avatar-container">
<img src="/images/activity/5.png" alt="头像" class="avatar">
<img :src="userStore.user?.avatar" :alt="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username" class="avatar">
<div class="avatar-text">
用户昵称~
{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}
</div>
</div>
@ -54,7 +54,7 @@
<!-- 学员中心 - 可展开菜单 -->
<div class="nav-item" :class="{ active: activeNavItem === 1 }" @click="toggleStudentMenu">
<div class="nav-item" :class="{ active: activeNavItem === 1 }" @click="toggleStudentMenu('/teacher/student-management/student-library')">
<img :src="activeNavItem === 1 ? '/images/teacher/学院管理(选中).png' : '/images/teacher/学员管理.png'" alt="">
<span>学员中心</span>
<n-icon class="expand-icon" :class="{ expanded: studentMenuExpanded }">
@ -82,7 +82,7 @@
</router-link>
<router-link to="/teacher/message-center" class="nav-item" :class="{ active: activeNavItem === 5 }"
@click="setActiveNavItem(5)">
<img :src="activeNavItem === 5 ? '/images/teacher/消息中心(选中).png' : '/images/teacher/消息中心.png'" alt="">
<img :src="activeNavItem === 5 ? '/images/profile/message-active.png' : '/images/profile/message.png'" alt="">
<span>消息中心</span>
</router-link>
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
@ -136,6 +136,9 @@
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ChevronDownOutline } from '@vicons/ionicons5'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const width = window.innerWidth;
@ -190,13 +193,14 @@ const toggleExamMenu = () => {
}
//
const toggleStudentMenu = () => {
const toggleStudentMenu = (path: string) => {
studentMenuExpanded.value = !studentMenuExpanded.value;
activeNavItem.value = 1;
//
if (studentMenuExpanded.value && !activeSubNavItem.value) {
activeSubNavItem.value = 'student-library';
router.push(path);
}
}

View File

@ -278,7 +278,7 @@
<script setup lang="ts">
import { computed, reactive, ref, onMounted, onUnmounted, watch } from 'vue';
import { createDiscreteApi } from 'naive-ui';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { AddCircle, SettingsOutline, TrashOutline, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue';
@ -295,6 +295,7 @@ const { dialog } = createDiscreteApi(['dialog'])
//
const router = useRouter()
const route = useRoute()
//
const goBack = () => {
@ -1079,8 +1080,16 @@ const previewExam = () => {
return;
}
//
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 practice
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/exam/preview`);
} else {
// 使
router.push('/teacher/exam-management/preview');
}
}
//

View File

@ -395,7 +395,18 @@ const handleTypeChange = (type: string) => {
//
const goBack = () => {
router.back();
//
const currentRoute = route.path;
const bankId = route.params.bankId;
if (currentRoute.includes('/course-editor/')) {
//
const courseId = route.params.id || route.params.courseId;
router.push(`/teacher/course-editor/${courseId}/question-bank/${bankId}/questions`);
} else {
//
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
}
};
//
@ -538,8 +549,16 @@ const createNewQuestion = async (bankId: string) => {
message.success('题目保存成功');
//
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
//
const courseId = route.params.id || route.params.courseId;
router.push(`/teacher/course-editor/${courseId}/question-bank/${bankId}/questions`);
} else {
//
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
}
} catch (error: any) {
console.error('创建题目流程失败:', error);

View File

@ -22,8 +22,9 @@
import { h, ref, VNode, computed } from 'vue';
import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui';
import type { DataTableColumns } from 'naive-ui';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
//
type Exam = {
@ -150,7 +151,36 @@ const examData = ref<Exam[]>([
const columns = createColumns({
handleAction: (action, row) => {
if(action === '试卷分析'){
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/exam/analysis?examId=${row.id}`);
} else {
router.push({ name: 'ExamAnalysis', query: { examId: row.id } });
}
return;
}
if(action === '批阅'){
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/review/student-list/${row.id}`);
} else {
router.push({ name: 'StudentList', params: { paperId: row.id } });
}
return;
}
if(action === '编辑'){
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/exam/edit/${row.id}`);
} else {
router.push({ name: 'EditExam', params: { id: row.id } });
}
return;
}
message.info(`执行操作: ${action} on row ${row.id}`);
@ -189,8 +219,16 @@ const paginationConfig = computed(() => ({
}));
const handleAddExam = () => {
//
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 practice
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/exam/add`);
} else {
// 使
router.push({ name: 'AddExam' });
}
};
</script>

View File

@ -135,7 +135,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { PersonOutline, CalendarOutline } from '@vicons/ionicons5'
//
@ -153,6 +153,7 @@ interface ExamItem {
//
const router = useRouter()
const route = useRoute()
//
const activeTab = ref('all')
@ -278,11 +279,19 @@ const handleDelete = (exam: ExamItem) => {
}
const handleAction = (exam: ExamItem) => {
// ID
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 practice
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/review/student-list/${exam.id}`);
} else {
// 使
router.push({
name: 'StudentList',
params: { paperId: exam.id }
})
});
}
}
const handlePageChange = (page: number) => {

View File

@ -54,7 +54,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
import { NButton, NSpace, NSelect, useMessage, useDialog } from 'naive-ui';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import ImportModal from '@/components/common/ImportModal.vue';
import { ExamApi } from '@/api';
import type { Repo } from '@/api/types';
@ -65,6 +65,7 @@ const dialog = useDialog();
//
const router = useRouter();
const route = useRoute();
//
interface QuestionBank {
@ -435,7 +436,16 @@ const deleteSelected = () => {
//
const enterQuestionBank = (bankId: string, bankTitle: string) => {
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 course-editor
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/question-bank/${bankId}/questions?title=${bankTitle}`);
} else {
// 使
router.push(`/teacher/exam-management/question-bank/${bankId}/questions?title=${bankTitle}`);
}
};
const editQuestionBank = (id: string) => {

View File

@ -196,7 +196,16 @@ const currentBankName = ref('加载中...');
//
const goToQuestionBank = () => {
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
//
const courseId = route.params.id || route.params.courseId;
router.push(`/teacher/course-editor/${courseId}/question-bank`);
} else {
//
router.push('/teacher/exam-management/question-bank');
}
};
//
@ -566,7 +575,16 @@ const loadQuestions = async () => {
//
const addQuestion = () => {
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 course-editor
const courseId = route.params.id || route.params.courseId;
router.push(`/teacher/course-editor/${courseId}/question-bank/${currentBankId.value}/add-question`);
} else {
// 使
router.push(`/teacher/exam-management/add-question/${currentBankId.value}`);
}
};
const importQuestions = () => {

View File

@ -359,13 +359,29 @@ const getStudentStatusText = (status: string) => {
}
const handleViewAnswer = (student: StudentExamInfo) => {
//
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=view`)
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 practice
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/review/grading/${examInfo.value.id}/${student.id}?mode=view`);
} else {
// 使
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=view`);
}
}
const handleGrade = (student: StudentExamInfo) => {
//
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=edit`)
//
const currentRoute = route.path;
if (currentRoute.includes('/course-editor/')) {
// 使 practice
const courseId = route.params.id;
router.push(`/teacher/course-editor/${courseId}/practice/review/grading/${examInfo.value.id}/${student.id}?mode=edit`);
} else {
// 使
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=edit`);
}
}
const exportResults = () => {

View File

@ -28,73 +28,34 @@
</template>
</n-button>
<n-button class="header-section flex-row justify-between" type="primary">
<n-button class="header-section flex-row justify-between" type="primary" @click="addChapter">
<template #icon>
<img class="chapter-icon" referrerpolicy="no-referrer" src="/images/teacher/加号_4.png" />
</template>
<span class="section-title">添加章节</span>
</n-button>
<div class="chapter-item flex-row" @click="toggleChapterExpansion" :class="{ 'collapsed': !isChapterExpanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapterExpanded }" referrerpolicy="no-referrer"
:src="isChapterExpanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第一章&nbsp;课前准备</span>
<n-dropdown v-show="isChapterExpanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapterExpanded }"
<template v-for="(chapter, chapterIndex) in chapters" :key="chapter.id">
<div class="chapter-item flex-row" @click="toggleChapterExpansion(chapterIndex)" :class="{ 'collapsed': !chapter.expanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !chapter.expanded }" referrerpolicy="no-referrer"
:src="chapter.expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">{{ chapterIndex + 1 }}&nbsp;{{ chapter.name }}</span>
<n-dropdown v-show="chapter.expanded" :options="chapterMenuOptions" @select="(key: string) => handleChapterMenuSelect(key, chapterIndex)">
<img class="chapter-options-icon" :class="{ 'transparent': !chapter.expanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div v-show="isChapterExpanded" class="chapter-content-item flex-row">
<div v-show="chapter.expanded" v-for="section in chapter.sections" :key="section.id" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">1.开课彩蛋新开始</span>
<span class="content-description">第一节课程定位程定位与目标</span>
</div>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
<span class="action-rename">重命名</span>
<span class="action-delete" @click="openDeleteModal">删除</span>
</div>
</div>
<div class="chapter-item flex-row" @click="toggleChapter2Expansion"
:class="{ 'collapsed': !isChapter2Expanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapter2Expanded }" referrerpolicy="no-referrer"
:src="isChapter2Expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第二章&nbsp;课前准备</span>
<n-dropdown v-show="isChapter2Expanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapter2Expanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div v-show="isChapter2Expanded" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">2.课程导入基础知识</span>
<span class="content-description">第二节课程基础知识讲解</span>
</div>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
<span class="action-rename">重命名</span>
<span class="action-delete" @click="openDeleteModal">删除</span>
</div>
</div>
<div class="chapter-item flex-row" @click="toggleChapter3Expansion"
:class="{ 'collapsed': !isChapter3Expanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapter3Expanded }" referrerpolicy="no-referrer"
:src="isChapter3Expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第三章&nbsp;课前准备</span>
<n-dropdown v-show="isChapter3Expanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapter3Expanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div v-show="isChapter3Expanded" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">3.实践操作技能训练</span>
<span class="content-description">第三节实践操作技能训练</span>
<span class="content-title">{{ section.contentTitle }}</span>
<span class="content-description">{{ section.contentDescription }}</span>
</div>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
<span class="action-rename">重命名</span>
<span class="action-delete" @click="openDeleteModal">删除</span>
</div>
</div>
</template>
</div>
@ -105,12 +66,12 @@
'tablet-content': isTablet,
'sidebar-collapsed-content': sidebarCollapsed && isMobile
}">
<div class="chapter-container">
<div class="chapter-container" v-if="chapters.length">
<div class="chapter-header flex-row justify-between2">
<span class="chapter-title-text"></span>
<n-button class="collapse-button flex-row justify-between" quaternary @click="toggleChapterExpansion">
<span class="collapse-text">{{ isChapterExpanded ? '收起' : '展开' }}</span>
<img class="collapse-icon" :class="{ 'rotated': !isChapterExpanded }" referrerpolicy="no-referrer"
<span class="chapter-title-text">{{ currentChapterIndex + 1 }}</span>
<n-button class="collapse-button flex-row justify-between" quaternary @click="toggleChapterExpansion()">
<span class="collapse-text">{{ chapters[currentChapterIndex].expanded ? '收起' : '展开' }}</span>
<img class="collapse-icon" :class="{ 'rotated': !chapters[currentChapterIndex].expanded }" referrerpolicy="no-referrer"
src="/images/teacher/箭头-灰.png" />
</n-button>
</div>
@ -120,12 +81,12 @@
<span class="label-text">本章名称</span>
</div>
<div class="chapter-name-input-container flex-row">
<n-input ref="chapterInputRef" v-model:value="chapterName" class="chapter-name-input"
<n-input ref="chapterInputRef" v-model:value="chapters[currentChapterIndex].name" class="chapter-name-input"
placeholder="请输入本章名称" @blur="handleChapterNameBlur" @focus="handleChapterNameFocus" />
</div>
</div>
<div v-show="isChapterExpanded" v-for="(section, index) in sections" :key="section.id"
<div v-show="chapters[currentChapterIndex].expanded" v-for="(section, index) in chapters[currentChapterIndex].sections" :key="section.id"
class="chapter-content-container">
<n-divider class="chapter-divider" />
@ -136,7 +97,7 @@
+ 1) }}</span>
</div>
<div class="lesson-input-container">
<n-input v-model:value="section.lessonName" class="lesson-input" placeholder="开课彩蛋新开始"
<n-input v-model:value="section.lessonName" class="lesson-input" placeholder="请输入章节"
@blur="handleLessonBlur" @focus="handleLessonFocus">
<template #suffix>
<n-button quaternary size="small" @click="() => removeLessonSection(section.id)">
@ -157,6 +118,33 @@
</div>
</div>
<!-- 如果选择了课件类型显示上传选项 -->
<div v-if="section.coursewareName" class="courseware-upload-section flex-row justify-end">
<span class="courseware-upload-label">选择文件</span>
<div class="courseware-upload-container">
<CustomDropdown v-model="section.coursewareUploadOption" :options="coursewareUploadOptions" placeholder="请选择文件来源"
@change="(value: any) => handleCoursewareUploadOptionChange(section, value)" />
<!-- 隐藏的课件文件输入框 -->
<input type="file" :id="`courseware-file-upload-${section.id}`" class="file-input"
accept=".pdf,.doc,.docx,.ppt,.pptx,.mp4,.avi,.mov,.wmv"
@change="handleCoursewareFileUpload($event, section)" multiple />
</div>
</div>
<!-- 显示已上传的课件文件列表 -->
<div v-if="section.coursewareFiles && section.coursewareFiles.length > 0"
class="uploaded-courseware-files-section flex-row justify-end">
<span class="uploaded-courseware-files-label">已选择课件</span>
<div class="uploaded-files-container">
<div v-for="(file, fileIndex) in section.coursewareFiles" :key="fileIndex" class="file-item">
<span class="file-name">{{ file.name }}</span>
<button class="remove-file-btn" @click="removeCoursewareFile(section, fileIndex)">
<img src="/images/teacher/关闭-灰.png" alt="删除" style="width: 12px; height: 12px;" />
</button>
</div>
</div>
</div>
<div class="exam-section flex-row justify-end">
<span class="exam-label">添加考试/练习</span>
<div class="exam-dropdown-container">
@ -196,7 +184,7 @@
</div>
</div>
<n-button v-show="isChapterExpanded" class="add-section-btn flex-row justify-between" quaternary
<n-button v-show="chapters[currentChapterIndex].expanded" class="add-section-btn flex-row justify-between" quaternary
@click="addSection">
<template #icon>
<img class="add-section-icon" referrerpolicy="no-referrer" src="/images/teacher/加号(2).png" />
@ -213,6 +201,9 @@
<!-- 作业库模态框 -->
<HomeworkLibraryModal v-model:show="showHomeworkLibraryModal" @confirm="handleHomeworkLibraryConfirm" />
<!-- 资源库模态框 -->
<ResourceLibraryModal v-model:show="showResourceLibraryModal" @confirm="handleResourceLibraryConfirm" />
</div>
</n-config-provider>
@ -225,6 +216,7 @@ import CustomDropdown from '@/components/CustomDropdown.vue';
import HomeworkDropdown from '@/components/HomeworkDropdown.vue';
import ExamPaperLibraryModal from '@/components/ExamPaperLibraryModal.vue';
import HomeworkLibraryModal from '@/components/HomeworkLibraryModal.vue';
import ResourceLibraryModal from '@/components/ResourceLibraryModal.vue';
import { ArrowBackOutline } from '@vicons/ionicons5';
import { useRouter } from 'vue-router';
@ -242,61 +234,130 @@ const showExamLibraryModal = ref(false);
//
const showHomeworkLibraryModal = ref(false);
// /
const isChapterExpanded = ref(true);
const isChapter2Expanded = ref(false);
const isChapter3Expanded = ref(false);
//
const showResourceLibraryModal = ref(false);
//
const isMobile = ref(false);
const isTablet = ref(false);
const sidebarCollapsed = ref(false);
//
const chapterName = ref('课前准备');
// section
interface Section {
id: number;
lessonName: string;
coursewareName: string;
coursewareUploadOption: string;
coursewareFiles: File[];
selectedExamOption: string;
homeworkName: string;
uploadedFiles: File[];
homeworkFiles: File[];
contentTitle: string;
contentDescription: string;
}
//
const sections = ref<Section[]>([
//
interface Chapter {
id: number;
name: string;
expanded: boolean;
sections: Section[];
}
//
const chapters = ref<Chapter[]>([
{
id: 1,
name: '课前准备',
expanded: true,
sections: [
{
id: 1,
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
homeworkFiles: [],
contentTitle: '1.开课彩蛋:新开始',
contentDescription: '第一节课程定位程定位与目标'
},
{
id: 2,
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
homeworkFiles: [],
contentTitle: '2.课程内容:扩展知识',
contentDescription: '第二节课程扩展内容'
}
]
},
{
id: 2,
name: '课前准备',
expanded: false,
sections: [
{
id: 1,
lessonName: '课程导入基础知识',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: [],
contentTitle: '2.课程导入:基础知识',
contentDescription: '第二节课程基础知识讲解'
}
]
},
{
id: 3,
name: '课前准备',
expanded: false,
sections: [
{
id: 1,
lessonName: '实践操作技能训练',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: [],
contentTitle: '3.实践操作:技能训练',
contentDescription: '第三节实践操作技能训练'
}
]
}
]);
// ID
const nextSectionId = ref(3);
//
const currentChapterIndex = ref(0);
// ID
const nextChapterId = ref(4);
//
const coursewareOptions = [
{ label: '课件准备PPT', value: '课件准备PPT' },
{ label: '视频课件', value: '视频课件' },
{ label: '音频课件', value: '音频课件' },
{ label: '文档课件', value: '文档课件' }
];
//
const coursewareUploadOptions = [
{ label: '本地上传', value: '本地上传' },
{ label: '从资源库选择', value: '从资源库选择' }
];
//
@ -358,19 +419,20 @@ const examOptions = [
];
//
const handleChapterMenuSelect = (key: string) => {
const handleChapterMenuSelect = (key: string, chapterIndex?: number) => {
const targetIndex = chapterIndex !== undefined ? chapterIndex : currentChapterIndex.value;
if (key === 'delete') {
openDeleteModal();
deleteChapter(targetIndex);
} else if (key === 'rename') {
//
console.log('重命名章节');
console.log('重命名章节', targetIndex);
}
};
//
const handleChapterNameBlur = () => {
//
console.log('章节名称已更新:', chapterName.value);
console.log('章节名称已更新:', chapters.value[currentChapterIndex.value].name);
};
//
@ -379,6 +441,58 @@ const handleChapterNameFocus = () => {
console.log('开始编辑章节名称');
};
//
const addChapter = () => {
//
chapters.value.forEach((chapter) => {
chapter.expanded = false;
});
const newChapter: Chapter = {
id: nextChapterId.value,
name: `新章节${nextChapterId.value}`,
expanded: true,
sections: [
{
id: 1,
lessonName: '新小节',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: [],
contentTitle: '1.新小节',
contentDescription: '新小节描述'
}
]
};
chapters.value.push(newChapter);
currentChapterIndex.value = chapters.value.length - 1;
nextChapterId.value++;
};
//
const deleteChapter = (chapterIndex: number) => {
if (chapters.value.length > 1) {
chapters.value.splice(chapterIndex, 1);
//
if (currentChapterIndex.value >= chapters.value.length) {
currentChapterIndex.value = chapters.value.length - 1;
} else if (currentChapterIndex.value > chapterIndex) {
currentChapterIndex.value--;
}
//
const hasExpandedChapter = chapters.value.some(chapter => chapter.expanded);
if (!hasExpandedChapter && chapters.value.length > 0) {
chapters.value[currentChapterIndex.value].expanded = true;
}
}
};
//
const openDeleteModal = () => {
showDeleteModal.value = true;
@ -418,16 +532,34 @@ const toggleSidebar = () => {
};
// /
const toggleChapterExpansion = () => {
isChapterExpanded.value = !isChapterExpanded.value;
};
const toggleChapter2Expansion = () => {
isChapter2Expanded.value = !isChapter2Expanded.value;
};
const toggleChapter3Expansion = () => {
isChapter3Expanded.value = !isChapter3Expanded.value;
const toggleChapterExpansion = (chapterIndex?: number) => {
if (chapterIndex !== undefined) {
//
if (chapters.value[chapterIndex].expanded) {
chapters.value[chapterIndex].expanded = false;
} else {
//
chapters.value.forEach((chapter) => {
chapter.expanded = false;
});
//
chapters.value[chapterIndex].expanded = true;
currentChapterIndex.value = chapterIndex;
}
} else {
//
const currentExpanded = chapters.value[currentChapterIndex.value].expanded;
if (currentExpanded) {
chapters.value[currentChapterIndex.value].expanded = false;
} else {
//
chapters.value.forEach((chapter) => {
chapter.expanded = false;
});
//
chapters.value[currentChapterIndex.value].expanded = true;
}
}
};
//
@ -453,17 +585,22 @@ const handleLessonFocus = () => {
//
const addSection = () => {
const newSection = {
id: nextSectionId.value,
lessonName: '开课彩蛋新开始',
const currentChapter = chapters.value[currentChapterIndex.value];
const newSectionId = Math.max(...currentChapter.sections.map(s => s.id), 0) + 1;
const newSection: Section = {
id: newSectionId,
lessonName: '新小节',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
homeworkFiles: [],
contentTitle: `${newSectionId}.新小节`,
contentDescription: '新小节描述'
};
sections.value.push(newSection);
nextSectionId.value++;
currentChapter.sections.push(newSection);
};
//
@ -486,6 +623,26 @@ const handleExamOptionChange = (section: Section, value: any) => {
}
};
//
const handleCoursewareUploadOptionChange = (section: Section, value: any) => {
console.log('课件上传选项变化:', value, 'section id:', section.id);
// ""
if (value === '本地上传') {
console.log('触发课件文件选择');
const fileInput = document.getElementById(`courseware-file-upload-${section.id}`) as HTMLInputElement;
if (fileInput) {
fileInput.click();
} else {
console.error('找不到课件文件输入框:', `courseware-file-upload-${section.id}`);
}
} else if (value === '从资源库选择') {
// ""
console.log('准备显示资源库模态框');
showResourceLibraryModal.value = true;
console.log('showResourceLibraryModal.value:', showResourceLibraryModal.value);
}
};
//
const handleHomeworkOptionChange = (section: Section, value: any) => {
console.log('作业选项变化:', value, 'section id:', section.id);
@ -518,6 +675,16 @@ const handleFileUpload = (event: Event, section: Section) => {
}
};
//
const handleCoursewareFileUpload = (event: Event, section: Section) => {
const target = event.target as HTMLInputElement;
if (target.files) {
const files = Array.from(target.files);
section.coursewareFiles = files;
console.log('课件文件已上传:', files);
}
};
//
const handleHomeworkFileUpload = (event: any, section: Section) => {
const target = event.target as HTMLInputElement;
@ -533,6 +700,11 @@ const removeFile = (section: Section, fileIndex: number) => {
section.uploadedFiles.splice(fileIndex, 1);
};
//
const removeCoursewareFile = (section: Section, fileIndex: number) => {
section.coursewareFiles.splice(fileIndex, 1);
};
//
const handleExamLibraryConfirm = (selectedExams: any[]) => {
console.log('选择的试卷:', selectedExams);
@ -549,11 +721,21 @@ const handleHomeworkLibraryConfirm = (selectedHomework: any[]) => {
showHomeworkLibraryModal.value = false;
};
//
const handleResourceLibraryConfirm = (selectedResources: any[]) => {
console.log('选择的资源:', selectedResources);
// section
//
showResourceLibraryModal.value = false;
};
//
const removeLessonSection = (sectionId: number) => {
const index = sections.value.findIndex(section => section.id === sectionId);
if (index > -1 && sections.value.length > 1) {
sections.value.splice(index, 1);
const removeLessonSection = (sectionId: number, chapterIndex?: number) => {
const targetChapterIndex = chapterIndex !== undefined ? chapterIndex : currentChapterIndex.value;
const chapter = chapters.value[targetChapterIndex];
const index = chapter.sections.findIndex((section: Section) => section.id === sectionId);
if (index > -1 && chapter.sections.length > 1) {
chapter.sections.splice(index, 1);
}
};
@ -1065,6 +1247,8 @@ const goBack = () => {
line-height: 21px;
transition: color 0.3s ease;
margin-right: 40px;
overflow: hidden;
text-overflow: ellipsis;
}
.chapter-item.collapsed .chapter-title {
@ -1087,40 +1271,69 @@ const goBack = () => {
}
.chapter-content-item {
width: 234px;
height: 125px;
margin: 2px 0 0 41px;
/* width: 234px; */
height: auto;
/* min-height: 90px; */
margin: 8px 8px 0 31px;
padding: 12px 8px;
border-radius: 6px;
transition: all 0.3s ease;
cursor: pointer;
}
.chapter-content-item:hover {
background: rgba(2, 136, 209, 0.08);
}
.content-text-group {
width: 145px;
height: 98px;
margin-top: 27px;
width: 100%;
height: auto;
margin: 0;
padding: 4px 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.content-title {
height: 23px;
height: auto;
overflow-wrap: break-word;
color: rgba(102, 102, 102, 1);
font-size: 16px;
color: rgba(51, 51, 51, 1);
font-size: 15px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
font-weight: 600;
text-align: left;
white-space: nowrap;
line-height: 18px;
margin-left: 6px;
line-height: 20px;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.2px;
word-break: break-word;
}
.content-description {
height: 45px;
height: auto;
overflow-wrap: break-word;
color: rgba(102, 102, 102, 1);
font-size: 16px;
color: rgba(153, 153, 153, 1);
font-size: 13px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
line-height: 18px;
margin-top: 30px;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.75;
font-style: italic;
word-break: break-word;
}
.action-menu {
@ -1193,9 +1406,9 @@ const goBack = () => {
height: 21px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 18px;
font-size: 20px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
font-weight: 600;
text-align: left;
white-space: nowrap;
line-height: 21px;
@ -1295,6 +1508,64 @@ const goBack = () => {
margin: 1px 0 0 5px;
}
/* 课件上传选项部分 */
.courseware-upload-section {
width: 100%;
max-width: 540px;
height: 42px;
margin: 10px 0 0 20px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.courseware-upload-label {
width: 120px;
height: 18px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 16px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: right;
white-space: nowrap;
line-height: 18px;
flex-shrink: 0;
}
.courseware-upload-container {
width: 400px;
height: 42px;
margin: 1px 0 0 5px;
position: relative;
}
/* 已上传课件文件部分 */
.uploaded-courseware-files-section {
width: 100%;
max-width: 540px;
height: auto;
margin: 10px 0 0 20px;
display: flex;
justify-content: flex-end;
align-items: flex-start;
}
.uploaded-courseware-files-label {
width: 120px;
height: 18px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 16px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: right;
white-space: nowrap;
line-height: 18px;
flex-shrink: 0;
margin-top: 12px;
}
/* 删除旧的课件输入框样式,使用 Naive UI 样式 */
.courseware-dropdown-icon {