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"> <div class="top">
<n-tabs v-model:value="activeTab" size="large"> <n-tabs v-model:value="activeTab" size="large">
<n-tab-pane name="ongoing" tab="进行中" /> <n-tab-pane name="ongoing" tab="进行中" />
<n-tab-pane name="finished" tab="已结束" />
<n-tab-pane name="draft" tab="草稿箱" /> <n-tab-pane name="draft" tab="草稿箱" />
<n-tab-pane name="finished" tab="已结束" />
</n-tabs> </n-tabs>
<div class="actions"> <div class="actions">
<n-button type="primary" @click="navigateToCreateCourse">创建课程</n-button> <n-button type="primary" @click="navigateToCreateCourse">创建课程</n-button>
<div class="search-container"> <div class="search-container">
<n-input v-model:value="searchValue" type="text" placeholder="请输入想要搜索的内容" /> <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>
</div> </div>
<!-- 主体 --> <!-- 主体 -->
<div class="course-container"> <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-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 === '下架中' }">{{ course.status }} <div class="section-title" :class="{ 'offline': course.status === 0 }">{{ course.statusText }}
</div> </div>
<n-popselect <n-popselect
:options="getOptionsForCourse(course)" :options="getOptionsForCourse(course)"
@ -50,7 +96,7 @@
<!-- 底部翻页按钮 --> <!-- 底部翻页按钮 -->
<div class="pagination"> <!-- <div class="pagination">
<div class="pagination-content"> <div class="pagination-content">
<div class="page-numbers"> <div class="page-numbers">
<a href="#" class="page-number">首页</a> <a href="#" class="page-number">首页</a>
@ -67,60 +113,186 @@
<a href="#" class="page-number">尾页</a> <a href="#" class="page-number">尾页</a>
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h } from 'vue'; import { ref, h, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { EllipsisVerticalSharp } 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';
//
interface CourseDisplayItem extends TeachCourse {
statusText?: string;
image?: string;
}
const router = useRouter(); const router = useRouter();
const message = useMessage(); const message = useMessage();
const dialog = useDialog(); const dialog = useDialog();
// //
const courseList = ref([ const originalCourseList = ref<CourseDisplayItem[]>([]);
{ id: 1, name: '前端开发基础课程', status: '发布中', image: '/images/teacher/fj.png', students: 120 }, //
{ id: 2, name: 'Vue.js 实战教程', status: '发布中', image: '/images/teacher/fj.png', students: 95 }, const courseList = ref<CourseDisplayItem[]>([]);
{ 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 }, const loading = ref<boolean>(false);
{ id: 5, name: 'TypeScript 高级教程', status: '发布中', image: '/images/teacher/fj.png', students: 73 }, //
{ id: 6, name: 'JavaScript 设计模式', status: '发布中', image: '/images/teacher/fj.png', students: 56 }, const error = ref<string>('');
{ 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 }, const getCourseList = async (forceRefresh: boolean = false) => {
{ id: 10, name: '移动端适配实战', status: '发布中', image: '/images/teacher/fj.png', students: 54 }, try {
{ id: 11, name: '微信小程序开发', status: '下架中', image: '/images/teacher/fj.png', students: 38 }, loading.value = true;
{ id: 12, name: 'Flutter 跨平台开发', status: '发布中', image: '/images/teacher/fj.png', students: 29 }, 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 searchValue = ref<string>('')
const activeTab = ref<string>('ongoing') 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 = () => { const navigateToCreateCourse = () => {
router.push('/teacher/course-create'); router.push('/teacher/course-create');
}; };
//
const handleSearch = async () => {
console.log('🔍 执行搜索:', searchValue.value);
//
await getCourseList(true);
};
// //
const getOptionsForCourse = (course: any) => { const getOptionsForCourse = (course: CourseDisplayItem) => {
if (course.status === '发布中') { if (course.status === 1) { //
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 === '下架中') { } 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) { //
return [
{ label: '查看', value: 'view', icon: '/images/teacher/查看.png' },
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
];
} }
return []; return [];
}; };
@ -178,35 +350,104 @@ const handleOptionSelect = (value: string, course: any) => {
// //
const handleDeleteCourse = (course: any) => { const handleDeleteCourse = (course: any) => {
//
if (course.status === 1) {
dialog.warning({
title: '无法删除',
content: `课程"${course.name}"正在进行中,请先下架课程后再删除。`,
positiveText: '先下架课程',
negativeText: '取消',
onPositiveClick: () => {
//
handleOfflineCourse(course);
}
});
return;
}
//
dialog.warning({ dialog.warning({
title: '确认删除', title: '确认删除',
content: `确定要删除课程"${course.name}"吗?此操作不可撤销。`, content: `确定要删除课程"${course.name}"吗?此操作不可撤销。`,
positiveText: '确定删除', positiveText: '确定删除',
negativeText: '取消', negativeText: '取消',
onPositiveClick: () => { onPositiveClick: async () => {
const index = courseList.value.findIndex(c => c.id === course.id); try {
if (index > -1) { //
courseList.value.splice(index, 1); 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}"已删除`); message.success(`课程"${course.name}"已删除`);
} catch (error) {
console.error('删除课程失败:', error);
message.error('删除课程失败,请稍后重试');
} }
} }
}); });
}; };
// //
const handleOfflineCourse = (course: any) => { const handleOfflineCourse = (course: CourseDisplayItem) => {
const targetCourse = courseList.value.find(c => c.id === course.id); if (!course.id) {
if (targetCourse) { message.error('课程ID不存在无法下架');
targetCourse.status = '下架中'; return;
message.success(`课程"${course.name}"已下架`);
} }
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 = 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); const targetCourse = courseList.value.find(c => c.id === course.id);
if (targetCourse) { if (targetCourse) {
targetCourse.status = '发布中'; targetCourse.status = 1; //
targetCourse.statusText = '进行中';
message.success(`课程"${course.name}"已发布`); message.success(`课程"${course.name}"已发布`);
} }
}; };
@ -262,6 +503,11 @@ const handleMoveCourse = (course: any) => {
} }
}); });
}; };
onMounted(() => {
getCourseList();
});
</script> </script>
@ -324,6 +570,32 @@ const handleMoveCourse = (course: any) => {
flex-direction: column; 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 { .course-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@ -552,9 +824,9 @@ const handleMoveCourse = (course: any) => {
gap: 30px; gap: 30px;
} }
.course-card { /* .course-card {
aspect-ratio: 200 / 220; aspect-ratio: 200 / 220;
} } */
.course-info img { .course-info img {
width: 85%; width: 85%;

View File

@ -75,8 +75,7 @@
<!-- 排序 --> <!-- 排序 -->
<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.sort" :options="sortOptions" placeholder="数字越小越排序靠前" <n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" />
class="form-input" />
</div> </div>
<!-- 课程结束时间 --> <!-- 课程结束时间 -->
@ -147,7 +146,7 @@
</div> </div>
<!-- 积分设置 --> <!-- 积分设置 -->
<div class="form-item form-integral"> <div class="form-item form-integral" v-if="false">
<label class="form-label required">积分设置</label> <label class="form-label required">积分设置</label>
<div class="setting-container"> <div class="setting-container">
<n-switch v-model:value="formData.pointsEnabled" class="form-toggle" /> <n-switch v-model:value="formData.pointsEnabled" class="form-toggle" />
@ -189,6 +188,7 @@ 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'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -351,18 +351,18 @@ const classOptions = [
] ]
// //
const sortOptions = [ // const sortOptions = [
{ label: '1', value: '1' }, // { label: '1', value: '1' },
{ label: '2', value: '2' }, // { label: '2', value: '2' },
{ label: '3', value: '3' }, // { label: '3', value: '3' },
{ label: '4', value: '4' }, // { label: '4', value: '4' },
{ label: '5', value: '5' }, // { label: '5', value: '5' },
{ label: '6', value: '6' }, // { label: '6', value: '6' },
{ label: '7', value: '7' }, // { label: '7', value: '7' },
{ label: '8', value: '8' }, // { label: '8', value: '8' },
{ label: '9', value: '9' }, // { label: '9', value: '9' },
{ label: '10', value: '10' } // { label: '10', value: '10' }
] // ]
// //
const triggerFileUpload = () => { const triggerFileUpload = () => {

View File

@ -314,7 +314,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, h, computed, watch } from 'vue' 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 { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5'
import { import {
NDataTable, NDataTable,
@ -386,6 +387,7 @@ const currentInviteClassId = ref<string | null>(null) // 当前邀请码对应
const message = useMessage() const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
const router = useRouter() const router = useRouter()
const route = useRoute()
// //
const selectedDepartment = ref('') const selectedDepartment = ref('')
@ -405,6 +407,7 @@ const currentEditId = ref('')
const isRenameMode = ref(false) const isRenameMode = ref(false)
const showBatchTransferModal = ref(false) const showBatchTransferModal = ref(false)
const selectedRowKeys = ref<string[]>([]) // keys const selectedRowKeys = ref<string[]>([]) // keys
const courseId = ref<string | null>(null) // ID
// //
const formData = ref<FormData>({ const formData = ref<FormData>({
@ -1028,24 +1031,30 @@ const closeAddClassModal = () => {
classFormData.value.className = '' classFormData.value.className = ''
} }
// // /API
const handleAddClass = async () => { const handleAddClass = async () => {
try { try {
await classFormRef.value?.validate() await classFormRef.value?.validate()
if (isRenameMode.value) { 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) { if (classIndex > -1) {
masterClassList.value[classIndex].className = classFormData.value.className masterClassList.value[classIndex].className = className
} }
message.success(`已将班级重命名为:${classFormData.value.className}`) message.success(`已将班级重命名为:${className}`)
} else { } 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 = { const newClass: ClassItem = {
id: newId, id: newId,
className: classFormData.value.className, className,
studentCount: 0, studentCount: 0,
creator: '王建国', creator: '王建国',
createTime: new Date().toLocaleString('zh-CN', { createTime: new Date().toLocaleString('zh-CN', {
@ -1057,12 +1066,9 @@ const handleAddClass = async () => {
}).replace(/\//g, '.').replace(',', '') }).replace(/\//g, '.').replace(',', '')
} }
masterClassList.value.push(newClass) masterClassList.value.push(newClass)
message.success(`已添加班级:${classFormData.value.className}`) message.success(`已添加班级:${className}`)
} }
//
closeAddClassModal() closeAddClassModal()
} catch (error) { } catch (error) {
message.error('请检查表单信息') message.error('请检查表单信息')
} }
@ -1082,7 +1088,7 @@ const handleRenameClass = (classItem: any) => {
showManageClassModal.value = false showManageClassModal.value = false
} }
// // API
const handleDeleteClass = (classItem: any) => { const handleDeleteClass = (classItem: any) => {
dialog.info({ dialog.info({
title: '确认删除', title: '确认删除',
@ -1091,17 +1097,13 @@ const handleDeleteClass = (classItem: any) => {
negativeText: '取消', negativeText: '取消',
onPositiveClick: async () => { onPositiveClick: async () => {
try { try {
// API await ClassApi.deleteClass(classItem.id)
await new Promise(resolve => setTimeout(resolve, 500))
message.success(`已删除班级:${classItem.className}`) message.success(`已删除班级:${classItem.className}`)
// //
const index = masterClassList.value.findIndex(item => item.id === classItem.id) const index = masterClassList.value.findIndex(item => item.id === classItem.id)
if (index > -1) { if (index > -1) {
masterClassList.value.splice(index, 1) masterClassList.value.splice(index, 1)
} }
} catch (error) { } catch (error) {
message.error('删除失败,请重试') message.error('删除失败,请重试')
} }
@ -1251,6 +1253,13 @@ onMounted(() => {
// 使使classId使 // 使使classId使
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value) const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
loadData(initialClassId) 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 CourseManagement from '@/components/admin/CourseManagement.vue'
import MyResources from '@/components/admin/MyResources.vue' import MyResources from '@/components/admin/MyResources.vue'
import StudentManagement from '@/components/admin/StudentManagement.vue' import StudentManagement from '@/components/admin/StudentManagement.vue'
import MessageCenter from '@/views/teacher/message/MessageCenter.vue'
// 课程管理子组件 // 课程管理子组件
import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue' import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue'
@ -205,26 +206,52 @@ const routes: RouteRecordRaw[] = [
] ]
}, },
{ {
path: 'practice', path: 'practice/exam',
name: 'Practice', name: 'PracticeExam',
redirect: (to) => `/teacher/course-editor/${to.params.id}/practice/exam`, component: ExamLibrary,
meta: { title: '考试管理' }, meta: { title: '试卷管理' }
children: [ },
{ {
path: 'exam', path: 'practice/exam/add',
name: 'PracticeExam', name: 'PracticeAddExam',
// component: () => import('../views/teacher/course/PracticeExam.vue'), component: AddExam,
component: ExamLibrary, meta: { title: '添加试卷' }
meta: { title: '试卷' } },
}, {
{ path: 'practice/exam/edit/:id',
path: 'review', name: 'PracticeEditExam',
name: 'PracticeReview', component: AddExam,
// component: () => import('../views/teacher/course/PracticeReview.vue'), meta: { title: '编辑试卷' }
component: MarkingCenter, },
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: 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', path: 'question-bank',
@ -313,6 +340,12 @@ const routes: RouteRecordRaw[] = [
component: MyResources, component: MyResources,
meta: { title: '我的资源' } meta: { title: '我的资源' }
}, },
{
path: 'message-center',
name: 'MessageCenter',
component: MessageCenter,
meta: { title: '消息中心' }
},
{ {
path: 'recycle-bin', path: 'recycle-bin',
name: 'RecycleBin', name: 'RecycleBin',

View File

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

View File

@ -278,7 +278,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, onMounted, onUnmounted, watch } from 'vue'; import { computed, reactive, ref, onMounted, onUnmounted, watch } from 'vue';
import { createDiscreteApi } from 'naive-ui'; 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 { AddCircle, SettingsOutline, TrashOutline, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue'; import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue'; import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue';
@ -295,6 +295,7 @@ const { dialog } = createDiscreteApi(['dialog'])
// //
const router = useRouter() const router = useRouter()
const route = useRoute()
// //
const goBack = () => { const goBack = () => {
@ -1079,8 +1080,16 @@ const previewExam = () => {
return; return;
} }
// //
router.push('/teacher/exam-management/preview'); 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 = () => { 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('题目保存成功'); message.success('题目保存成功');
// //
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`); 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) { } catch (error: any) {
console.error('创建题目流程失败:', error); console.error('创建题目流程失败:', error);

View File

@ -22,8 +22,9 @@
import { h, ref, VNode, computed } from 'vue'; import { h, ref, VNode, computed } from 'vue';
import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui'; import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui';
import type { DataTableColumns } from 'naive-ui'; import type { DataTableColumns } from 'naive-ui';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); const router = useRouter();
const route = useRoute();
// //
type Exam = { type Exam = {
@ -150,7 +151,36 @@ const examData = ref<Exam[]>([
const columns = createColumns({ const columns = createColumns({
handleAction: (action, row) => { handleAction: (action, row) => {
if(action === '试卷分析'){ if(action === '试卷分析'){
router.push({ name: 'ExamAnalysis', query: { examId: row.id } }); //
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; return;
} }
message.info(`执行操作: ${action} on row ${row.id}`); message.info(`执行操作: ${action} on row ${row.id}`);
@ -189,8 +219,16 @@ const paginationConfig = computed(() => ({
})); }));
const handleAddExam = () => { const handleAddExam = () => {
// //
router.push({ name: 'AddExam' }); 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> </script>

View File

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

View File

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

View File

@ -196,7 +196,16 @@ const currentBankName = ref('加载中...');
// //
const goToQuestionBank = () => { const goToQuestionBank = () => {
router.push('/teacher/exam-management/question-bank'); //
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 addQuestion = () => {
router.push(`/teacher/exam-management/add-question/${currentBankId.value}`); //
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 = () => { const importQuestions = () => {

View File

@ -359,13 +359,29 @@ const getStudentStatusText = (status: string) => {
} }
const handleViewAnswer = (student: StudentExamInfo) => { 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) => { 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 = () => { const exportResults = () => {

View File

@ -28,73 +28,34 @@
</template> </template>
</n-button> </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> <template #icon>
<img class="chapter-icon" referrerpolicy="no-referrer" src="/images/teacher/加号_4.png" /> <img class="chapter-icon" referrerpolicy="no-referrer" src="/images/teacher/加号_4.png" />
</template> </template>
<span class="section-title">添加章节</span> <span class="section-title">添加章节</span>
</n-button> </n-button>
<div class="chapter-item flex-row" @click="toggleChapterExpansion" :class="{ 'collapsed': !isChapterExpanded }"> <template v-for="(chapter, chapterIndex) in chapters" :key="chapter.id">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapterExpanded }" referrerpolicy="no-referrer" <div class="chapter-item flex-row" @click="toggleChapterExpansion(chapterIndex)" :class="{ 'collapsed': !chapter.expanded }">
:src="isChapterExpanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" /> <img class="chapter-arrow-icon" :class="{ 'rotated': !chapter.expanded }" referrerpolicy="no-referrer"
<span class="chapter-title">第一章&nbsp;课前准备</span> :src="chapter.expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<n-dropdown v-show="isChapterExpanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect"> <span class="chapter-title">{{ chapterIndex + 1 }}&nbsp;{{ chapter.name }}</span>
<img class="chapter-options-icon" :class="{ 'transparent': !isChapterExpanded }" <n-dropdown v-show="chapter.expanded" :options="chapterMenuOptions" @select="(key: string) => handleChapterMenuSelect(key, chapterIndex)">
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" /> <img class="chapter-options-icon" :class="{ 'transparent': !chapter.expanded }"
</n-dropdown> referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</div> </n-dropdown>
<div v-show="isChapterExpanded" 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>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }"> <div v-show="chapter.expanded" v-for="section in chapter.sections" :key="section.id" class="chapter-content-item flex-row">
<span class="action-rename">重命名</span> <div class="content-text-group flex-col justify-between justify-between">
<span class="action-delete" @click="openDeleteModal">删除</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> </div>
</div> </template>
<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>
</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> </div>
@ -105,12 +66,12 @@
'tablet-content': isTablet, 'tablet-content': isTablet,
'sidebar-collapsed-content': sidebarCollapsed && isMobile '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"> <div class="chapter-header flex-row justify-between2">
<span class="chapter-title-text"></span> <span class="chapter-title-text">{{ currentChapterIndex + 1 }}</span>
<n-button class="collapse-button flex-row justify-between" quaternary @click="toggleChapterExpansion"> <n-button class="collapse-button flex-row justify-between" quaternary @click="toggleChapterExpansion()">
<span class="collapse-text">{{ isChapterExpanded ? '收起' : '展开' }}</span> <span class="collapse-text">{{ chapters[currentChapterIndex].expanded ? '收起' : '展开' }}</span>
<img class="collapse-icon" :class="{ 'rotated': !isChapterExpanded }" referrerpolicy="no-referrer" <img class="collapse-icon" :class="{ 'rotated': !chapters[currentChapterIndex].expanded }" referrerpolicy="no-referrer"
src="/images/teacher/箭头-灰.png" /> src="/images/teacher/箭头-灰.png" />
</n-button> </n-button>
</div> </div>
@ -120,12 +81,12 @@
<span class="label-text">本章名称</span> <span class="label-text">本章名称</span>
</div> </div>
<div class="chapter-name-input-container flex-row"> <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" /> placeholder="请输入本章名称" @blur="handleChapterNameBlur" @focus="handleChapterNameFocus" />
</div> </div>
</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"> class="chapter-content-container">
<n-divider class="chapter-divider" /> <n-divider class="chapter-divider" />
@ -136,7 +97,7 @@
+ 1) }}</span> + 1) }}</span>
</div> </div>
<div class="lesson-input-container"> <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"> @blur="handleLessonBlur" @focus="handleLessonFocus">
<template #suffix> <template #suffix>
<n-button quaternary size="small" @click="() => removeLessonSection(section.id)"> <n-button quaternary size="small" @click="() => removeLessonSection(section.id)">
@ -157,6 +118,33 @@
</div> </div>
</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"> <div class="exam-section flex-row justify-end">
<span class="exam-label">添加考试/练习</span> <span class="exam-label">添加考试/练习</span>
<div class="exam-dropdown-container"> <div class="exam-dropdown-container">
@ -196,7 +184,7 @@
</div> </div>
</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"> @click="addSection">
<template #icon> <template #icon>
<img class="add-section-icon" referrerpolicy="no-referrer" src="/images/teacher/加号(2).png" /> <img class="add-section-icon" referrerpolicy="no-referrer" src="/images/teacher/加号(2).png" />
@ -213,6 +201,9 @@
<!-- 作业库模态框 --> <!-- 作业库模态框 -->
<HomeworkLibraryModal v-model:show="showHomeworkLibraryModal" @confirm="handleHomeworkLibraryConfirm" /> <HomeworkLibraryModal v-model:show="showHomeworkLibraryModal" @confirm="handleHomeworkLibraryConfirm" />
<!-- 资源库模态框 -->
<ResourceLibraryModal v-model:show="showResourceLibraryModal" @confirm="handleResourceLibraryConfirm" />
</div> </div>
</n-config-provider> </n-config-provider>
@ -225,6 +216,7 @@ import CustomDropdown from '@/components/CustomDropdown.vue';
import HomeworkDropdown from '@/components/HomeworkDropdown.vue'; import HomeworkDropdown from '@/components/HomeworkDropdown.vue';
import ExamPaperLibraryModal from '@/components/ExamPaperLibraryModal.vue'; import ExamPaperLibraryModal from '@/components/ExamPaperLibraryModal.vue';
import HomeworkLibraryModal from '@/components/HomeworkLibraryModal.vue'; import HomeworkLibraryModal from '@/components/HomeworkLibraryModal.vue';
import ResourceLibraryModal from '@/components/ResourceLibraryModal.vue';
import { ArrowBackOutline } from '@vicons/ionicons5'; import { ArrowBackOutline } from '@vicons/ionicons5';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -242,61 +234,130 @@ const showExamLibraryModal = ref(false);
// //
const showHomeworkLibraryModal = ref(false); const showHomeworkLibraryModal = ref(false);
// / //
const isChapterExpanded = ref(true); const showResourceLibraryModal = ref(false);
const isChapter2Expanded = ref(false);
const isChapter3Expanded = ref(false);
// //
const isMobile = ref(false); const isMobile = ref(false);
const isTablet = ref(false); const isTablet = ref(false);
const sidebarCollapsed = ref(false); const sidebarCollapsed = ref(false);
//
const chapterName = ref('课前准备');
// section // section
interface Section { interface Section {
id: number; id: number;
lessonName: string; lessonName: string;
coursewareName: string; coursewareName: string;
coursewareUploadOption: string;
coursewareFiles: File[];
selectedExamOption: string; selectedExamOption: string;
homeworkName: string; homeworkName: string;
uploadedFiles: File[]; uploadedFiles: File[];
homeworkFiles: 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, id: 1,
lessonName: '开课彩蛋新开始', name: '课前准备',
coursewareName: '课件准备PPT', expanded: true,
selectedExamOption: '', sections: [
homeworkName: '请添加作业', {
uploadedFiles: [], id: 1,
homeworkFiles: [] lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: [],
contentTitle: '1.开课彩蛋:新开始',
contentDescription: '第一节课程定位程定位与目标'
},
{
id: 2,
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '',
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: [],
contentTitle: '2.课程内容:扩展知识',
contentDescription: '第二节课程扩展内容'
}
]
}, },
{ {
id: 2, id: 2,
lessonName: '开课彩蛋新开始', name: '课前准备',
coursewareName: '课件准备PPT', expanded: false,
selectedExamOption: '', sections: [
homeworkName: '请添加作业', {
uploadedFiles: [], id: 1,
homeworkFiles: [] 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 = [ const coursewareOptions = [
{ label: '课件准备PPT', value: '课件准备PPT' }, { label: '课件准备PPT', value: '课件准备PPT' },
{ label: '视频课件', value: '视频课件' }, { 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') { if (key === 'delete') {
openDeleteModal(); deleteChapter(targetIndex);
} else if (key === 'rename') { } else if (key === 'rename') {
// //
console.log('重命名章节'); console.log('重命名章节', targetIndex);
} }
}; };
// //
const handleChapterNameBlur = () => { const handleChapterNameBlur = () => {
// //
console.log('章节名称已更新:', chapterName.value); console.log('章节名称已更新:', chapters.value[currentChapterIndex.value].name);
}; };
// //
@ -379,6 +441,58 @@ const handleChapterNameFocus = () => {
console.log('开始编辑章节名称'); 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 = () => { const openDeleteModal = () => {
showDeleteModal.value = true; showDeleteModal.value = true;
@ -418,16 +532,34 @@ const toggleSidebar = () => {
}; };
// / // /
const toggleChapterExpansion = () => { const toggleChapterExpansion = (chapterIndex?: number) => {
isChapterExpanded.value = !isChapterExpanded.value; if (chapterIndex !== undefined) {
}; //
if (chapters.value[chapterIndex].expanded) {
const toggleChapter2Expansion = () => { chapters.value[chapterIndex].expanded = false;
isChapter2Expanded.value = !isChapter2Expanded.value; } else {
}; //
chapters.value.forEach((chapter) => {
const toggleChapter3Expansion = () => { chapter.expanded = false;
isChapter3Expanded.value = !isChapter3Expanded.value; });
//
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 addSection = () => {
const newSection = { const currentChapter = chapters.value[currentChapterIndex.value];
id: nextSectionId.value, const newSectionId = Math.max(...currentChapter.sections.map(s => s.id), 0) + 1;
lessonName: '开课彩蛋新开始', const newSection: Section = {
id: newSectionId,
lessonName: '新小节',
coursewareName: '课件准备PPT', coursewareName: '课件准备PPT',
coursewareUploadOption: '',
coursewareFiles: [],
selectedExamOption: '', selectedExamOption: '',
homeworkName: '请添加作业', homeworkName: '请添加作业',
uploadedFiles: [], uploadedFiles: [],
homeworkFiles: [] homeworkFiles: [],
contentTitle: `${newSectionId}.新小节`,
contentDescription: '新小节描述'
}; };
sections.value.push(newSection); currentChapter.sections.push(newSection);
nextSectionId.value++;
}; };
// //
@ -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) => { const handleHomeworkOptionChange = (section: Section, value: any) => {
console.log('作业选项变化:', value, 'section id:', section.id); 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 handleHomeworkFileUpload = (event: any, section: Section) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
@ -533,6 +700,11 @@ const removeFile = (section: Section, fileIndex: number) => {
section.uploadedFiles.splice(fileIndex, 1); section.uploadedFiles.splice(fileIndex, 1);
}; };
//
const removeCoursewareFile = (section: Section, fileIndex: number) => {
section.coursewareFiles.splice(fileIndex, 1);
};
// //
const handleExamLibraryConfirm = (selectedExams: any[]) => { const handleExamLibraryConfirm = (selectedExams: any[]) => {
console.log('选择的试卷:', selectedExams); console.log('选择的试卷:', selectedExams);
@ -549,11 +721,21 @@ const handleHomeworkLibraryConfirm = (selectedHomework: any[]) => {
showHomeworkLibraryModal.value = false; showHomeworkLibraryModal.value = false;
}; };
//
const handleResourceLibraryConfirm = (selectedResources: any[]) => {
console.log('选择的资源:', selectedResources);
// section
//
showResourceLibraryModal.value = false;
};
// //
const removeLessonSection = (sectionId: number) => { const removeLessonSection = (sectionId: number, chapterIndex?: number) => {
const index = sections.value.findIndex(section => section.id === sectionId); const targetChapterIndex = chapterIndex !== undefined ? chapterIndex : currentChapterIndex.value;
if (index > -1 && sections.value.length > 1) { const chapter = chapters.value[targetChapterIndex];
sections.value.splice(index, 1); 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; line-height: 21px;
transition: color 0.3s ease; transition: color 0.3s ease;
margin-right: 40px; margin-right: 40px;
overflow: hidden;
text-overflow: ellipsis;
} }
.chapter-item.collapsed .chapter-title { .chapter-item.collapsed .chapter-title {
@ -1087,40 +1271,69 @@ const goBack = () => {
} }
.chapter-content-item { .chapter-content-item {
width: 234px; /* width: 234px; */
height: 125px; height: auto;
margin: 2px 0 0 41px; /* 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 { .content-text-group {
width: 145px; width: 100%;
height: 98px; height: auto;
margin-top: 27px; margin: 0;
padding: 4px 0;
display: flex;
flex-direction: column;
gap: 6px;
} }
.content-title { .content-title {
height: 23px; height: auto;
overflow-wrap: break-word; overflow-wrap: break-word;
color: rgba(102, 102, 102, 1); color: rgba(51, 51, 51, 1);
font-size: 16px; font-size: 15px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif; font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal; font-weight: 600;
text-align: left; text-align: left;
white-space: nowrap; line-height: 20px;
line-height: 18px; margin: 0;
margin-left: 6px; 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 { .content-description {
height: 45px; height: auto;
overflow-wrap: break-word; overflow-wrap: break-word;
color: rgba(102, 102, 102, 1); color: rgba(153, 153, 153, 1);
font-size: 16px; font-size: 13px;
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;
line-height: 18px; 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 { .action-menu {
@ -1193,9 +1406,9 @@ const goBack = () => {
height: 21px; height: 21px;
overflow-wrap: break-word; overflow-wrap: break-word;
color: rgba(51, 51, 51, 1); color: rgba(51, 51, 51, 1);
font-size: 18px; font-size: 20px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif; font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal; font-weight: 600;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
line-height: 21px; line-height: 21px;
@ -1295,6 +1508,64 @@ const goBack = () => {
margin: 1px 0 0 5px; 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 样式 */ /* 删除旧的课件输入框样式,使用 Naive UI 样式 */
.courseware-dropdown-icon { .courseware-dropdown-icon {