style: 课件部分弹窗

This commit is contained in:
Wxp 2025-08-24 18:20:16 +08:00
parent 16ee40e020
commit 7be3eca61e
21 changed files with 3355 additions and 1078 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 980 B

View File

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 963 B

View File

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 705 B

View File

@ -100,7 +100,7 @@ const courseList = ref([
//
const navigateToCreateCourse = () => {
router.push('/teacher/course-create');
// router.push('/teacher/course-create');
};
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,232 @@
<template>
<div v-if="visible" class="common-modal">
<div class="modal-overlay" @click="closeModal"></div>
<div class="modal-content">
<!-- 模态框标题 -->
<div class="modal-header">
<h3 class="modal-title">{{ title }}</h3>
</div>
<!-- 模态框内容区域 -->
<div class="modal-body">
<slot name="content" :get-value="getValueFromContent"></slot>
</div>
<!-- 模态框底部按钮 -->
<div class="modal-footer">
<button class="btn btn-cancel" @click="closeModal">{{ cancelText }}</button>
<button
class="btn btn-confirm"
@click="confirmAction"
:disabled="disabled"
>
{{ confirmText }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, onMounted } from 'vue'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
required: true
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
},
disabled: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits(['close', 'confirm'])
//
const contentValue = ref(null)
//
const getValueFromContent = (value: any) => {
console.log('CommonModal.getValueFromContent 接收到值:', value)
contentValue.value = value
}
//
const closeModal = () => {
emit('close')
}
//
const confirmAction = async () => {
if (!props.disabled) {
console.log('CommonModal.confirmAction 准备发送值:', contentValue.value)
//
emit('confirm', contentValue.value)
}
}
</script>
<style scoped>
.common-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 2px;
width: 580px;
min-width: 580px;
min-height: 380px;
max-width: 90vw;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 20px;
}
.modal-header {
padding-bottom: 10px;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-title {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #000;
text-align: left;
}
.modal-body {
padding: 24px 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: left;
}
.modal-footer {
padding-bottom: 10px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-cancel {
background-color: white;
border: 1px solid #d9d9d9;
color: #666;
}
.btn-cancel:hover {
border-color: #0288D1;
color: #0288D1;
}
.btn-confirm {
background-color: #0288D1;
border: 1px solid #0288D1;
color: white;
}
.btn-confirm:hover {
background-color: #0277BD;
border-color: #0277BD;
}
.btn-confirm:disabled {
background-color: #f5f5f5;
border-color: #d9d9d9;
color: #bfbfbf;
cursor: not-allowed;
}
/* 响应式设计 */
@media (max-width: 768px) {
.modal-content {
width: 90vw;
min-width: 320px;
min-height: 300px;
margin: 20px;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 16px 20px;
}
.btn {
min-width: 70px;
height: 32px;
font-size: 13px;
}
}
@media (max-width: 480px) {
.modal-content {
width: 95vw;
min-width: 280px;
min-height: 280px;
margin: 10px;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 12px 16px;
}
.btn {
min-width: 60px;
height: 30px;
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div class="create-folder-content">
<div class="form-group">
<label class="form-label">
<span class="required-asterisk">*</span>
文件夹名称:
</label>
<input
type="text"
v-model="folderName"
class="form-input"
placeholder="请输入文件夹名称"
@keyup.enter="handleConfirm"
ref="folderNameInput"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
getValue: {
type: Function,
required: true
}
})
// Emits
const emit = defineEmits(['confirm'])
//
const folderName = ref('')
const folderNameInput = ref<HTMLInputElement>()
//
watch(() => props.visible, (newVal) => {
if (newVal) {
folderName.value = ''
nextTick(() => {
folderNameInput.value?.focus()
})
}
})
//
watch(folderName, (newVal) => {
if (props.getValue) {
props.getValue(newVal.trim())
}
})
//
const handleConfirm = () => {
const trimmedName = folderName.value.trim()
if (trimmedName) {
emit('confirm', trimmedName)
}
}
//
defineExpose({
getFolderName: () => {
const name = folderName.value.trim()
console.log('CreateFolderContent.getFolderName() 返回:', name)
return name
},
clearFolderName: () => {
folderName.value = ''
}
})
</script>
<style scoped>
.create-folder-content {
width: 100%;
}
.form-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
width: 100%;
}
.form-label {
font-size: 14px;
color: #333;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.required-asterisk {
color: #ff4d4f;
margin-right: 4px;
}
.form-input {
flex: 1;
height: 36px;
padding: 0 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
color: #333;
background: white;
transition: all 0.3s ease;
box-sizing: border-box;
min-width: 0;
}
.form-input:focus {
outline: none;
border-color: #0288D1;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
}
.form-input::placeholder {
color: #bfbfbf;
}
</style>

View File

@ -0,0 +1,216 @@
<template>
<div class="move-file-content">
<div class="form-group">
<label class="form-label">
<span class="required-asterisk">*</span>
选择文件:
</label>
<div class="select-wrapper">
<select
v-model="selectedFolder"
class="form-select"
@change="handleFolderChange"
>
<option value="" disabled>请选择文件夹</option>
<option
v-for="folder in availableFolders"
:key="folder.id"
:value="folder.id"
>
{{ folder.name }}
</option>
</select>
<div class="select-arrow">
<img src="/images/teacher/箭头-灰.png" alt="下拉箭头" class="arrow-icon">
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
availableFolders: {
type: Array as () => Array<{id: number, name: string}>,
default: () => []
},
getValue: {
type: Function,
required: true
}
})
// Emits
const emit = defineEmits(['confirm'])
//
const selectedFolder = ref('')
//
watch(() => props.visible, (newVal) => {
if (newVal) {
selectedFolder.value = ''
}
})
//
watch(selectedFolder, (newVal) => {
if (props.getValue && newVal) {
const folder = props.availableFolders.find(f => f.id === newVal)
if (folder) {
props.getValue(folder)
}
}
})
//
const handleFolderChange = () => {
if (selectedFolder.value) {
const folder = props.availableFolders.find(f => f.id === selectedFolder.value)
if (folder) {
emit('confirm', folder)
}
}
}
//
defineExpose({
getSelectedFolder: () => selectedFolder.value,
clearSelection: () => {
selectedFolder.value = ''
}
})
</script>
<style scoped>
.move-file-content {
width: 100%;
}
.form-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
width: 100%;
}
.form-label {
font-size: 14px;
color: #333;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.required-asterisk {
color: #ff4d4f;
margin-right: 4px;
}
.select-wrapper {
position: relative;
flex: 1;
min-width: 0;
}
.form-select {
width: 100%;
height: 36px;
padding: 0 12px;
padding-right: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
color: #333;
background: white;
transition: all 0.3s ease;
box-sizing: border-box;
appearance: none;
cursor: pointer;
background-image: none;
}
.form-select:focus {
outline: none;
border-color: #0288D1;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
}
.form-select:disabled {
background-color: #f5f5f5;
color: #bfbfbf;
cursor: not-allowed;
}
/* 下拉框选项样式 */
.form-select option {
padding: 8px 12px;
font-size: 14px;
color: #333;
background: white;
border: none;
}
.form-select option:hover {
background-color: #f5f5f5;
}
.form-select option:checked {
background-color: #e6f7ff;
color: #0288D1;
}
.form-select option:disabled {
color: #bfbfbf;
background-color: #f5f5f5;
font-style: italic;
}
/* 默认提示选项样式 */
.form-select option[value=""] {
color: #bfbfbf;
background-color: #fafafa;
font-style: italic;
}
/* 下拉框悬停效果 */
.form-select:hover:not(:disabled) {
border-color: #40a9ff;
}
/* 确保下拉框在不同浏览器中显示一致 */
.form-select::-ms-expand {
display: none;
}
.select-arrow {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
transition: opacity 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.arrow-icon {
width: 12px;
height: 12px;
object-fit: contain;
transition: opacity 0.3s ease;
}
.form-select:focus + .select-arrow .arrow-icon {
opacity: 0.8;
}
</style>

View File

@ -120,7 +120,7 @@ const routes: RouteRecordRaw[] = [
path: 'course-editor/:id',
name: 'CourseEditor',
component: CourseEditor,
meta: { title: '编辑课程' },
meta: { title: '课程管理' },
redirect: (to) => `/teacher/course-editor/${to.params.id}/courseware`,
children: [
{
@ -157,9 +157,23 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'practice',
name: 'PracticeManagement',
component: PracticeManagement,
meta: { title: '考试管理' },
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'),
meta: { title: '试卷' }
},
{
path: 'review',
name: 'PracticeReview',
component: () => import('../views/teacher/course/PracticeReview.vue'),
meta: { title: '阅卷中心' }
}
]
},
{
path: 'question-bank',
@ -167,6 +181,12 @@ const routes: RouteRecordRaw[] = [
component: QuestionBankManagement,
meta: { title: '题库管理' }
},
{
path: 'add-question',
name: 'AddQuestion',
component: () => import('../views/teacher/course/AddQuestion.vue'),
meta: { title: '新增试题' }
},
{
path: 'certificate',
name: 'CertificateManagement',

View File

@ -83,13 +83,30 @@ const breadcrumbItems = computed(() => {
// matched
const matchedRoutes = route.matched;
// matchedRoutes''
return matchedRoutes
//
let breadcrumbs = matchedRoutes
.filter(item => item.meta.title !== '管理后台')
.map(item => ({
title: item.meta.title || '未知页面',
path: item.path
}));
// ""
const currentPath = route.path;
if (currentPath.includes('/add-question')) {
//
const courseIndex = breadcrumbs.findIndex(item => item.title === '课程管理');
if (courseIndex !== -1) {
//
const courseId = route.params.id;
breadcrumbs.splice(courseIndex + 1, 0, {
title: '题库',
path: `/teacher/course-editor/${courseId}/question-bank`
});
}
}
return breadcrumbs;
});
//
@ -120,6 +137,10 @@ const updateActiveNavItem = () => {
</script>
<style scoped>
.admin-dashboard {
padding-top: 64px;
min-height: 100vh;
}
.top-image-container {
position: relative;
width: 100%;

View File

@ -0,0 +1,955 @@
<template>
<div class="add-question">
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 左侧表单区域 -->
<div class="form-section">
<form class="question-form">
<!-- 题目类型 -->
<div class="form-group">
<label class="form-label required">题目类型</label>
<div class="question-type-tabs">
<button
v-for="type in questionTypes"
:key="type.value"
type="button"
class="type-tab"
:class="{ active: selectedType === type.value }"
@click="selectedType = type.value"
>
{{ type.label }}
</button>
</div>
</div>
<!-- 题目内容 -->
<div class="form-group">
<label class="form-label required">题目内容</label>
<div class="rich-editor">
<div class="editor-toolbar">
<select class="font-size-select">
<option value="12">12</option>
<option value="14">14</option>
<option value="16">16</option>
<option value="18">18</option>
</select>
<button type="button" class="toolbar-btn" title="加粗">B</button>
<button type="button" class="toolbar-btn" title="文字样式">Aa</button>
<button type="button" class="toolbar-btn" title="对齐">A</button>
<button type="button" class="toolbar-btn" title="列表"></button>
<button type="button" class="toolbar-btn" title="插入图片"></button>
</div>
<textarea
v-model="questionContent"
class="question-content-input"
placeholder="请输入题目内容"
rows="6"
></textarea>
</div>
</div>
<!-- 选择答案 -->
<div class="form-group" v-if="selectedType === 'single' || selectedType === 'multiple'">
<label class="form-label required">选择答案</label>
<div class="answer-options">
<div
v-for="(option, index) in answerOptions"
:key="index"
class="answer-option"
>
<input
:type="selectedType === 'single' ? 'radio' : 'checkbox'"
:name="selectedType === 'single' ? 'correctAnswer' : 'correctAnswers'"
:value="option.letter"
:checked="selectedType === 'single' ? singleAnswer === option.letter : multipleAnswers.includes(option.letter)"
@change="handleOptionChange(option.letter)"
:id="`option-${option.letter}`"
/>
<label :for="`option-${option.letter}`" class="option-label">{{ option.letter }}.</label>
<input
v-model="option.content"
type="text"
class="option-input"
placeholder="请输入内容"
/>
</div>
<button type="button" class="add-option-btn" @click="addOption">
+添加选项
</button>
</div>
</div>
<!-- 判断题答案 -->
<div class="form-group" v-if="selectedType === 'judge'">
<label class="form-label required">正确答案</label>
<div class="judge-answer">
<label class="radio-label">
<input type="radio" v-model="judgeAnswer" value="true" />
<span>正确</span>
</label>
<label class="radio-label">
<input type="radio" v-model="judgeAnswer" value="false" />
<span>错误</span>
</label>
</div>
</div>
<!-- 填空题答案 -->
<div class="form-group" v-if="selectedType === 'fill'">
<label class="form-label required">正确答案</label>
<div class="fill-answers">
<div
v-for="(answer, index) in fillAnswers"
:key="index"
class="fill-answer-item"
>
<span class="blank-number">{{ index + 1 }}</span>
<input
v-model="answer.content"
type="text"
class="fill-answer-input"
placeholder="请输入答案"
/>
<button
type="button"
class="remove-blank-btn"
@click="removeBlank(index)"
v-if="fillAnswers.length > 1"
>
×
</button>
</div>
<button type="button" class="add-blank-btn" @click="addBlank">
+添加空格
</button>
</div>
</div>
<!-- 简答题答案 -->
<div class="form-group" v-if="selectedType === 'short'">
<label class="form-label required">参考答案</label>
<textarea
v-model="shortAnswer"
class="short-answer-input"
placeholder="请输入参考答案"
rows="4"
></textarea>
</div>
<!-- 答案解析 -->
<div class="form-group">
<label class="form-label">答案解析</label>
<textarea
v-model="answerAnalysis"
class="analysis-input"
placeholder="请输入答案解析"
rows="4"
></textarea>
</div>
<!-- 分类难度分值 -->
<div class="form-row">
<div class="form-group">
<label class="form-label required">分类</label>
<select v-model="selectedCategory" class="form-select">
<option value="">请选择分类</option>
<option value="folder1">文件夹一</option>
<option value="folder2">文件夹二</option>
<option value="folder3">文件夹三</option>
</select>
</div>
<div class="form-group">
<label class="form-label required">难度</label>
<select v-model="selectedDifficulty" class="form-select">
<option value="">请选择难度</option>
<option value="easy"></option>
<option value="medium"></option>
<option value="hard"></option>
</select>
</div>
<div class="form-group">
<label class="form-label required">分值</label>
<select v-model="selectedScore" class="form-select">
<option value="">请选择分值</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
<option value="20">20</option>
</select>
</div>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<button type="button" class="btn btn-cancel" @click="cancel">取消</button>
<button type="button" class="btn btn-save" @click="saveQuestion">保存</button>
</div>
</form>
</div>
<!-- 右侧预览区域 -->
<div class="preview-section">
<h3 class="preview-title">题目预览</h3>
<div class="question-preview">
<!-- 题目内容预览 -->
<div class="preview-question">
<span class="question-number">1.</span>
<span class="question-text">{{ questionContent || '请输入题目内容' }}</span>
</div>
<!-- 答案选项预览 -->
<div v-if="selectedType === 'single' || selectedType === 'multiple'" class="preview-options">
<div
v-for="option in answerOptions"
:key="option.letter"
class="preview-option"
:class="{
'correct': isOptionCorrect(option.letter),
'selected': isOptionCorrect(option.letter)
}"
>
<span class="option-letter">{{ option.letter }}.</span>
<span class="option-content">{{ option.content || '请输入内容' }}</span>
</div>
</div>
<!-- 判断题预览 -->
<div v-if="selectedType === 'judge'" class="preview-judge">
<div class="preview-option" :class="{ 'correct': judgeAnswer === 'true' }">
<span class="option-letter">A.</span>
<span class="option-content">正确</span>
</div>
<div class="preview-option" :class="{ 'correct': judgeAnswer === 'false' }">
<span class="option-letter">B.</span>
<span class="option-content">错误</span>
</div>
</div>
<!-- 填空题预览 -->
<div v-if="selectedType === 'fill'" class="preview-fill">
<div class="preview-question">
<span class="question-number">1.</span>
<span class="question-text">
{{ getFillQuestionText() }}
</span>
</div>
<div class="fill-answers-preview">
<div
v-for="(answer, index) in fillAnswers"
:key="index"
class="fill-answer-preview"
>
<span class="blank-label">{{ index + 1 }}</span>
<span class="blank-answer">{{ answer.content || '_____' }}</span>
</div>
</div>
</div>
<!-- 简答题预览 -->
<div v-if="selectedType === 'short'" class="preview-short">
<div class="short-answer-preview">
<strong>参考答案</strong>
<p>{{ shortAnswer || '请输入参考答案' }}</p>
</div>
</div>
<!-- 答案解析预览 -->
<div v-if="answerAnalysis" class="preview-analysis">
<strong>答案解析</strong>
<p>{{ answerAnalysis }}</p>
</div>
<!-- 元数据预览 -->
<div class="preview-metadata">
<div class="metadata-item">
<span class="metadata-label">分类</span>
<span class="metadata-value">{{ getCategoryText(selectedCategory) }}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">难度</span>
<span class="metadata-value">{{ getDifficultyText(selectedDifficulty) }}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">分值</span>
<span class="metadata-value">{{ selectedScore ? selectedScore + '分' : '' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
//
const questionTypes = [
{ value: 'single', label: '单选题' },
{ value: 'multiple', label: '多选题' },
{ value: 'judge', label: '判断题' },
{ value: 'fill', label: '填空题' },
{ value: 'short', label: '简答题' },
{ value: 'composite', label: '复合题' }
]
//
const selectedType = ref('single')
const questionContent = ref('')
const answerOptions = ref([
{ letter: 'A', content: '' },
{ letter: 'B', content: '' },
{ letter: 'C', content: '' },
{ letter: 'D', content: '' }
])
const singleAnswer = ref('')
const multipleAnswers = ref<string[]>([])
const judgeAnswer = ref('')
const fillAnswers = ref([{ content: '' }])
const shortAnswer = ref('')
const answerAnalysis = ref('')
const selectedCategory = ref('')
const selectedDifficulty = ref('')
const selectedScore = ref('')
//
const isOptionCorrect = (letter: string) => {
if (selectedType.value === 'single') {
return singleAnswer.value === letter
} else if (selectedType.value === 'multiple') {
return multipleAnswers.value.includes(letter)
}
return false
}
//
const addOption = () => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const nextLetter = letters[answerOptions.value.length]
if (nextLetter) {
answerOptions.value.push({ letter: nextLetter, content: '' })
}
}
const handleOptionChange = (letter: string) => {
if (selectedType.value === 'single') {
singleAnswer.value = letter
} else if (selectedType.value === 'multiple') {
if (multipleAnswers.value.includes(letter)) {
multipleAnswers.value = multipleAnswers.value.filter((l: string) => l !== letter)
} else {
multipleAnswers.value.push(letter)
}
}
}
const addBlank = () => {
fillAnswers.value.push({ content: '' })
}
const removeBlank = (index: number) => {
if (fillAnswers.value.length > 1) {
fillAnswers.value.splice(index, 1)
}
}
const getFillQuestionText = () => {
if (!questionContent.value) return '请输入题目内容'
return questionContent.value.replace(/\{\}/g, '_____')
}
const getCategoryText = (value: string) => {
const categoryMap: Record<string, string> = {
'folder1': '文件夹一',
'folder2': '文件夹二',
'folder3': '文件夹三'
}
return categoryMap[value] || ''
}
const getDifficultyText = (value: string) => {
const difficultyMap: Record<string, string> = {
'easy': '易',
'medium': '中',
'hard': '难'
}
return difficultyMap[value] || ''
}
const cancel = () => {
//
history.back()
}
const saveQuestion = () => {
//
if (!questionContent.value.trim()) {
alert('请输入题目内容')
return
}
if (selectedType.value === 'single' && !singleAnswer.value) {
alert('请选择正确答案')
return
}
if (selectedType.value === 'multiple' && multipleAnswers.value.length === 0) {
alert('请选择正确答案')
return
}
if (selectedType.value === 'judge' && !judgeAnswer.value) {
alert('请选择正确答案')
return
}
if (selectedType.value === 'fill' && fillAnswers.value.some((a: { content: string }) => !a.content.trim())) {
alert('请填写所有空格的答案')
return
}
if (selectedType.value === 'short' && !shortAnswer.value.trim()) {
alert('请输入参考答案')
return
}
if (!selectedCategory.value) {
alert('请选择分类')
return
}
if (!selectedDifficulty.value) {
alert('请选择难度')
return
}
if (!selectedScore.value) {
alert('请选择分值')
return
}
//
console.log('保存题目:', {
type: selectedType.value,
content: questionContent.value,
answer: selectedType.value === 'single' ? singleAnswer.value :
selectedType.value === 'multiple' ? multipleAnswers.value :
selectedType.value === 'judge' ? judgeAnswer.value :
selectedType.value === 'fill' ? fillAnswers.value :
shortAnswer.value,
analysis: answerAnalysis.value,
category: selectedCategory.value,
difficulty: selectedDifficulty.value,
score: selectedScore.value
})
alert('题目保存成功!')
}
</script>
<style scoped>
.add-question {
background: #F6F6F6;
min-height: 100vh;
padding: 0;
}
/* 主要内容区域 */
.main-content {
display: flex;
gap: 20px;
min-height: calc(100vh - 80px);
padding-bottom: 80px;
}
/* 左侧表单区域 */
.form-section {
flex: 3;
background: white;
padding: 24px;
}
.question-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-row {
display: flex;
gap: 24px;
}
.form-row .form-group {
flex: 1;
}
.form-label {
font-size: 14px;
font-weight: 500;
color: #333;
display: flex;
align-items: center;
}
.form-label.required::after {
content: '*';
color: #ff4d4f;
margin-left: 4px;
}
/* 题目类型标签 */
.question-type-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.type-tab {
padding: 8px 16px;
border: 1px solid #d9d9d9;
background: white;
color: #666;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.type-tab:hover {
border-color: #0288D1;
color: #0288D1;
}
.type-tab.active {
background: #0288D1;
color: white;
border-color: #0288D1;
}
/* 富文本编辑器 */
.rich-editor {
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.font-size-select {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 12px;
}
.toolbar-btn {
width: 24px;
height: 24px;
border: 1px solid #d9d9d9;
background: white;
border-radius: 2px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: all 0.3s ease;
}
.toolbar-btn:hover {
border-color: #0288D1;
color: #0288D1;
}
.question-content-input {
width: 100%;
padding: 12px;
border: none;
outline: none;
resize: vertical;
font-size: 14px;
line-height: 1.6;
min-height: 120px;
}
/* 答案选项 */
.answer-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.answer-option {
display: flex;
align-items: center;
gap: 8px;
}
.option-label {
font-size: 14px;
color: #333;
min-width: 20px;
}
.option-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.option-input:focus {
outline: none;
border-color: #0288D1;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
}
.add-option-btn {
color: #0288D1;
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 8px 0;
text-align: left;
}
.add-option-btn:hover {
text-decoration: underline;
}
/* 判断题答案 */
.judge-answer {
display: flex;
gap: 24px;
}
.radio-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
/* 填空题答案 */
.fill-answers {
display: flex;
flex-direction: column;
gap: 12px;
}
.fill-answer-item {
display: flex;
align-items: center;
gap: 8px;
}
.blank-number {
font-size: 14px;
color: #333;
min-width: 60px;
}
.fill-answer-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.remove-blank-btn {
width: 24px;
height: 24px;
border: 1px solid #ff4d4f;
background: white;
color: #ff4d4f;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.add-blank-btn {
color: #0288D1;
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 8px 0;
text-align: left;
}
/* 简答题答案 */
.short-answer-input {
width: 100%;
padding: 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
/* 答案解析 */
.analysis-input {
width: 100%;
padding: 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
/* 表单选择器 */
.form-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
background: white;
}
.form-select:focus {
outline: none;
border-color: #0288D1;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
}
/* 操作按钮 */
.form-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background: white;
z-index: 1000;
}
.btn {
padding: 12px 12px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
min-width: 100px;
font-weight: 500;
}
.btn-cancel {
background: white;
color: #0288D1;
border: 1px solid #0288D1;
}
.btn-cancel:hover {
background: #f0f8ff;
}
.btn-save {
background: #0288D1;
color: white;
}
.btn-save:hover {
background: #0277BD;
}
/* 右侧预览区域 */
.preview-section {
flex: 1;
background: white;
padding: 24px;
height: fit-content;
}
.preview-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid #e8e8e8;
}
.question-preview {
display: flex;
flex-direction: column;
gap: 20px;
}
.preview-question {
display: flex;
gap: 8px;
line-height: 1.6;
}
.question-number {
font-weight: 500;
color: #333;
}
.question-text {
color: #333;
flex: 1;
}
.preview-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-option {
display: flex;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s ease;
}
.preview-option.correct {
background: #e6f7ff;
color: #0288D1;
}
.preview-option.selected {
background: #0288D1;
color: white;
}
.option-letter {
font-weight: 500;
min-width: 20px;
}
.option-content {
flex: 1;
}
.preview-judge {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-fill {
display: flex;
flex-direction: column;
gap: 16px;
}
.fill-answers-preview {
display: flex;
flex-direction: column;
gap: 8px;
}
.fill-answer-preview {
display: flex;
gap: 8px;
align-items: center;
}
.blank-label {
font-size: 14px;
color: #666;
min-width: 60px;
}
.blank-answer {
color: #0288D1;
font-weight: 500;
}
.short-answer-preview {
line-height: 1.6;
}
.short-answer-preview p {
margin: 8px 0 0 0;
color: #333;
}
.preview-analysis {
line-height: 1.6;
}
.preview-analysis p {
margin: 8px 0 0 0;
color: #333;
}
.preview-metadata {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #e8e8e8;
}
.metadata-item {
display: flex;
gap: 8px;
}
.metadata-label {
font-size: 14px;
color: #666;
min-width: 40px;
}
.metadata-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
}
.form-section,
.preview-section {
flex: none;
}
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 16px;
}
.question-type-tabs {
justify-content: center;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="course-editor">
<!-- 左侧导航菜单 -->
<div class="sidebar">
<div class="sidebar" v-if="!hideSidebar">
<router-link :to="`/teacher/course-editor/${courseId}/courseware`" class="menu-item"
:class="{ active: $route.path.includes('courseware') }">
<img :src="$route.path.includes('courseware') ? '/images/teacher/课件-选中.png' : '/images/teacher/课件.png'"
@ -17,34 +17,52 @@
<!-- 作业二级导航 -->
<div class="menu-group">
<div class="menu-header" @click="toggleHomework">
<img :src="$route.path.includes('homework') ? '/images/teacher/作业-选中.png' : '/images/teacher/作业.png'"
alt="作业" />
<img src="/images/teacher/作业.png" alt="作业" />
<span>作业</span>
<i class="n-base-icon" :class="{ 'expanded': homeworkExpanded }">
<i class="n-base-icon" :class="{ expanded: homeworkExpanded }">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="#C2C2C2"></path>
fill="#C2C2C2" />
</svg>
</i>
</div>
<div class="submenu" v-show="homeworkExpanded">
<router-link :to="`/teacher/course-editor/${courseId}/homework/library`" class="submenu-item"
:class="{ active: $route.path.includes('homework/library') }">
:class="{ active: $route.path.includes('/homework/library') }">
<span>作业库</span>
</router-link>
<router-link :to="`/teacher/course-editor/${courseId}/homework/review`" class="submenu-item"
:class="{ active: $route.path.includes('homework/review') }">
:class="{ active: $route.path.includes('/homework/review') }">
<span>批阅作业</span>
</router-link>
</div>
</div>
<router-link :to="`/teacher/course-editor/${courseId}/practice`" class="menu-item"
:class="{ active: $route.path.includes('practice') }">
<img :src="$route.path.includes('practice') ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
alt="练考通" />
<span>练考通</span>
</router-link>
<!-- 练考通二级导航 -->
<div class="menu-group">
<div class="menu-header" @click="togglePractice">
<img src="/images/teacher/练考通.png" alt="练考通" />
<span>练考通</span>
<i class="n-base-icon" :class="{ expanded: practiceExpanded }">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="#C2C2C2" />
</svg>
</i>
</div>
<div class="submenu" v-show="practiceExpanded">
<router-link :to="`/teacher/course-editor/${courseId}/practice/exam`" class="submenu-item"
:class="{ active: $route.path.includes('/practice/exam') }">
<span>试卷</span>
</router-link>
<router-link :to="`/teacher/course-editor/${courseId}/practice/review`" class="submenu-item"
:class="{ active: $route.path.includes('/practice/review') }">
<span>阅卷中心</span>
</router-link>
</div>
</div>
<router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
:class="{ active: $route.path.includes('question-bank') }">
<img :src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
@ -84,7 +102,7 @@
</div>
<!-- 右侧内容区域 -->
<div class="content-area">
<div class="content-area" :class="{ 'full-width': hideSidebar }">
<router-view />
</div>
</div>
@ -92,7 +110,7 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref } from 'vue'
import { ref, computed } from 'vue'
const route = useRoute()
@ -106,6 +124,30 @@ const homeworkExpanded = ref(false)
const toggleHomework = () => {
homeworkExpanded.value = !homeworkExpanded.value
}
//
const practiceExpanded = ref(false)
// /
const togglePractice = () => {
practiceExpanded.value = !practiceExpanded.value
}
//
const hideSidebar = computed(() => {
const currentPath = route.path
//
const hideSidebarPaths = [
'add-question', //
'edit-question', //
'question-preview', //
'bulk-import', //
'question-analysis' //
]
//
return hideSidebarPaths.some(path => currentPath.includes(path))
})
</script>
<style scoped>
@ -125,6 +167,18 @@ const toggleHomework = () => {
margin-right: 5px;
}
/* 右侧内容区域 */
.content-area {
flex: 1;
overflow: auto;
}
/* 全宽显示(隐藏侧边栏的页面) */
.content-area.full-width {
width: 100%;
margin-left: 0;
}
.menu-item {
display: flex;
align-items: center;
@ -175,13 +229,13 @@ const toggleHomework = () => {
display: flex;
align-items: center;
padding: 12px 15px;
margin-bottom: 5px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 3px solid transparent;
font-size: 16px;
color: #666;
border-radius: 5px;
position: relative;
}
.menu-header:hover {
@ -199,70 +253,51 @@ const toggleHomework = () => {
.menu-header span {
font-size: 16px;
color: #666;
flex: 1;
}
.menu-header .n-base-icon {
margin-left: auto;
transition: transform 0.3s ease;
width: 16px;
height: 16px;
transition: transform 0.2s ease;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-header .n-base-icon.expanded {
.menu-header .n-base-icon svg {
width: 16px;
height: 16px;
transition: transform 0.3s ease;
}
.menu-header .n-base-icon.expanded svg {
transform: rotate(90deg);
}
.submenu {
margin-left: 0;
margin-left: 30px;
margin-bottom: 5px;
}
.submenu-item {
display: flex;
align-items: center;
padding: 10px 15px;
margin-bottom: 2px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 3px solid transparent;
display: block;
padding: 8px 15px;
margin-bottom: 3px;
text-decoration: none;
color: inherit;
font-size: 14px;
color: #666;
border-radius: 5px;
}
.submenu-item::before {
content: '';
width: 16px;
height: 16px;
margin-left: 30px;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
border-radius: 3px;
transition: all 0.3s ease;
}
.submenu-item:hover {
background: #f5f5f5;
background: #f0f8ff;
color: #0288D1;
}
.submenu-item.active {
background: #F5F8FB;
background: #e6f7ff;
color: #0288D1;
}
.submenu-item.active span {
color: #0288D1;
}
.submenu-item span {
font-size: 14px;
color: #666;
}
/* 右侧内容区域 */
.content-area {
flex: 1;
background-color: #fff;
overflow: auto;
}
</style>

View File

@ -6,7 +6,8 @@
<div class="toolbar-actions">
<button class="btn btn-primary" @click="addCourseware">添加课件</button>
<button class="btn btn-new" @click="createFolder">新建文件夹</button>
<button class="btn btn-default" @click="moveFiles" :disabled="selectedFiles.length === 0">移动</button>
<button class="btn btn-default" @click="moveFiles" :disabled="selectedFiles.length === 0"
:class="{ 'btn-default--active': selectedFiles.length > 0 }">移动</button>
<button class="btn btn-danger" @click="deleteSelected" :disabled="selectedFiles.length === 0">删除</button>
<div class="search-box">
@ -71,6 +72,22 @@
<!-- 删除确认模态框 -->
<DeleteFolderConfirmModal v-if="showDeleteConfirmModal" :show="showDeleteConfirmModal" @confirm="confirmDelete"
@cancel="cancelDelete" />
<!-- 新建文件夹模态框 -->
<CommonModal :visible="showCreateFolderModal" title="新建文件夹" @close="closeCreateFolderModal"
@confirm="handleCreateFolder">
<template #content="{ getValue }">
<CreateFolderContent :visible="showCreateFolderModal" :get-value="getValue" />
</template>
</CommonModal>
<!-- 移动文件模态框 -->
<CommonModal :visible="showMoveFileModal" title="移动文件" @close="closeMoveFileModal" @confirm="handleMoveFiles">
<template #content="{ getValue }">
<MoveFileContent :visible="showMoveFileModal" :available-folders="fileList.filter(f => f.type === 'folder')"
:get-value="getValue" />
</template>
</CommonModal>
</div>
</template>
@ -81,6 +98,9 @@ import type { DataTableColumns, DropdownOption } from 'naive-ui'
import AddCoursewareModal from './AddCoursewareModal.vue'
import UploadFileModal from './UploadFileModal.vue'
import DeleteFolderConfirmModal from '@/components/common/DeleteFolderConfirmModal.vue'
import CommonModal from '@/components/common/CommonModal.vue'
import CreateFolderContent from '@/components/common/CreateFolderContent.vue'
import MoveFileContent from '@/components/common/MoveFileContent.vue'
const message = useMessage()
@ -107,6 +127,8 @@ const selectedFiles = ref<number[]>([])
const showAddCoursewareModal = ref(false)
const showUploadFileModal = ref(false)
const showDeleteConfirmModal = ref(false)
const showCreateFolderModal = ref(false)
const showMoveFileModal = ref(false)
//
const itemsToDelete = ref<{ type: 'single' | 'multiple', data: any }>({ type: 'single', data: null })
@ -589,12 +611,67 @@ const closeAddCoursewareModal = () => {
}
const createFolder = () => {
message.info('新建文件夹功能')
showCreateFolderModal.value = true
}
const moveFiles = () => {
if (selectedFiles.value.length === 0) return
message.info(`移动 ${selectedFiles.value.length} 个文件`)
showMoveFileModal.value = true
}
//
const closeCreateFolderModal = () => {
showCreateFolderModal.value = false
}
//
const closeMoveFileModal = () => {
showMoveFileModal.value = false
}
//
const handleCreateFolder = (folderName: string) => {
console.log('handleCreateFolder 接收到参数:', folderName)
//
const newFolder: FileItem = {
id: Date.now(),
name: folderName,
type: 'folder',
size: '0B',
creator: '王建国', //
createTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '.'),
isTop: false,
expanded: false,
children: []
}
console.log('创建的新文件夹对象:', newFolder)
//
fileList.value.push(newFolder)
//
message.success(`文件夹 "${folderName}" 创建成功`)
//
showCreateFolderModal.value = false
}
//
const handleMoveFiles = (targetFolder: any) => {
//
message.success(`成功移动 ${selectedFiles.value.length} 个文件到 "${targetFolder.name}"`)
selectedFiles.value = [] //
//
showMoveFileModal.value = false
}
const deleteSelected = () => {
@ -814,6 +891,10 @@ const toggleFolder = (folder: FileItem) => {
color: #1890ff;
}
.btn-default--active {
border-color: #1890ff !important;
color: #1890ff !important;
}
.btn-danger {
background: white;
color: #FF4D4F;

View File

@ -0,0 +1,655 @@
<template>
<div class="local-upload-modal" v-if="visible">
<div class="modal-overlay" @click="closeModal"></div>
<div class="modal-content">
<!-- 弹框标题 -->
<div class="modal-header">
<h3 class="modal-title">本地上传</h3>
<!-- <button class="close-btn" @click="closeModal">&times;</button> -->
</div>
<!-- 文件上传列表 -->
<div class="file-list">
<table class="file-table">
<thead>
<tr>
<th>文件名</th>
<th>大小</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="file in fileList" :key="file.id">
<td>
<div class="file-info">
<img :src="getFileImage(file.name)" :alt="getFileExtension(file.name) + ' icon'" class="file-type-icon-img">
<span>{{ file.name }}</span>
</div>
</td>
<td>{{ formatFileSize(file.size) }}</td>
<td>
<div class="progress-bar">
<div class="progress-fill"
:class="file.status === 'success' ? 'success' : file.status === 'failed' ? 'failed' : ''"
:style="{ width: file.progress + '%' }"></div>
</div>
<span class="status-text" :class="file.status">
{{ getStatusText(file.status, file.progress) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 隐藏的文件输入框 -->
<input
type="file"
ref="fileInput"
@change="handleFileSelect"
multiple
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
style="display: none;"
/>
<!-- 上传结果通知 - 在模态框中央显示 -->
<div v-if="notifications.length > 0" class="upload-notifications-overlay">
<div class="notification-center">
<n-alert
v-for="notification in notifications"
:key="notification.id"
type="default"
:show-icon="true"
class="custom-notification-alert"
:class="notification.type"
>
<template #icon>
<img
:src="notification.type === 'success' ? '/images/teacher/upload-succeed.png' : '/images/teacher/upload-fail.png'"
:alt="notification.type === 'success' ? '上传成功' : '上传失败'"
class="notification-icon-img"
/>
</template>
{{ notification.message }}
</n-alert>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeModal">完成</button>
<button class="btn btn-primary" @click="triggerFileSelect">上传更多</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NAlert } from 'naive-ui'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits(['close', 'upload-more'])
//
const fileList = ref([
{
id: 1,
name: '这是一个表格文件.xlsx',
size: 18022, // 17.6k
status: 'success',
progress: 100
},
{
id: 2,
name: '这是一个表格文件.xlsx',
size: 18022, // 17.6k
status: 'failed',
progress: 0
}
])
const notifications = ref([]) //
//
const fileInput = ref<HTMLInputElement>()
//
const hasFailedFiles = computed(() => {
return fileList.value.some(file => file.status === 'failed')
})
//
const closeModal = () => {
emit('close')
}
//
const triggerFileSelect = () => {
fileInput.value?.click()
}
//
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
//
notifications.value = []
//
Array.from(files).forEach((file, index) => {
const newFile = {
id: Date.now() + index,
name: file.name,
size: file.size,
status: 'uploading' as const,
progress: 0
}
fileList.value.push(newFile)
//
simulateUpload(newFile)
})
//
target.value = ''
}
}
//
const simulateUpload = (file: any) => {
let progress = 0
const interval = setInterval(() => {
progress += Math.random() * 20
if (progress >= 100) {
progress = 100
file.status = 'success'
file.progress = progress
//
const notificationId = Date.now()
notifications.value.push({
id: notificationId,
type: 'success',
message: `${file.name} 上传成功`
})
// 3
setTimeout(() => {
const index = notifications.value.findIndex(n => n.id === notificationId)
if (index > -1) {
notifications.value.splice(index, 1)
}
}, 3000)
clearInterval(interval)
} else {
file.progress = Math.floor(progress)
}
}, 200)
}
//
const getFileExtension = (fileName: string) => {
return fileName.split('.').pop()?.toLowerCase() || 'default'
}
//
const getFileImage = (fileName: string) => {
const extension = getFileExtension(fileName)
const iconMap: { [key: string]: string } = {
'doc': 'doc.png',
'docx': 'doc.png',
'pdf': 'pdf.png',
'xls': 'xls.png',
'xlsx': 'xls.png',
'ppt': 'ppt.png',
'pptx': 'ppt.png',
'mp3': 'mp3.png',
'mp4': 'mp4.png',
'default': 'default-file.png' //
}
const imageName = iconMap[extension] || iconMap['default']
return `/images/profile/${imageName}`
}
const getFileIcon = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
switch (extension) {
case 'doc':
case 'docx':
return 'icon-doc'
case 'pdf':
return 'icon-pdf'
case 'xls':
case 'xlsx':
return 'icon-xls'
case 'ppt':
case 'pptx':
return 'icon-ppt'
case 'mp3':
return 'icon-mp3'
case 'mp4':
return 'icon-mp4'
default:
return 'icon-file'
}
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i]
}
const getStatusText = (status: string, progress: number) => {
switch (status) {
case 'success':
return `上传成功${progress}%`
case 'failed':
return `上传失败${progress}%`
case 'uploading':
return `上传中${progress}%`
default:
return `等待上传${progress}%`
}
}
</script>
<style scoped>
.local-upload-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
/* 文件上传区域样式 */
.file-upload-area {
display: none; /* 隐藏文件上传区域 */
}
.upload-btn {
display: none; /* 隐藏上传按钮 */
}
.upload-hint {
display: none; /* 隐藏上传提示 */
}
/* 通知容器样式 - 在模态框中央显示 */
.upload-notifications-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1001; /* 确保在最上层 */
pointer-events: none; /* 不阻挡其他交互 */
}
.notification-center {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: auto; /* 恢复通知的交互 */
}
/* 通知容器样式 */
.upload-notifications {
padding: 16px 24px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
border-top: 1px solid #e8e8e8;
}
/* 确保模态框内容使用flexbox布局 */
.modal-content {
position: relative;
background: white;
border-radius: 2px;
width: 1000px;
min-height: 700px;
max-height: 80vh;
overflow: hidden;
padding: 20px 20px 40px 20px;
display: flex;
flex-direction: column;
}
/* 文件列表区域 */
.file-list {
padding: 20px 0;
flex: 1; /* 占据剩余空间 */
}
/* 底部操作按钮 - 确保在最底部 */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: auto; /* 推到底部 */
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #000;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #666;
}
.file-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #e8e8e8;
}
.file-table th {
background-color: #fafafa;
font-weight: 600;
color: #062333;
font-size: 14px;
padding: 12px;
text-align: center;
border-bottom: 1px solid #e8e8e8;
border-right: 1px solid #e8e8e8;
}
.file-table th:last-child {
border-right: none;
}
.file-table td {
padding: 12px;
border-bottom: 1px solid #f5f5f5;
border-right: 1px solid #f5f5f5;
vertical-align: middle;
height: 50px;
font-size: 14px;
color: #062333;
}
.file-table td:last-child {
border-right: none;
}
.file-table tr:last-child td {
border-bottom: none;
}
.file-info {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.file-info img {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
}
.file-info i {
display: none; /* 隐藏旧的图标 */
}
.file-info span {
font-size: 14px;
color: #062333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-table td:nth-child(2) {
text-align: center;
}
.file-table td:nth-child(3) {
text-align: center;
}
.progress-bar {
width: 60px;
height: 6px;
background-color: #F0F0F0;
border-radius: 3px;
overflow: hidden;
display: inline-block;
margin-right: 8px;
vertical-align: middle;
}
.progress-fill {
height: 100%;
background-color: #0288D1;
transition: width 0.3s ease;
}
.progress-fill.success {
background-color: #0288D1;
}
.progress-fill.failed {
background-color: #ED1C1C;
}
.status-text {
min-width: 95px;
font-size: 14px;
color: #062333;
display: inline-block;
vertical-align: middle;
}
.status-text.success {
color: #062333;
}
.status-text.failed {
color: #ED1C1C;
}
/* 通知容器样式 */
.upload-notifications {
padding: 16px 24px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center; /* 左对齐 */
}
/* Naive UI Alert组件的自定义样式 */
.custom-notification-alert {
--n-color: #424242 !important; /* 深灰色背景 */
--n-content-text-color: #E0E0E0 !important; /* 浅色文字 */
--n-title-text-color: #E0E0E0 !important;
--n-border-radius: 8px !important; /* 圆角 */
margin-bottom: 8px; /* 通知之间的间距 */
padding: 8px 12px !important; /* 紧凑的内边距 */
border: none !important; /* 移除默认边框 */
box-shadow: none !important; /* 移除默认阴影 */
width: fit-content !important; /* 宽度自适应内容 */
max-width: 100% !important; /* 最大宽度不超过容器 */
display: flex !important; /* 使用flexbox布局 */
align-items: center !important; /* 垂直居中 */
gap: 8px !important; /* 图标和文字之间的间距 */
}
/* 强制覆盖Naive UI的默认图标样式 */
.custom-notification-alert .n-alert__icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 !important; /* 重置默认边距 */
order: -1 !important; /* 确保图标在最前面 */
width: 24px !important; /* 固定图标容器宽度 */
height: 24px !important; /* 固定图标容器高度 */
flex-shrink: 0 !important;
position: relative !important; /* 相对定位 */
}
:deep(.n-alert .n-alert__icon) {
top: 14%;
left: 10px;
}
.notification-icon-img {
width: auto !important;
height: 16px !important;
object-fit: contain !important;
flex-shrink: 0 !important;
display: block !important; /* 确保图片正确显示 */
position: absolute !important; /* 绝对定位 */
top: 50% !important; /* 垂直居中 */
left: 50% !important; /* 水平居中 */
transform: translate(-50%, -50%) !important; /* 完美居中 */
}
/* 确保文字内容正确显示 */
.custom-notification-alert .n-alert-body {
flex: 1 !important; /* 让文字区域占据剩余空间 */
}
/* 移除旧的图标样式 */
.custom-notification-alert .n-alert__icon i {
display: none;
}
/* 移除旧的图标颜色样式 */
.custom-notification-alert.success .n-alert__icon i,
.custom-notification-alert.failed .n-alert__icon i {
display: none;
}
/* 定义自定义图标内容 */
.icon-success::before {
content: "✓";
}
.icon-failed::before {
content: "✗";
}
.btn {
padding: 6px 16px;
border: 1px solid;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
min-width: 100px;
}
.btn-secondary {
background-color: #E2F5FF;
border-color: #0288D1;
color: #0288D1;
}
.btn-secondary:hover {
border-color: #0288D1;
color: #0288D1;
}
.btn-primary {
background-color: #0288D1;
border-color: #0288D1;
color: white;
}
.btn-primary:hover {
background-color: #0288D1;
border-color: #0288D1;
}
/* 文件类型图标样式 */
.icon-doc::before {
content: "📄";
}
.icon-pdf::before {
content: "📕";
}
.icon-xls::before {
content: "📊";
}
.icon-ppt::before {
content: "📑";
}
.icon-mp3::before {
content: "🎵";
}
.icon-mp4::before {
content: "🎬";
}
.icon-file::before {
content: "📁";
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="practice-exam">
<h2>试卷管理</h2>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="practice-review">
<h2>阅卷中心</h2>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -51,8 +51,11 @@ import {
dateZhCN
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useRoute, useRouter } from 'vue-router'
const message = useMessage()
const route = useRoute()
const router = useRouter()
//
interface Question {
@ -440,7 +443,8 @@ const searchQuestions = () => {
}
const addQuestion = () => {
message.info('添加试题功能')
const courseId = route.params.id
router.push(`/teacher/course-editor/${courseId}/add-question`)
}
const importQuestions = () => {

View File

@ -356,7 +356,6 @@ watch(() => props.show, (newVal) => {
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #e0e0e0;
}
/* 响应式设计 */

View File

@ -7,9 +7,9 @@
<span class="btn-text">选择文件</span>
<!-- 下拉选项 -->
<div v-show="showDropdown" class="upload-methods flex-col">
<label class="local-upload">
<input type="file" @change="handleLocalUpload" style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4" />
<label class="local-upload" @click="openLocalUpload">
<!-- <input type="file" @change="handleLocalUpload" style="display: none;"
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4" /> -->
本地上传
</label>
<label class="resource-upload" @click="openResourceModal">
@ -42,31 +42,38 @@
<!-- 资源选择模态框 -->
<ResourceSelectionModal v-model:show="showResourceModal" @select="handleResourceSelection" />
<!-- 本地上传模态框 -->
<LocalUploadModal :visible="showLocalUploadModal" @close="closeLocalUploadModal" @upload-more="handleUploadMore" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ResourceSelectionModal from './ResourceSelectionModal.vue'
import LocalUploadModal from './LocalUploadModal.vue'
//
const showDropdown = ref(false)
//
const showLocalUploadModal = ref(false)
//
const toggleDropdown = () => {
showDropdown.value = !showDropdown.value
}
//
const handleLocalUpload = (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
console.log('本地上传文件:', files[0])
//
showDropdown.value = false
}
}
// const handleLocalUpload = (event: Event) => {
// const target = event.target as HTMLInputElement
// const files = target.files
// if (files && files.length > 0) {
// console.log(':', files[0])
// //
// showDropdown.value = false
// }
// }
//
// const handleResourceUpload = (event: Event) => {
@ -79,6 +86,24 @@ const handleLocalUpload = (event: Event) => {
// }
// }
//
const openLocalUpload = () => {
showLocalUploadModal.value = true
showDropdown.value = false
}
//
const closeLocalUploadModal = () => {
showLocalUploadModal.value = false
}
//
const handleUploadMore = () => {
closeLocalUploadModal()
//
console.log('用户选择上传更多文件')
}
//
const openResourceModal = () => {
console.log('打开资源选择模态框')
@ -173,7 +198,6 @@ const handleConfirm = () => {
.btn-text {
width: 100%;
height: 21px;
overflow-wrap: break-word;
color: rgba(255, 255, 255, 1);
font-size: 18px;
@ -181,7 +205,6 @@ const handleConfirm = () => {
font-weight: normal;
text-align: center;
white-space: nowrap;
line-height: 21px;
margin: 6px 0 0 0;
display: flex;
align-items: center;