696 lines
16 KiB
Vue
Raw Normal View History

2025-08-21 19:39:07 +08:00
<template>
<div class="chapter-management">
2025-08-22 16:59:07 +08:00
<!-- 顶部操作栏 -->
<div class="toolbar">
<h2>全部章节</h2>
<div class="toolbar-actions">
<n-space>
<n-button type="primary" @click="addChapter">添加章节</n-button>
<n-button @click="importChapters">导入</n-button>
<n-button @click="exportChapters">导出</n-button>
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
<div class="search-container">
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" style="width: 200px;">
</n-input>
<n-button type="primary" @click="searchChapters">搜索</n-button>
</div>
</n-space>
2025-08-22 16:59:07 +08:00
</div>
</div>
<!-- 章节列表表格 -->
<div class="table-box">
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey" :checked-row-keys="selectedChapters"
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" :loading="loading" />
2025-08-22 16:59:07 +08:00
<!-- 自定义分页器 -->
<div class="custom-pagination">
<div class="pagination-content">
<div class="page-numbers">
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('first')">
首页
</span>
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('prev')">
上一页
</span>
<span v-for="page in visiblePages" :key="page" class="page-number page-number-bordered"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</span>
<span v-if="showRightEllipsis" class="page-number">...</span>
<span v-if="totalPages > 1" class="page-number page-number-bordered" @click="goToPage(totalPages)">
{{ totalPages }}
</span>
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
@click="goToPage('next')">
下一页
</span>
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
@click="goToPage('last')">
尾页
</span>
2025-08-22 16:59:07 +08:00
</div>
</div>
</div>
2025-08-21 19:39:07 +08:00
</div>
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
2025-08-21 19:39:07 +08:00
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, h } from 'vue'
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
2025-08-22 16:59:07 +08:00
import type { DataTableColumns } from 'naive-ui'
import { useRouter, useRoute } from 'vue-router'
import ImportModal from '@/components/common/ImportModal.vue'
import TeachCourseApi from '@/api/modules/teachCourse'
2025-08-22 19:52:05 +08:00
const router = useRouter()
const route = useRoute()
// 获取当前课程ID
const courseId = ref(route.params.id as string)
2025-08-21 19:39:07 +08:00
2025-08-22 16:59:07 +08:00
const message = useMessage()
// 章节类型定义
interface Chapter {
id: string
2025-08-22 16:59:07 +08:00
name: string
type: string
sort: string | number
createTime?: string
2025-08-22 16:59:07 +08:00
isParent: boolean
children?: Chapter[]
expanded?: boolean
level?: number
parentId?: string
2025-08-21 19:39:07 +08:00
}
const showImportModal = ref(false)
// 加载状态
const loading = ref(false)
// const error = ref('')
const handleImportSuccess = () => {
message.success('章节导入成功')
// 重新加载章节列表
fetchCourseChapters()
}
const handleTemplateDownload = () => {
message.success('模板下载成功')
}
2025-08-22 16:59:07 +08:00
// 搜索关键词
const searchKeyword = ref('')
// 选中的章节
const selectedChapters = ref<string[]>([])
2025-08-22 16:59:07 +08:00
// 章节列表数据
const chapterList = ref<Chapter[]>([])
2025-08-22 16:59:07 +08:00
// 扁平化章节列表(用于显示和分页)
const flattenedChapters = computed(() => {
const result: Chapter[] = []
// 分离章节level=1和节level=2过滤掉level=0的项目
const chapters = chapterList.value.filter(item => item.level === 1)
const sections = chapterList.value.filter(item => item.level === 2)
// 为每个章节添加其子节
chapters.forEach(chapter => {
chapter.children = sections.filter(section => section.parentId === chapter.id)
result.push(chapter)
// 如果章节展开,添加其子节
if (chapter.expanded && chapter.children) {
chapter.children.forEach(section => {
result.push(section)
})
}
})
2025-08-22 16:59:07 +08:00
return result
})
// 过滤后的章节列表
const filteredChapters = computed(() => {
if (!searchKeyword.value) {
return flattenedChapters.value
}
return flattenedChapters.value.filter((chapter: Chapter) =>
chapter.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// 分页相关状态
const currentPage = ref(1)
const pageSize = ref(10)
const totalPages = computed(() => Math.ceil(filteredChapters.value.length / pageSize.value))
// 可见的页码范围
const visiblePages = computed(() => {
const pages = []
const current = currentPage.value
const total = totalPages.value
if (total <= 7) {
// 如果总页数小于等于7显示所有页码
for (let i = 1; i <= total; i++) {
pages.push(i)
}
} else {
// 显示当前页附近的页码
const start = Math.max(1, current - 2)
const end = Math.min(total, current + 2)
for (let i = start; i <= end; i++) {
pages.push(i)
}
}
return pages
})
// 是否显示右侧省略号
const showRightEllipsis = computed(() => {
return currentPage.value < totalPages.value - 3
})
// 分页后的数据
const paginatedChapters = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredChapters.value.slice(start, end)
})
// 表格行键
const rowKey = (row: Chapter) => row.id
// 表格选择处理
const handleCheck = (rowKeys: string[]) => {
2025-08-22 16:59:07 +08:00
selectedChapters.value = rowKeys
2025-08-21 19:39:07 +08:00
}
2025-08-22 16:59:07 +08:00
// 行样式名称
const rowClassName = () => {
return 'chapter-table-row'
}
// 分页方法
const goToPage = (page: string | number) => {
if (typeof page === 'string') {
switch (page) {
case 'first':
currentPage.value = 1
break
case 'prev':
if (currentPage.value > 1) currentPage.value--
break
case 'next':
if (currentPage.value < totalPages.value) currentPage.value++
break
case 'last':
currentPage.value = totalPages.value
break
}
} else {
currentPage.value = page
}
}
// 展开/收起章节
const toggleChapter = (chapter: Chapter) => {
if (chapter.level === 1 && chapter.children) {
2025-08-22 16:59:07 +08:00
chapter.expanded = !chapter.expanded
}
}
// 章节操作方法
const addChapter = () => {
// 跳转到当前课程下的章节编辑器传递mode=add参数表示新增模式
router.push(`/teacher/chapter-editor-teacher/${courseId.value}?mode=add`)
2025-08-22 16:59:07 +08:00
}
const importChapters = () => {
showImportModal.value = true
2025-08-22 16:59:07 +08:00
}
const exportChapters = () => {
message.info('导出章节功能')
}
const deleteSelected = async () => {
2025-08-22 16:59:07 +08:00
if (selectedChapters.value.length === 0) return
// 删除逻辑
message.info('删除选中章节')
2025-08-22 16:59:07 +08:00
}
const searchChapters = async () => {
// 搜索逻辑
message.info('搜索章节')
2025-08-22 16:59:07 +08:00
}
const editChapter = (chapter: Chapter) => {
console.log('编辑章节:', chapter)
// 跳转到章节编辑器页面传递章节ID参数表示编辑模式
const courseId = route.params.id
if (courseId) {
router.push(`/teacher/chapter-editor-teacher/${courseId}?chapterId=${chapter.id}`)
} else {
message.error('课程ID不存在')
}
2025-08-22 16:59:07 +08:00
}
const deleteChapter = async (chapter: Chapter) => {
// 删除单个章节
message.info(`删除章节: ${chapter.name}`)
2025-08-22 16:59:07 +08:00
}
2025-08-23 19:20:14 +08:00
// 表格列配置 - 使用 minWidth 实现响应式
2025-08-22 16:59:07 +08:00
const columns: DataTableColumns<Chapter> = [
{
2025-08-23 19:20:14 +08:00
type: 'selection',
minWidth: 50
2025-08-22 16:59:07 +08:00
},
{
title: '章节名称',
key: 'name',
2025-08-23 19:20:14 +08:00
width: 400,
minWidth: 400,
ellipsis: {
tooltip: true
},
2025-08-22 16:59:07 +08:00
render: (row: Chapter) => {
const isChapter = row.level === 1; // level=1 表示章
2025-08-22 16:59:07 +08:00
return h('div', {
style: {
display: 'flex',
alignItems: 'center',
cursor: isChapter ? 'pointer' : 'default',
marginLeft: isChapter ? '0px' : '-3px'
2025-08-22 16:59:07 +08:00
},
onClick: isChapter ? () => toggleChapter(row) : undefined
2025-08-22 16:59:07 +08:00
}, [
isChapter ? h('i', {
2025-08-22 16:59:07 +08:00
style: {
marginRight: '8px',
fontSize: '12px',
color: '#999',
2025-08-22 16:59:07 +08:00
transition: 'transform 0.2s',
transform: row.expanded ? 'rotate(90deg)' : 'rotate(0deg)'
2025-08-22 16:59:07 +08:00
}
}, [
'▶'
2025-08-23 19:20:14 +08:00
]) : null,
2025-08-22 16:59:07 +08:00
h('span', {
style: { color: '#062333', fontSize: '13px' }
2025-08-22 16:59:07 +08:00
}, row.name)
])
}
},
{
title: '类型',
key: 'type',
2025-08-23 19:20:14 +08:00
minWidth: 60,
2025-08-22 16:59:07 +08:00
render: (row: Chapter) => {
const isChapter = row.level === 1; // level=1 表示章
if (isChapter || row.type === '-') {
2025-08-22 16:59:07 +08:00
return h('span', { style: { color: '#BABABA' } }, '-')
}
return h('div', {
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#062333',
fontSize: '12px'
2025-08-22 16:59:07 +08:00
}
}, row.type)
}
},
{
title: '排序',
key: 'sort',
2025-08-23 19:20:14 +08:00
minWidth: 50,
2025-08-22 16:59:07 +08:00
render: (row: Chapter) => {
const isChapter = row.level === 1; // level=1 表示章
if (isChapter) {
return h('span', { style: { color: '#BABABA' } }, '-')
}
2025-08-22 16:59:07 +08:00
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
}
},
{
title: '创建时间',
key: 'createTime',
2025-08-23 19:20:14 +08:00
minWidth: 180,
2025-08-22 16:59:07 +08:00
render: (row: Chapter) => {
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.createTime)
}
},
{
title: '操作',
key: 'actions',
2025-08-23 19:20:14 +08:00
minWidth: 160,
2025-08-22 16:59:07 +08:00
render: (row: Chapter) => {
return h('div', { style: { display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'center' } }, [
h(NButton, {
size: 'small',
type: 'info',
secondary: true,
onClick: () => editChapter(row)
}, { default: () => '编辑' }),
h(NButton, {
size: 'small',
type: 'error',
secondary: true,
onClick: () => deleteChapter(row)
}, { default: () => '删除' })
])
}
}
]
const fetchCourseChapters = () => {
TeachCourseApi.getCourseSections(courseId.value).then(res => {
console.log('章节数据:', res.data)
// 将API返回的CourseSection数据映射为本地Chapter格式
const sections = res.data.result || []
chapterList.value = sections.map((section): Chapter => ({
id: section.id || '0',
name: section.name || '',
type: section.type?.toString() || '-',
sort: section.sort_order?.toString() || '-',
isParent: (section.level || 0) === 1,
expanded: false,
children: []
}))
}).catch(error => {
console.error('获取章节数据失败:', error)
message.error('获取章节数据失败')
})
}
onMounted(() => {
fetchCourseChapters()
})
2025-08-22 16:59:07 +08:00
</script>
<style scoped>
.chapter-management {
width: 100%;
background: #fff;
overflow: auto;
}
/* 顶部工具栏 */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 25px;
background: #fff;
padding: 30px 0 20px 30px;
border-bottom: 2px solid #F6F6F6;
}
.toolbar h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.btn {
padding: 7px 16px;
border: none;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 2px;
}
.btn-primary {
background: #0288D1;
color: white;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-new {
background-color: #fff;
border: 1px solid #0288D1;
color: #0288D1;
}
.btn-default {
background: white;
color: #999999;
border: 1px solid #999999;
}
.btn-default:hover {
border-color: #1890ff;
color: #1890ff;
}
.btn-danger {
background: white;
color: #FF4D4F;
border: 1px solid #FF4D4F;
}
.search-box {
display: flex;
align-items: center;
border: 1px solid #F1F3F4;
border-radius: 2px;
overflow: hidden;
}
.search-box input {
border: none;
padding: 6px 12px;
outline: none;
width: 200px;
font-size: 14px;
}
.btn-search {
background: #0288D1;
color: white;
border: none;
padding: 6px 16px;
cursor: pointer;
font-size: 16px;
}
.btn-search:hover {
background: #0277bd;
}
.table-box {
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
/* Naive UI 表格样式定制 */
:deep(.chapter-data-table) {
background: #fff;
padding: 40px;
height: 100%;
min-height: 500px;
position: relative;
}
/* 表格头部样式 */
:deep(.chapter-data-table .n-data-table-thead) {
background: #f8f9fa;
}
:deep(.chapter-data-table .n-data-table-th) {
background: #f8f9fa !important;
color: #062333 !important;
font-weight: 600;
font-size: 13px;
border-bottom: 1px solid #e9ecef;
text-align: center;
2025-08-23 19:20:14 +08:00
}
/* 表格行样式 */
:deep(.chapter-data-table .n-data-table-td) {
font-size: 13px;
color: #062333;
border-bottom: 1px solid #f0f0f0;
padding: 12px 8px;
vertical-align: middle;
text-align: center;
}
/* 所有列居中对齐 */
:deep(.chapter-data-table .n-data-table-td) {
text-align: center !important;
}
:deep(.chapter-data-table .n-data-table-th) {
text-align: center !important;
}
:deep(.chapter-data-table .n-data-table-tr:hover) {
background: #fafafa;
}
/* 复选框样式 */
:deep(.chapter-data-table .n-checkbox) {
--n-size: 16px;
}
/* 隐藏Naive UI默认的展开触发器 */
:deep(.chapter-data-table .n-data-table-expand-trigger) {
display: none !important;
}
/* 隐藏Naive UI默认的展开占位符 */
:deep(.chapter-data-table .n-data-table-expand-placeholder) {
display: none !important;
}
/* 按钮组样式调整 */
:deep(.chapter-data-table .n-button) {
font-size: 12px;
height: 28px;
padding: 0 12px;
margin: 2px;
}
/* 操作按钮样式 - 只有边框和文字颜色,无背景色 */
:deep(.chapter-data-table .n-button--info-type.n-button--secondary) {
background-color: transparent !important;
border: 1px solid #0288D1;
color: #0288D1;
}
:deep(.chapter-data-table .n-button--info-type.n-button--secondary:hover) {
background-color: rgba(32, 128, 240, 0.05) !important;
border: 1px solid #0288D1;
color: #248DD3;
}
:deep(.chapter-data-table .n-button--error-type.n-button--secondary) {
background-color: transparent !important;
border: 1px solid #FF4D4F;
color: #FD8485;
}
:deep(.chapter-data-table .n-button--error-type.n-button--secondary:hover) {
background-color: rgba(208, 48, 80, 0.05) !important;
border: 1px solid #FF4D4F;
color: #FD8485;
}
/* 表格行样式 */
:deep(.chapter-table-row) {
transition: background-color 0.2s;
}
:deep(.chapter-table-row:hover) {
background-color: #f8f9fa;
}
/* 自定义分页器样式 */
.custom-pagination {
display: flex;
justify-content: center;
background: #fff;
padding: 20px 0;
margin-top: auto;
width: 100%;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.pagination-content {
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto;
padding: 0 10px;
}
.page-numbers {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
white-space: nowrap;
}
.page-number {
display: inline-block;
min-width: 38px;
height: 38px;
line-height: 38px;
text-align: center;
color: #333;
text-decoration: none;
font-size: 14px;
padding: 0 5px;
margin: 0 4px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.page-number-bordered {
border: 1px solid #d9d9d9;
}
.page-number.active {
background-color: #0088D1;
color: white;
border-color: #0088D1;
}
.page-number:hover:not(.disabled) {
border-color: #0088D1;
}
.page-number.disabled {
color: #ccc;
cursor: not-allowed;
}
.page-number.disabled:hover {
color: #ccc;
border-color: #d9d9d9;
}
.nav-button {
padding: 0 8px;
border: none;
2025-08-21 19:39:07 +08:00
}
2025-08-22 16:59:07 +08:00
.nav-button:hover:not(.disabled) {
color: #0088D1;
2025-08-21 19:39:07 +08:00
}
</style>