feat:对接部分课程和班级接口
This commit is contained in:
parent
7fb049d31d
commit
7911565249
309
src/api/modules/teachCourse.ts
Normal file
309
src/api/modules/teachCourse.ts
Normal 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' }
|
||||
});
|
||||
}
|
||||
}
|
223
src/components/ResourceLibraryModal.vue
Normal file
223
src/components/ResourceLibraryModal.vue
Normal 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>
|
@ -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: () => {
|
||||
const index = courseList.value.findIndex(c => c.id === course.id);
|
||||
if (index > -1) {
|
||||
courseList.value.splice(index, 1);
|
||||
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 targetCourse = courseList.value.find(c => c.id === course.id);
|
||||
if (targetCourse) {
|
||||
targetCourse.status = '下架中';
|
||||
message.success(`课程"${course.name}"已下架`);
|
||||
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 = 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%;
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 })
|
||||
// 假设后端返回新班级id,前端可用res.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()
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
|
@ -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',
|
||||
name: 'PracticeExam',
|
||||
// component: () => import('../views/teacher/course/PracticeExam.vue'),
|
||||
component: ExamLibrary,
|
||||
meta: { title: '试卷' }
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
name: 'PracticeReview',
|
||||
// component: () => import('../views/teacher/course/PracticeReview.vue'),
|
||||
component: MarkingCenter,
|
||||
meta: { title: '阅卷中心' }
|
||||
}
|
||||
]
|
||||
path: 'practice/exam',
|
||||
name: 'PracticeExam',
|
||||
component: ExamLibrary,
|
||||
meta: { title: '试卷管理' }
|
||||
},
|
||||
{
|
||||
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: 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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
// 跳转到预览页面
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复试卷数据
|
||||
|
@ -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('题目保存成功');
|
||||
|
||||
// 返回题库管理页面
|
||||
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) {
|
||||
console.error('创建题目流程失败:', error);
|
||||
|
@ -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 === '试卷分析'){
|
||||
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;
|
||||
}
|
||||
message.info(`执行操作: ${action} on row ${row.id}`);
|
||||
@ -189,8 +219,16 @@ const paginationConfig = computed(() => ({
|
||||
}));
|
||||
|
||||
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>
|
||||
|
@ -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
|
||||
router.push({
|
||||
name: 'StudentList',
|
||||
params: { paperId: exam.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) => {
|
||||
|
@ -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) => {
|
||||
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) => {
|
||||
|
@ -196,7 +196,16 @@ const currentBankName = ref('加载中...');
|
||||
|
||||
// 返回题库管理页面
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
|
@ -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 = () => {
|
||||
|
@ -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">第一章 课前准备</span>
|
||||
<n-dropdown v-show="isChapterExpanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
|
||||
<img class="chapter-options-icon" :class="{ 'transparent': !isChapterExpanded }"
|
||||
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
|
||||
</n-dropdown>
|
||||
</div>
|
||||
<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>
|
||||
<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 }}章 {{ 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 class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
|
||||
<span class="action-rename">重命名</span>
|
||||
<span class="action-delete" @click="openDeleteModal">删除</span>
|
||||
<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">{{ 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 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">第二章 课前准备</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">第三章 课前准备</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>
|
||||
</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,
|
||||
lessonName: '开课彩蛋新开始',
|
||||
coursewareName: '课件准备PPT',
|
||||
selectedExamOption: '',
|
||||
homeworkName: '请添加作业',
|
||||
uploadedFiles: [],
|
||||
homeworkFiles: []
|
||||
name: '课前准备',
|
||||
expanded: true,
|
||||
sections: [
|
||||
{
|
||||
id: 1,
|
||||
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,
|
||||
lessonName: '开课彩蛋新开始',
|
||||
coursewareName: '课件准备PPT',
|
||||
selectedExamOption: '',
|
||||
homeworkName: '请添加作业',
|
||||
uploadedFiles: [],
|
||||
homeworkFiles: []
|
||||
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 {
|
||||
|
0
src/views/teacher/course/PracticeLayout.vue
Normal file
0
src/views/teacher/course/PracticeLayout.vue
Normal file
Loading…
x
Reference in New Issue
Block a user