style: 章节模态框

This commit is contained in:
QDKF 2025-08-25 18:28:46 +08:00
parent 2d0dd00fc4
commit cc7c4ec23a
9 changed files with 2278 additions and 179 deletions

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,179 @@
<template>
<div ref="dropdownRef" class="custom-dropdown" :class="{ 'open': isOpen }">
<div class="dropdown-input" @click="toggleDropdown">
<div class="input-content">
{{ displayValue || placeholder }}
</div>
<div class="dropdown-icon" :class="{ 'rotated': isOpen }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.14645 5.64645C3.34171 5.45118 3.65829 5.45118 3.85355 5.64645L8 9.79289L12.1464 5.64645C12.3417 5.45118 12.6583 5.45118 12.8536 5.64645C13.0488 5.84171 13.0488 6.15829 12.8536 6.35355L8.35355 10.8536C8.15829 11.0488 7.84171 11.0488 7.64645 10.8536L3.14645 6.35355C2.95118 6.15829 2.95118 5.84171 3.14645 5.64645Z" fill="currentColor"/>
</svg>
</div>
</div>
<div v-if="isOpen" class="dropdown-menu">
<div
v-for="option in options"
:key="option.value"
class="dropdown-option"
:class="{ 'first-option': option.value === '从考试/练习选择' }"
@click="selectOption(option)"
>
{{ option.label }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
interface DropdownOption {
label: string
value: string
}
interface Props {
modelValue?: string
options: DropdownOption[]
placeholder?: string
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择',
modelValue: ''
})
const emit = defineEmits<{
'update:modelValue': [value: string]
'change': [value: string]
}>()
const isOpen = ref(false)
const dropdownRef = ref<HTMLElement>()
const displayValue = computed(() => {
if (!props.modelValue) return ''
const option = props.options.find((opt: DropdownOption) => opt.value === props.modelValue)
return option ? option.label : props.modelValue
})
const toggleDropdown = () => {
isOpen.value = !isOpen.value
}
const selectOption = (option: DropdownOption) => {
emit('update:modelValue', option.value)
emit('change', option.value) // change
isOpen.value = false
}
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
isOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.custom-dropdown {
position: relative;
width: 100%;
height: 42px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-dropdown:hover {
border-color: #0288D1;
}
.custom-dropdown.open {
border-color: #0288D1;
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.1);
}
.dropdown-input {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 12px;
}
.input-content {
flex: 1;
font-size: 14px;
color: #333;
line-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.input-content:empty::before {
content: attr(data-placeholder);
color: #999;
}
.dropdown-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: #8C9191;
transition: transform 0.3s ease;
}
.dropdown-icon.rotated {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1.5px solid #D8D8D8;
border-radius: 0 0 2px 2px;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.dropdown-option {
padding: 8px 12px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
}
.dropdown-option:hover {
background: rgba(2, 136, 209, 0.1);
color: #0288D1;
}
.dropdown-option.first-option {
color: #0288D1;
font-weight: 500;
}
.dropdown-option.first-option:hover {
color: #0277BD;
background: rgba(2, 136, 209, 0.15);
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<div class="custom-pagination">
<!-- 上一页按钮 -->
<button
class="pagination-btn prev-btn"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2674 15.793C11.9675 16.0787 11.4927 16.0672 11.2071 15.7673L6.20572 10.5168C5.9298 10.2271 5.9298 9.7719 6.20572 9.48223L11.2071 4.23177C11.4927 3.93184 11.9675 3.92031 12.2674 4.206C12.5673 4.49169 12.5789 4.96642 12.2932 5.26634L7.78458 9.99952L12.2932 14.7327C12.5789 15.0326 12.5673 15.5074 12.2674 15.793Z" fill="currentColor"/>
</svg>
</button>
<!-- 页码按钮 -->
<button
class="pagination-btn page-btn active"
@click="handlePageChange(1)"
>
1
</button>
<!-- 下一页按钮 -->
<button
class="pagination-btn next-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(currentPage + 1)"
>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.73271 4.20694C8.03263 3.92125 8.50737 3.93279 8.79306 4.23271L13.7944 9.48318C14.0703 9.77285 14.0703 10.2281 13.7944 10.5178L8.79306 15.7682C8.50737 16.0681 8.03263 16.0797 7.73271 15.794C7.43279 15.5083 7.42125 15.0336 7.70694 14.7336L12.2155 10.0005L7.70694 5.26729C7.42125 4.96737 7.43279 4.49264 7.73271 4.20694Z" fill="currentColor"/>
</svg>
</button>
<!-- 分隔线 -->
<div class="separator"></div>
<!-- 每页条数选择器 -->
<div class="page-size-selector">
<span class="page-size-text">每页{{ pageSize }}</span>
</div>
<!-- 跳转输入框 -->
<div class="jump-to-section">
<span class="jump-text">跳至</span>
<input
type="number"
class="jump-input"
v-model="jumpPage"
min="1"
:max="totalPages"
@keyup.enter="handleJumpToPage"
@blur="handleJumpToPage"
/>
<span class="page-unit"></span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
interface Props {
currentPage: number
pageSize: number
total: number
pageSizes?: number[]
}
const props = withDefaults(defineProps<Props>(), {
pageSizes: () => [10, 20, 50]
})
const emit = defineEmits<{
'update:currentPage': [page: number]
'update:pageSize': [size: number]
}>()
const jumpPage = ref(props.currentPage)
//
const totalPages = computed(() => Math.ceil(props.total / props.pageSize))
//
watch(() => props.currentPage, (newPage) => {
jumpPage.value = newPage
})
//
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
emit('update:currentPage', page)
}
}
//
const handleJumpToPage = () => {
const page = parseInt(jumpPage.value.toString())
if (page >= 1 && page <= totalPages.value) {
emit('update:currentPage', page)
} else {
jumpPage.value = props.currentPage
}
}
</script>
<style scoped>
.custom-pagination {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.pagination-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fff;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.pagination-btn:hover:not(:disabled) {
border-color: #d0d0d0;
background: #f5f5f5;
color: #333;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
color: #ccc;
}
.pagination-btn.active {
background: #0288D1;
border-color: #0288D1;
color: #fff;
}
.pagination-btn.active:hover {
background: #0277BD;
border-color: #0277BD;
}
.separator {
width: 1px;
height: 20px;
background: #e0e0e0;
margin: 0 8px;
}
.page-size-selector {
display: flex;
align-items: center;
color: #666;
font-size: 14px;
}
.page-size-text {
white-space: nowrap;
}
.jump-to-section {
display: flex;
align-items: center;
gap: 4px;
color: #666;
font-size: 14px;
}
.jump-text {
white-space: nowrap;
}
.jump-input {
width: 60px;
height: 28px;
border: 1px solid #e0e0e0;
border-radius: 3px;
padding: 0 8px;
font-size: 14px;
text-align: center;
outline: none;
transition: border-color 0.3s ease;
}
.jump-input:focus {
border-color: #0288D1;
}
.page-unit {
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,589 @@
<template>
<n-modal :show="show" @update:show="handleUpdateShow" preset="card"
style="width: 90%; max-width: 1200px; max-height: 80vh;" :mask-closable="false" :closable="false">
<template #header>
<div class="modal-header">
<h2 class="modal-title">试卷库</h2>
</div>
</template>
<div class="modal-content">
<!-- 筛选和搜索区域 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-item">
<span class="filter-label">类型</span>
<n-select v-model:value="selectedType" :options="typeOptions" placeholder="全部" class="type-select" />
</div>
<div class="search-item">
<div class="custom-search-input">
<input v-model="searchKeyword" type="text" placeholder="请输入文档名称" class="search-input-field" />
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" />
</div>
</div>
<div class="info-text">
已全部加载,{{ totalCount }}份考试/练习
</div>
<n-button type="primary" class="import-btn">
<template #icon>
<n-icon>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</n-icon>
</template>
导入试卷
</n-button>
</div>
</div>
<!-- 试卷列表 -->
<div class="exam-list">
<div class="exam-grid">
<div v-for="exam in filteredExams" :key="exam.id" class="exam-card"
:class="{ 'selected': selectedExams.includes(exam.id) }">
<div class="card-checkbox">
<n-checkbox :checked="selectedExams.includes(exam.id)" @update:checked="toggleExamSelection(exam.id)" />
</div>
<div class="card-content">
<div class="title-section">
<n-tag :type="exam.status === '未开始' ? 'info' : 'success'" size="small" class="status-tag"
:data-status="exam.status">
{{ exam.status }}
</n-tag>
<h3 class="exam-title">{{ exam.title }}</h3>
</div>
<div class="exam-details">
<div class="detail-item">
<span class="detail-label">开考时间:</span>
<span class="detail-value">{{ exam.startTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">考试时长:</span>
<span class="detail-value">{{ exam.duration }}</span>
</div>
<div class="detail-item">
<span class="detail-label">考题数量:</span>
<span class="detail-value">{{ exam.questionCount }}</span>
</div>
</div>
<div class="card-footer">
<span class="view-details" @click="viewExamDetails(exam)">查看详情 ></span>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-section">
<CustomPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalCount"
:page-sizes="[10, 20, 50]"
/>
</div>
</div>
<template #footer>
<div class="modal-footer">
<n-button @click="handleCancel">取消</n-button>
<n-button type="primary" @click="handleConfirm" :disabled="selectedExams.length === 0">
确定
</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { NModal, NButton, NSelect, NCheckbox, NTag, NIcon } from 'naive-ui'
import CustomPagination from './CustomPagination.vue'
interface ExamPaper {
id: number
title: string
status: '未开始' | '进行中'
startTime: string
duration: string
questionCount: string
type: string
}
interface Props {
show: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:show': [value: boolean]
'confirm': [selectedExams: ExamPaper[]]
}>()
//
const selectedType = ref('全部')
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const selectedExams = ref<number[]>([])
//
const typeOptions = [
{ label: '全部', value: '全部' },
{ label: '考试', value: '考试' },
{ label: '练习', value: '练习' },
{ label: '测验', value: '测验' }
]
//
const examPapers = ref<ExamPaper[]>([
{
id: 1,
title: 'C++语言程序设计基础考试',
status: '未开始',
startTime: '2025.07.18 10:00',
duration: '120分钟',
questionCount: '100题',
type: '考试'
},
{
id: 2,
title: 'Java编程基础练习',
status: '进行中',
startTime: '2025.07.19 14:00',
duration: '90分钟',
questionCount: '80题',
type: '练习'
},
{
id: 3,
title: 'Python数据分析测验',
status: '未开始',
startTime: '2025.07.20 09:00',
duration: '60分钟',
questionCount: '50题',
type: '测验'
},
{
id: 4,
title: 'Web前端开发考试',
status: '未开始',
startTime: '2025.07.21 15:00',
duration: '150分钟',
questionCount: '120题',
type: '考试'
},
{
id: 5,
title: '数据库设计练习',
status: '进行中',
startTime: '2025.07.22 11:00',
duration: '100分钟',
questionCount: '70题',
type: '练习'
},
{
id: 6,
title: '算法与数据结构测验',
status: '未开始',
startTime: '2025.07.23 16:00',
duration: '75分钟',
questionCount: '60题',
type: '测验'
}
])
//
const filteredExams = computed(() => {
let filtered = examPapers.value
//
if (selectedType.value !== '全部') {
filtered = filtered.filter(exam => exam.type === selectedType.value)
}
//
if (searchKeyword.value) {
filtered = filtered.filter(exam =>
exam.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
return filtered
})
const totalCount = computed(() => filteredExams.value.length)
//
const handleUpdateShow = (value: boolean) => {
emit('update:show', value)
}
const toggleExamSelection = (examId: number) => {
const index = selectedExams.value.indexOf(examId)
if (index > -1) {
selectedExams.value.splice(index, 1)
} else {
selectedExams.value.push(examId)
}
}
const viewExamDetails = (exam: ExamPaper) => {
console.log('查看试卷详情:', exam)
//
}
const handleCancel = () => {
selectedExams.value = []
emit('update:show', false)
}
const handleConfirm = () => {
const selectedExamPapers = examPapers.value.filter(exam =>
selectedExams.value.includes(exam.id)
)
emit('confirm', selectedExamPapers)
emit('update:show', false)
}
//
watch(() => props.show, (newVal) => {
if (!newVal) {
selectedExams.value = []
searchKeyword.value = ''
selectedType.value = '全部'
currentPage.value = 1
}
})
</script>
<style scoped>
.modal-header {
display: flex;
align-items: center;
justify-content: left;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-title {
font-size: 18px;
padding-bottom: 10px;
font-weight: 500;
color: #000;
margin: 0;
}
.modal-content {
max-height: 60vh;
overflow-y: auto;
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
/* Webkit浏览器隐藏滚动条 */
.modal-content::-webkit-scrollbar {
display: none;
}
.filter-section {
margin-bottom: 20px;
}
.filter-row {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.type-select {
width: 180px;
}
.search-item {
flex: 1;
min-width: 200px;
}
.custom-search-input {
position: relative;
width: 180px;
}
.search-input-field {
width: 100%;
height: 34px;
padding: 8px 32px 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 3px;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
}
.search-input-field:focus {
border-color: #0288D1;
}
.search-input-field::placeholder {
color: #999;
}
.search-icon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
pointer-events: none;
}
.info-text {
font-size: 10px;
color: #999;
white-space: nowrap;
}
.import-btn {
white-space: nowrap;
}
.exam-list {
margin-bottom: 20px;
}
.exam-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 16px;
}
.exam-card {
transition: all 0.3s ease;
cursor: pointer;
display: flex;
align-items: center;
gap: 16px;
}
.card-checkbox {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.status-tag {
font-size: 12px;
font-weight: 500;
}
/* 状态标签基础样式 */
.status-tag :deep(.n-tag) {
border: none !important;
}
.status-tag :deep(.n-tag__border) {
display: none !important;
}
/* 未开始状态 */
.status-tag :deep(.n-tag) {
background-color: #E2F5FF !important;
}
.status-tag :deep(.n-tag__content) {
color: #0288D1 !important;
}
/* 进行中状态 - 使用更具体的选择器 */
.exam-card:has(.status-tag[data-status="进行中"]) .status-tag :deep(.n-tag),
.status-tag[data-status="进行中"] :deep(.n-tag) {
background-color: #D3FFE5 !important;
}
.exam-card:has(.status-tag[data-status="进行中"]) .status-tag :deep(.n-tag__content),
.status-tag[data-status="进行中"] :deep(.n-tag__content) {
color: #10D781 !important;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
border: 1.5px solid #D8D8D8;
padding: 15px 15px 15px 15px;
position: relative;
}
.title-section {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1.5px solid #D8D8D8;
padding-bottom: 10px;
}
.exam-title {
font-size: 16px;
font-weight: 600;
color: #333333;
margin: 0;
line-height: 1.4;
}
.exam-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
justify-content: flex-start;
}
.detail-label {
color: #999;
min-width: 70px;
font-weight: 500;
}
.detail-value {
color: #999;
font-weight: 400;
}
.card-footer {
position: absolute;
bottom: 15px;
right: 18px;
}
.view-details {
color: #0288D1;
font-size: 12px;
cursor: pointer;
transition: color 0.3s ease;
}
.view-details:hover {
color: #0277BD;
text-decoration: underline;
}
.pagination-section {
display: flex;
justify-content: flex-start;
padding-top: 20px;
}
/* 分页器样式 */
.pagination-section :deep(.n-pagination) {
--n-item-border-radius: 4px;
--n-item-font-size: 14px;
--n-item-height: 32px;
--n-item-padding: 0 12px;
--n-item-color: #fff;
--n-item-color-hover: #f5f5f5;
--n-item-color-pressed: #e8e8e8;
--n-item-color-active: #0288D1;
--n-item-text-color: #666;
--n-item-text-color-hover: #333;
--n-item-text-color-pressed: #333;
--n-item-text-color-active: #fff;
--n-item-border: 1px solid #e0e0e0;
--n-item-border-hover: 1px solid #d0d0d0;
--n-item-border-pressed: 1px solid #c0c0c0;
--n-item-border-active: 1px solid #0288D1;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-bottom: 20px;
}
/* 自定义按钮样式 */
.modal-footer :deep(.n-button) {
height: 32px !important;
border-radius: 3px;
}
/* 取消按钮样式 */
.modal-footer :deep(.n-button--default-type) {
background-color: #E2F5FF !important;
border-color: #0288D1 !important;
color: #0288D1 !important;
}
.modal-footer :deep(.n-button--default-type:hover) {
background-color: #D3E8F5 !important;
border-color: #0277BD !important;
color: #0277BD !important;
}
/* 确定按钮样式 */
.modal-footer :deep(.n-button--primary-type) {
background-color: #0288D1 !important;
border-color: #0288D1 !important;
color: #fff !important;
}
.modal-footer :deep(.n-button--primary-type:hover) {
background-color: #0277BD !important;
border-color: #0277BD !important;
color: #fff !important;
}
.modal-footer :deep(.n-button--primary-type:disabled) {
background-color: #0288D1 !important;
border-color: #0288D1 !important;
color: #fff !important;
opacity: 0.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.exam-grid {
grid-template-columns: 1fr;
}
.filter-item,
.search-item,
.info-text,
.import-btn {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,173 @@
<template>
<div class="homework-dropdown" @click="toggleDropdown">
<div class="dropdown-display">
<span class="dropdown-text">{{ selectedOption || placeholder }}</span>
<div class="dropdown-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
</div>
<div v-if="isOpen" class="dropdown-options">
<div
v-for="option in options"
:key="option.value"
class="dropdown-option"
:class="{ 'selected': selectedOption === option.value }"
@click.stop="selectOption(option)"
>
{{ option.label }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
interface Option {
label: string
value: string
}
interface Props {
modelValue?: string
options: Option[]
placeholder?: string
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请添加作业'
})
const emit = defineEmits<{
'update:modelValue': [value: string]
'change': [value: string]
}>()
const isOpen = ref(false)
const selectedOption = ref(props.modelValue)
//
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement
if (!target.closest('.homework-dropdown')) {
isOpen.value = false
}
}
// modelValue
watch(() => props.modelValue, (newValue) => {
selectedOption.value = newValue
})
// isOpen/
watch(isOpen, (newValue) => {
if (newValue) {
document.addEventListener('click', handleClickOutside)
} else {
document.removeEventListener('click', handleClickOutside)
}
})
const toggleDropdown = () => {
isOpen.value = !isOpen.value
}
const selectOption = (option: Option) => {
selectedOption.value = option.value
emit('update:modelValue', option.value)
emit('change', option.value)
isOpen.value = false
}
</script>
<style scoped>
.homework-dropdown {
position: relative;
width: 100%;
min-width: 200px;
}
.dropdown-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 3px;
background: #fff;
cursor: pointer;
transition: border-color 0.3s ease;
min-height: 34px;
}
.dropdown-display:hover {
border-color: #d0d0d0;
}
.dropdown-text {
color: #333;
font-size: 14px;
flex: 1;
}
.dropdown-text:empty::before {
content: attr(data-placeholder);
color: #999;
}
.dropdown-icon {
display: flex;
align-items: center;
color: #8C9191;
transition: transform 0.3s ease;
}
.dropdown-icon svg {
width: 16px;
height: 16px;
}
.homework-dropdown:has(.dropdown-options) .dropdown-icon {
transform: rotate(180deg);
}
.dropdown-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #e0e0e0;
border-top: none;
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.dropdown-option {
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.3s ease;
}
.dropdown-option:hover {
background-color: #f5f5f5;
}
.dropdown-option.selected {
background-color: #E2F5FF;
color: #0288D1;
}
.dropdown-option:first-child {
color: #0288D1;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,487 @@
<template>
<n-modal :show="show" @update:show="handleUpdateShow" preset="card"
style="width: 90%; max-width: 1000px; max-height: 80vh;" :mask-closable="false" :closable="false">
<template #header>
<div class="modal-header">
<h2 class="modal-title">作业库</h2>
</div>
</template>
<div class="modal-content">
<!-- 筛选和搜索区域 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-item">
<span class="filter-label">类型</span>
<n-select v-model:value="selectedType" :options="typeOptions" placeholder="全部" class="type-select" />
</div>
<div class="search-item">
<span class="search-label">搜索</span>
<div class="custom-search-input">
<input v-model="searchKeyword" type="text" placeholder="请输入文档名称" class="search-input-field" />
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" />
</div>
</div>
<div class="info-text">
已全部加载,{{ totalCount }}份作业
</div>
<n-button type="primary" class="import-btn">
<template #icon>
<n-icon>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</n-icon>
</template>
导入作业
</n-button>
</div>
</div>
<!-- 作业列表 -->
<div class="homework-list">
<div class="homework-grid">
<div v-for="homework in filteredHomework" :key="homework.id" class="homework-card"
:class="{ 'selected': selectedHomework.includes(homework.id) }">
<div class="card-checkbox">
<n-checkbox :checked="selectedHomework.includes(homework.id)" @update:checked="toggleHomeworkSelection(homework.id)" />
</div>
<div class="card-content">
<div class="title-row">
<h3 class="homework-title">{{ homework.title }}</h3>
<span class="view-details" @click="viewHomeworkDetails(homework)">查看详情 ></span>
</div>
<div class="homework-description">
{{ homework.description }}
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-section">
<CustomPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalCount"
:page-sizes="[10, 20, 50]"
/>
</div>
</div>
<template #footer>
<div class="modal-footer">
<n-button @click="handleCancel">取消</n-button>
<n-button type="primary" @click="handleConfirm" :disabled="selectedHomework.length === 0">
确定
</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { NModal, NButton, NSelect, NCheckbox, NIcon } from 'naive-ui'
import CustomPagination from './CustomPagination.vue'
interface Homework {
id: number
title: string
description: string
type: string
}
interface Props {
show: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:show': [value: boolean]
'confirm': [selectedHomework: Homework[]]
}>()
//
const selectedType = ref('全部')
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const selectedHomework = ref<number[]>([])
//
const typeOptions = [
{ label: '全部', value: '全部' },
{ label: '编程作业', value: '编程作业' },
{ label: '文档作业', value: '文档作业' },
{ label: '实验作业', value: '实验作业' }
]
//
const homeworkList = ref<Homework[]>([
{
id: 1,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '编程作业'
},
{
id: 2,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '实验作业'
},
{
id: 3,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '文档作业'
},
{
id: 4,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '编程作业'
},
{
id: 5,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '实验作业'
},
{
id: 6,
title: '作业标题作业标题作业标题作业标题',
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
type: '文档作业'
}
])
//
const filteredHomework = computed(() => {
let filtered = homeworkList.value
//
if (selectedType.value !== '全部') {
filtered = filtered.filter(homework => homework.type === selectedType.value)
}
//
if (searchKeyword.value) {
filtered = filtered.filter(homework =>
homework.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
return filtered
})
const totalCount = computed(() => filteredHomework.value.length)
//
const handleUpdateShow = (value: boolean) => {
emit('update:show', value)
}
const toggleHomeworkSelection = (homeworkId: number) => {
const index = selectedHomework.value.indexOf(homeworkId)
if (index > -1) {
selectedHomework.value.splice(index, 1)
} else {
selectedHomework.value.push(homeworkId)
}
}
const viewHomeworkDetails = (homework: Homework) => {
console.log('查看作业详情:', homework)
//
}
const handleCancel = () => {
selectedHomework.value = []
emit('update:show', false)
}
const handleConfirm = () => {
const selectedHomeworkItems = homeworkList.value.filter(homework =>
selectedHomework.value.includes(homework.id)
)
emit('confirm', selectedHomeworkItems)
emit('update:show', false)
}
//
watch(() => props.show, (newVal) => {
if (!newVal) {
selectedHomework.value = []
searchKeyword.value = ''
selectedType.value = '全部'
currentPage.value = 1
}
})
</script>
<style scoped>
.modal-header {
display: flex;
align-items: center;
justify-content: left;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-title {
font-size: 18px;
padding-bottom: 10px;
font-weight: 500;
color: #000;
margin: 0;
}
.modal-content {
max-height: 60vh;
overflow-y: auto;
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
/* Webkit浏览器隐藏滚动条 */
.modal-content::-webkit-scrollbar {
display: none;
}
.filter-section {
margin-bottom: 20px;
}
.filter-row {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.type-select {
width: 180px;
}
.search-item {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.search-label {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.custom-search-input {
position: relative;
width: 180px;
}
.search-input-field {
width: 100%;
height: 34px;
padding: 8px 32px 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 3px;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
}
.search-input-field:focus {
border-color: #0288D1;
}
.search-input-field::placeholder {
color: #999;
}
.search-icon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
pointer-events: none;
}
.info-text {
font-size: 10px;
color: #999;
white-space: nowrap;
}
.import-btn {
white-space: nowrap;
}
.homework-list {
margin-bottom: 20px;
}
.homework-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.homework-card {
transition: all 0.3s ease;
cursor: pointer;
display: flex;
align-items: center;
gap: 16px;
margin-left: 10px;
}
.card-checkbox {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
border: 1.5px solid #D8D8D8;
padding: 15px;
position: relative;
}
.title-row {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1.5px solid #D8D8D8;
padding-bottom: 12px;
}
.homework-title {
font-size: 16px;
font-weight: 500;
color: #000;
margin: 0;
line-height: 1.4;
flex: 1;
}
.homework-description {
font-size: 14px;
color: #666;
line-height: 1.5;
}
.view-details {
color: #0288D1;
font-size: 12px;
cursor: pointer;
transition: color 0.3s ease;
white-space: nowrap;
margin-left: 12px;
}
.view-details:hover {
color: #0277BD;
text-decoration: underline;
}
.pagination-section {
display: flex;
justify-content: flex-start;
padding-top: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-bottom: 20px;
}
/* 自定义按钮样式 */
.modal-footer :deep(.n-button) {
height: 32px !important;
border-radius: 3px;
}
/* 取消按钮样式 */
.modal-footer :deep(.n-button--default-type) {
background-color: #E2F5FF !important;
border-color: #0288D1 !important;
color: #0288D1 !important;
}
.modal-footer :deep(.n-button--default-type:hover) {
background-color: #D3E8F5 !important;
border-color: #0277BD !important;
color: #0277BD !important;
}
/* 确定按钮样式 */
.modal-footer :deep(.n-button--primary-type) {
background-color: #0288D1 !important;
border-color: #0288D1 !important;
color: #fff !important;
}
.modal-footer :deep(.n-button--primary-type:hover) {
background-color: #0277BD !important;
border-color: #0277BD !important;
color: #fff !important;
}
.modal-footer :deep(.n-button--primary-type:disabled) {
background-color: #0288D1 !important;
border-color: #0288D1 !important;
color: #fff !important;
opacity: 0.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.homework-grid {
grid-template-columns: 1fr;
}
.filter-item,
.search-item,
.info-text,
.import-btn {
width: 100%;
}
}
</style>

View File

@ -33,31 +33,20 @@
<ChevronDownOutline />
</n-icon>
</div>
<!-- 考试管理子菜单 -->
<div class="submenu-container" :class="{ expanded: examMenuExpanded }">
<router-link
to="/teacher/exam-management/question-management"
class="submenu-item"
<router-link to="/teacher/exam-management/question-management" class="submenu-item"
:class="{ active: activeSubNavItem === 'question-management' }"
@click="setActiveSubNavItem('question-management')"
>
@click="setActiveSubNavItem('question-management')">
<span>试题管理</span>
</router-link>
<router-link
to="/teacher/exam-management/exam-library"
class="submenu-item"
:class="{ active: activeSubNavItem === 'exam-library' }"
@click="setActiveSubNavItem('exam-library')"
>
<router-link to="/teacher/exam-management/exam-library" class="submenu-item"
:class="{ active: activeSubNavItem === 'exam-library' }" @click="setActiveSubNavItem('exam-library')">
<span>试卷管理</span>
</router-link>
<router-link
to="/teacher/exam-management/marking-center"
class="submenu-item"
:class="{ active: activeSubNavItem === 'marking-center' }"
@click="setActiveSubNavItem('marking-center')"
>
<router-link to="/teacher/exam-management/marking-center" class="submenu-item"
:class="{ active: activeSubNavItem === 'marking-center' }" @click="setActiveSubNavItem('marking-center')">
<span>阅卷中心</span>
</router-link>
</div>
@ -88,12 +77,14 @@
<!-- 面包屑 -->
<div class="breadcrumb">
<span class="breadcrumb-separator"></span>
<n-breadcrumb>
<n-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="index"
@click="handleBreadcrumbClick(item.path)" :class="{ 'clickable': index < breadcrumbItems.length - 1 }">
<div class="custom-breadcrumb">
<span v-for="(item, index) in breadcrumbItems" :key="index" class="breadcrumb-item"
:class="{ 'clickable': index < breadcrumbItems.length - 1, 'last-item': index === breadcrumbItems.length - 1 }"
@click="index < breadcrumbItems.length - 1 ? handleBreadcrumbClick(item.path) : null">
{{ item.title }}
</n-breadcrumb-item>
</n-breadcrumb>
<span v-if="index < breadcrumbItems.length - 1" class="breadcrumb-separator"> > </span>
</span>
</div>
</div>
<router-view></router-view>
</div>
@ -132,7 +123,7 @@ const setActiveNavItem = (index: number) => {
const toggleExamMenu = () => {
examMenuExpanded.value = !examMenuExpanded.value;
activeNavItem.value = 4;
//
if (examMenuExpanded.value && !activeSubNavItem.value) {
activeSubNavItem.value = 'question-management';
@ -155,8 +146,8 @@ const handleBreadcrumbClick = (path: string) => {
//
const hideSidebar = computed(() => {
const currentPath = route.path
//
return currentPath.includes('course-editor')
//
return currentPath.includes('course-editor') || currentPath.includes('chapter-editor-teacher')
})
//
@ -165,6 +156,10 @@ const breadcrumbItems = computed(() => {
//
if (currentPath.includes('course-editor')) {
// ID
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/);
const courseId = courseIdMatch ? courseIdMatch[1] : '未知';
let breadcrumbs = [
{
title: '课程管理',
@ -174,72 +169,300 @@ const breadcrumbItems = computed(() => {
//
if (currentPath.includes('courseware')) {
breadcrumbs.push({
title: '课件管理',
path: currentPath
});
breadcrumbs.push(
{
title: '课件管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '课件管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('question-bank')) {
breadcrumbs.push({
title: '题库管理',
path: currentPath
});
breadcrumbs.push(
{
title: '题库管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '题库管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('add-question')) {
breadcrumbs.push(
{
title: '题库管理',
path: currentPath.replace('/add-question', '/question-bank')
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
},
{
title: '新增试题',
path: currentPath
}
);
} else if (currentPath.includes('chapters')) {
breadcrumbs.push({
title: '章节管理',
path: currentPath
});
breadcrumbs.push(
{
title: '章节管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '章节管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('homework')) {
breadcrumbs.push({
title: '作业管理',
path: currentPath
});
breadcrumbs.push(
{
title: '作业管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '作业管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('practice')) {
breadcrumbs.push({
title: '练考通',
path: currentPath
});
breadcrumbs.push(
{
title: '练考通',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '练考通',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('certificate')) {
breadcrumbs.push({
title: '证书管理',
path: currentPath
});
breadcrumbs.push(
{
title: '证书管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '证书管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('discussion')) {
breadcrumbs.push({
title: '讨论管理',
path: currentPath
});
breadcrumbs.push(
{
title: '讨论管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '讨论管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('statistics')) {
breadcrumbs.push({
title: '统计管理',
path: currentPath
});
breadcrumbs.push(
{
title: '统计管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '统计管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('notification')) {
breadcrumbs.push({
title: '通知管理',
path: currentPath
});
breadcrumbs.push(
{
title: '通知管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '通知管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('management')) {
breadcrumbs.push({
title: '综合管理',
path: currentPath
});
breadcrumbs.push(
{
title: '综合管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
breadcrumbs.push(
{
title: '综合管理',
path: currentPath
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
}
);
}
console.log('课程编辑器页面面包屑:', breadcrumbs);
return breadcrumbs;
}
// >>>>
if (currentPath.includes('chapter-editor-teacher')) {
const courseId = route.params.courseId;
let breadcrumbs = [
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
},
{
title: '章节',
path: `/teacher/course-editor/${courseId}/chapters`
},
{
title: '章节名称',
path: currentPath
}
];
console.log('章节编辑页面面包屑:', breadcrumbs);
return breadcrumbs;
}
// >>>>
if (currentPath.includes('chapter-editor-teacher')) {
const courseId = route.params.courseId;
let breadcrumbs = [
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
},
{
title: '章节',
path: `/teacher/course-editor/${courseId}/chapters`
},
{
title: '章节名称',
path: currentPath
}
];
console.log('章节编辑页面面包屑:', breadcrumbs);
return breadcrumbs;
}
//
const matchedRoutes = route.matched;
let breadcrumbs = matchedRoutes
@ -512,7 +735,7 @@ const updateActiveNavItem = () => {
margin: 0 10px 15px;
}
.submenu-container{
.submenu-container {
width: 200px;
}
}
@ -594,37 +817,60 @@ const updateActiveNavItem = () => {
}
.breadcrumb-separator {
width: 4px;
height: 17px;
margin-right: 10px;
background-color: #0C99DA;
}
@media screen and (max-width: 480px) {
.nav-container .nav-item {
width: 150px;
height: 45px;
margin: 0 5px 10px;
}
.nav-container .nav-item img {
margin-left: 20px;
}
.submenu-container{
width: 150px;
}
@media screen and (max-width: 480px) {
.nav-container .nav-item {
width: 150px;
height: 45px;
margin: 0 5px 10px;
}
/* 可点击的面包屑项样式 */
.breadcrumb :deep(.n-breadcrumb-item.clickable) {
cursor: pointer;
color: #0C99DA;
.nav-container .nav-item img {
margin-left: 20px;
}
.submenu-container {
width: 150px;
}
}
.breadcrumb :deep(.n-breadcrumb-item.clickable:hover) {
color: #0277BD;
/* 面包屑样式 */
.custom-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 14px;
color: #666;
cursor: pointer;
transition: color 0.3s ease;
}
.breadcrumb-item.clickable:hover {
color: #0C99DA;
text-decoration: underline;
}
.breadcrumb-item.last-item {
color: #999;
cursor: default;
}
.breadcrumb-item.last-item:hover {
color: #999;
text-decoration: none;
}
.breadcrumb-separator {
color: #666;
margin: 0 4px;
font-weight: normal;
}
</style>

View File

@ -26,15 +26,16 @@
<span class="section-title">添加章节</span>
</n-button>
<div class="chapter-item flex-row">
<img class="chapter-arrow-icon" referrerpolicy="no-referrer" src="/images/teacher/路径18.png" />
<div class="chapter-item flex-row" @click="toggleChapterExpansion" :class="{ 'collapsed': !isChapterExpanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapterExpanded }" referrerpolicy="no-referrer"
:src="isChapterExpanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第一章&nbsp;课前准备</span>
<n-dropdown :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
<n-dropdown v-show="isChapterExpanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapterExpanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div class="chapter-content-item flex-row">
<div v-show="isChapterExpanded" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">1.开课彩蛋新开始</span>
<span class="content-description">第一节课程定位程定位与目标</span>
@ -45,14 +46,47 @@
</div>
</div>
<div class="chapter-item-container flex-row justify-between">
<img class="chapter-item-thumbnail" referrerpolicy="no-referrer" src="/images/teacher/切片_58.png" />
<span class="chapter-item-title">第二章&nbsp;课前准备</span>
<div class="chapter-item flex-row" @click="toggleChapter2Expansion"
:class="{ 'collapsed': !isChapter2Expanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapter2Expanded }" referrerpolicy="no-referrer"
:src="isChapter2Expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第二章&nbsp;课前准备</span>
<n-dropdown v-show="isChapter2Expanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapter2Expanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div class="chapter-item-container-next flex-row justify-between">
<img class="chapter-item-thumbnail-next" referrerpolicy="no-referrer" src="/images/teacher/切片_58.png" />
<span class="chapter-item-title-next">第三章&nbsp;课前准备</span>
<div v-show="isChapter2Expanded" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">2.课程导入基础知识</span>
<span class="content-description">第二节课程基础知识讲解</span>
</div>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
<span class="action-rename">重命名</span>
<span class="action-delete" @click="openDeleteModal">删除</span>
</div>
</div>
<div class="chapter-item flex-row" @click="toggleChapter3Expansion"
:class="{ 'collapsed': !isChapter3Expanded }">
<img class="chapter-arrow-icon" :class="{ 'rotated': !isChapter3Expanded }" referrerpolicy="no-referrer"
:src="isChapter3Expanded ? '/images/teacher/路径18.png' : '/images/teacher/collapse.png'" />
<span class="chapter-title">第三章&nbsp;课前准备</span>
<n-dropdown v-show="isChapter3Expanded" :options="chapterMenuOptions" @select="handleChapterMenuSelect">
<img class="chapter-options-icon" :class="{ 'transparent': !isChapter3Expanded }"
referrerpolicy="no-referrer" src="/images/teacher/分组76.png" />
</n-dropdown>
</div>
<div v-show="isChapter3Expanded" class="chapter-content-item flex-row">
<div class="content-text-group flex-col justify-between justify-between">
<span class="content-title">3.实践操作技能训练</span>
<span class="content-description">第三节实践操作技能训练</span>
</div>
<div class="action-menu flex-col" :class="{ 'visible': isMenuVisible }">
<span class="action-rename">重命名</span>
<span class="action-delete" @click="openDeleteModal">删除</span>
</div>
</div>
</div>
@ -98,12 +132,7 @@
<template #suffix>
<n-button quaternary size="small" @click="() => removeLessonSection(section.id)">
<template #icon>
<n-icon size="16">
<svg viewBox="0 0 24 24">
<path fill="currentColor"
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
</svg>
</n-icon>
<img src="/images/teacher/关闭-灰.png" alt="关闭" style="width: 10px; height: 10px; padding: 0;" />
</template>
</n-button>
</template>
@ -122,16 +151,37 @@
<div class="exam-section flex-row justify-end">
<span class="exam-label">添加考试/练习</span>
<div class="exam-dropdown-container">
<n-select v-model:value="section.selectedExamOption" class="exam-dropdown-display"
placeholder="请添加考试/练习" :options="examOptions" />
<CustomDropdown v-model="section.selectedExamOption" :options="examOptions" placeholder="请添加考试/练习"
@change="(value: any) => handleExamOptionChange(section, value)" />
<!-- 隐藏的文件输入框 -->
<input type="file" :id="`file-upload-${section.id}`" class="file-input"
accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.zip,.rar"
@change="handleFileUpload($event, section)" multiple ref="fileInputRef" />
</div>
</div>
<!-- 显示已上传的文件列表 -->
<div v-if="section.uploadedFiles && section.uploadedFiles.length > 0"
class="uploaded-files-section flex-row justify-end">
<span class="uploaded-files-label">已上传文件</span>
<div class="uploaded-files-container">
<div v-for="(file, fileIndex) in section.uploadedFiles" :key="fileIndex" class="file-item">
<span class="file-name">{{ file.name }}</span>
<button class="remove-file-btn" @click="removeFile(section, fileIndex)">
<img src="/images/teacher/关闭-灰.png" alt="删除" style="width: 12px; height: 12px;" />
</button>
</div>
</div>
</div>
<div class="homework-section flex-row justify-end">
<span class="homework-label">添加作业</span>
<div class="homework-input-container">
<n-select v-model:value="section.homeworkName" class="homework-input" placeholder="请添加作业"
:options="homeworkOptions" filterable tag />
<HomeworkDropdown v-model="section.homeworkName" :options="homeworkOptions" placeholder="请添加作业"
@change="(value: any) => handleHomeworkOptionChange(section, value)" />
<!-- 隐藏的文件输入框用于作业上传 -->
<input type="file" :id="`homework-file-upload-${section.id}`" style="display: none;" multiple
accept=".pdf,.doc,.docx,.txt" @change="(event: any) => handleHomeworkFileUpload(event, section)" />
</div>
</div>
</div>
@ -148,6 +198,12 @@
<n-modal v-model:show="showDeleteModal" preset="dialog" title="确认删除" content="确定要删除这个章节吗?" positive-text="确认"
negative-text="取消" @positive-click="handleDeleteConfirm" @negative-click="handleDeleteCancel" />
<!-- 试卷库模态框 -->
<ExamPaperLibraryModal v-model:show="showExamLibraryModal" @confirm="handleExamLibraryConfirm" />
<!-- 作业库模态框 -->
<HomeworkLibraryModal v-model:show="showHomeworkLibraryModal" @confirm="handleHomeworkLibraryConfirm" />
</div>
</n-config-provider>
@ -156,6 +212,10 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import type { GlobalThemeOverrides } from 'naive-ui';
import CustomDropdown from '@/components/CustomDropdown.vue';
import HomeworkDropdown from '@/components/HomeworkDropdown.vue';
import ExamPaperLibraryModal from '@/components/ExamPaperLibraryModal.vue';
import HomeworkLibraryModal from '@/components/HomeworkLibraryModal.vue';
//
const isMenuVisible = ref(false);
@ -163,8 +223,16 @@ const isMenuVisible = ref(false);
//
const showDeleteModal = ref(false);
//
const showExamLibraryModal = ref(false);
//
const showHomeworkLibraryModal = ref(false);
// /
const isChapterExpanded = ref(true);
const isChapter2Expanded = ref(false);
const isChapter3Expanded = ref(false);
//
const isMobile = ref(false);
@ -174,21 +242,36 @@ const sidebarCollapsed = ref(false);
//
const chapterName = ref('课前准备');
// section
interface Section {
id: number;
lessonName: string;
coursewareName: string;
selectedExamOption: string;
homeworkName: string;
uploadedFiles: File[];
homeworkFiles: File[];
}
//
const sections = ref([
const sections = ref<Section[]>([
{
id: 1,
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
selectedExamOption: '',
homeworkName: '请添加作业'
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
},
{
id: 2,
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
selectedExamOption: '',
homeworkName: '请添加作业'
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
}
]);
@ -205,10 +288,8 @@ const coursewareOptions = [
//
const homeworkOptions = [
{ label: '请添加作业', value: '请添加作业' },
{ label: '课后练习', value: '课后练习' },
{ label: '实践作业', value: '实践作业' },
{ label: '小组作业', value: '小组作业' }
{ label: '从作业库选择', value: '从作业库选择' },
{ label: '本地上传', value: '本地上传' }
];
//
@ -328,6 +409,14 @@ const toggleChapterExpansion = () => {
isChapterExpanded.value = !isChapterExpanded.value;
};
const toggleChapter2Expansion = () => {
isChapter2Expanded.value = !isChapter2Expanded.value;
};
const toggleChapter3Expansion = () => {
isChapter3Expanded.value = !isChapter3Expanded.value;
};
//
onMounted(() => {
checkScreenSize();
@ -356,12 +445,97 @@ const addSection = () => {
lessonName: '开课彩蛋新开始',
coursewareName: '课件准备PPT',
selectedExamOption: '',
homeworkName: '请添加作业'
homeworkName: '请添加作业',
uploadedFiles: [],
homeworkFiles: []
};
sections.value.push(newSection);
nextSectionId.value++;
};
//
const handleExamOptionChange = (section: Section, value: any) => {
console.log('选项变化:', value, 'section id:', section.id);
// ""
if (value === '本地上传') {
console.log('触发文件选择');
const fileInput = document.getElementById(`file-upload-${section.id}`) as HTMLInputElement;
if (fileInput) {
fileInput.click();
} else {
console.error('找不到文件输入框:', `file-upload-${section.id}`);
}
} else if (value === '从考试/练习选择') {
// "/"
console.log('准备显示试卷库模态框');
showExamLibraryModal.value = true;
console.log('showExamLibraryModal.value:', showExamLibraryModal.value);
}
};
//
const handleHomeworkOptionChange = (section: Section, value: any) => {
console.log('作业选项变化:', value, 'section id:', section.id);
// ""
if (value === '本地上传') {
console.log('触发作业文件选择');
const fileInput = document.getElementById(`homework-file-upload-${section.id}`) as HTMLInputElement;
if (fileInput) {
fileInput.click();
} else {
console.error('找不到作业文件输入框:', `homework-file-upload-${section.id}`);
}
} else if (value === '从作业库选择') {
// ""
console.log('准备显示作业库模态框');
showHomeworkLibraryModal.value = true;
console.log('showHomeworkLibraryModal.value:', showHomeworkLibraryModal.value);
showHomeworkLibraryModal.value = true;
console.log('showHomeworkLibraryModal.value:', showHomeworkLibraryModal.value);
}
};
//
const handleFileUpload = (event: Event, section: Section) => {
const target = event.target as HTMLInputElement;
if (target.files) {
const files = Array.from(target.files);
section.uploadedFiles = files;
console.log('文件已上传:', files);
}
};
//
const handleHomeworkFileUpload = (event: any, section: Section) => {
const target = event.target as HTMLInputElement;
if (target.files) {
const files = Array.from(target.files);
section.homeworkFiles = files;
console.log('作业文件已上传:', files);
}
};
//
const removeFile = (section: Section, fileIndex: number) => {
section.uploadedFiles.splice(fileIndex, 1);
};
//
const handleExamLibraryConfirm = (selectedExams: any[]) => {
console.log('选择的试卷:', selectedExams);
// section
//
showExamLibraryModal.value = false;
};
//
const handleHomeworkLibraryConfirm = (selectedHomework: any[]) => {
console.log('选择的作业:', selectedHomework);
// section
//
showHomeworkLibraryModal.value = false;
};
//
const removeLessonSection = (sectionId: number) => {
const index = sections.value.findIndex(section => section.id === sectionId);
@ -711,7 +885,6 @@ const removeLessonSection = (sectionId: number) => {
.block_3 {
width: 100%;
max-width: 1400px;
min-height: 100vh;
margin: 15px auto 0;
position: relative;
@ -831,16 +1004,35 @@ const removeLessonSection = (sectionId: number) => {
background: #F5F8FB 100% no-repeat;
background-size: 100% 100%;
margin: 23px 0 0 16px;
cursor: pointer;
transition: all 0.3s ease;
justify-content: space-around;
align-items: center;
}
.chapter-item.collapsed {
background: transparent;
}
.chapter-item:hover {
background: rgba(2, 136, 209, 0.05);
}
.chapter-arrow-icon {
margin-left: 10px;
width: 9px;
height: 11px;
margin: 23px 0 0 13px;
transform: rotate(180deg);
transition: transform 0.3s ease;
/* 旋转图标180度 */
}
.chapter-arrow-icon.rotated {
width: 11px;
height: 9px;
transform: rotate(0deg);
}
.chapter-title {
width: 131px;
height: 21px;
@ -852,13 +1044,27 @@ const removeLessonSection = (sectionId: number) => {
text-align: left;
white-space: nowrap;
line-height: 21px;
margin: 15px 0 0 11px;
transition: color 0.3s ease;
margin-right: 40px;
}
.chapter-item.collapsed .chapter-title {
color: rgba(51, 51, 51, 1);
}
.chapter-options-icon {
width: 5px;
width: 6px;
height: 20px;
margin: 17px 16px 0 55px;
margin-right: 5px;
transition: opacity 0.3s ease;
}
.chapter-options-icon.transparent {
opacity: 0 !important;
}
.chapter-item:hover .chapter-options-icon.transparent {
opacity: 0.8 !important;
}
.chapter-content-item {
@ -945,57 +1151,7 @@ const removeLessonSection = (sectionId: number) => {
margin: 14px 0 0 20px;
}
.chapter-item-container {
width: 151px;
height: 26px;
margin: 42px 0 0 28px;
gap: 10px;
}
.chapter-item-thumbnail {
width: 11px;
height: 9px;
margin-top: 6px;
}
.chapter-item-title {
width: 130px;
height: 26px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 21px;
}
.chapter-item-container-next {
width: 151px;
height: 26px;
margin: 42px 0 762px 28px;
gap: 10px;
}
.chapter-item-thumbnail-next {
width: 11px;
height: 9px;
margin-top: 6px;
}
.chapter-item-title-next {
width: 130px;
height: 26px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 18px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: left;
white-space: nowrap;
line-height: 21px;
}
.chapter-detail-container {
position: relative;
@ -1278,7 +1434,6 @@ const removeLessonSection = (sectionId: number) => {
text-align: right;
white-space: nowrap;
line-height: 18px;
margin: 11px 0 0 0;
flex-shrink: 0;
}
@ -1306,7 +1461,6 @@ const removeLessonSection = (sectionId: number) => {
text-align: right;
white-space: nowrap;
line-height: 18px;
margin: 10px 0 0 0;
flex-shrink: 0;
}
@ -1335,10 +1489,81 @@ const removeLessonSection = (sectionId: number) => {
text-align: right;
white-space: nowrap;
line-height: 18px;
margin: 10px 0 0 0;
flex-shrink: 0;
}
/* 文件上传相关样式 */
.file-input {
display: none;
}
.uploaded-files-section {
width: 100%;
max-width: 540px;
height: auto;
margin: 10px 0 0 20px;
display: flex;
justify-content: flex-end;
align-items: flex-start;
}
.uploaded-files-label {
width: 120px;
height: 18px;
overflow-wrap: break-word;
color: rgba(51, 51, 51, 1);
font-size: 16px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
font-weight: normal;
text-align: right;
white-space: nowrap;
line-height: 18px;
flex-shrink: 0;
margin-top: 12px;
}
.uploaded-files-container {
width: 400px;
margin: 1px 0 0 5px;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.file-name {
color: #333;
font-size: 14px;
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
flex: 1;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remove-file-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px;
border-radius: 2px;
transition: background-color 0.3s ease;
}
.remove-file-btn:hover {
background: rgba(255, 77, 79, 0.1);
}
/* 删除未使用的样式 */
@ -1365,7 +1590,6 @@ const removeLessonSection = (sectionId: number) => {
text-align: right;
white-space: nowrap;
line-height: 18px;
margin: 9px 0 0 0;
flex-shrink: 0;
}