style: 课件部分弹窗
BIN
public/images/profile/default-file.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
public/images/profile/doc.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 980 B |
Before Width: | Height: | Size: 963 B After Width: | Height: | Size: 963 B |
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 705 B |
@ -100,7 +100,7 @@ const courseList = ref([
|
||||
|
||||
// 跳转到创建课程页面
|
||||
const navigateToCreateCourse = () => {
|
||||
router.push('/teacher/course-create');
|
||||
// router.push('/teacher/course-create');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
232
src/components/common/CommonModal.vue
Normal 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>
|
129
src/components/common/CreateFolderContent.vue
Normal 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>
|
216
src/components/common/MoveFileContent.vue
Normal 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>
|
@ -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',
|
||||
|
@ -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%;
|
||||
|
955
src/views/teacher/course/AddQuestion.vue
Normal 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>
|
@ -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="练考通" />
|
||||
|
||||
<!-- 练考通二级导航 -->
|
||||
<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>
|
@ -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;
|
||||
|
655
src/views/teacher/course/LocalUploadModal.vue
Normal 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">×</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>
|
15
src/views/teacher/course/PracticeExam.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="practice-exam">
|
||||
<h2>试卷管理</h2>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
15
src/views/teacher/course/PracticeReview.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="practice-review">
|
||||
<h2>阅卷中心</h2>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -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 = () => {
|
||||
|
@ -356,7 +356,6 @@ watch(() => props.show, (newVal) => {
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
@ -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;
|
||||
|