2025-08-22 16:59:07 +08:00

787 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="chapter-management">
<!-- 顶部操作栏 -->
<div class="toolbar">
<h2>全部章节</h2>
<div class="toolbar-actions">
<button class="btn btn-primary" @click="addChapter">添加章节</button>
<button class="btn btn-new" @click="importChapters">导入</button>
<button class="btn btn-new" @click="exportChapters">导出</button>
<button class="btn btn-danger" @click="deleteSelected" :disabled="selectedChapters.length === 0">删除</button>
<div class="search-box">
<input type="text" placeholder="请输入想要搜索的内容" v-model="searchKeyword" />
<button class="btn btn-search" @click="searchChapters">搜索</button>
</div>
</div>
</div>
<!-- 章节列表表格 -->
<div class="table-box">
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
<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" />
</n-config-provider>
<!-- 自定义分页器 -->
<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>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import { NButton, useMessage, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
const message = useMessage()
// 章节类型定义
interface Chapter {
id: number
name: string
type: string
sort: string | number
createTime: string
isParent: boolean
children?: Chapter[]
expanded?: boolean
}
// 搜索关键词
const searchKeyword = ref('')
// 选中的章节
const selectedChapters = ref<number[]>([])
// 章节列表数据
const chapterList = ref<Chapter[]>([
{
id: 1,
name: '第一章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: [
{
id: 2,
name: '开课彩蛋:新开始新征程',
type: '视频',
sort: 1,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 3,
name: '课件准备PPT',
type: '课件',
sort: 2,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 4,
name: '第一节 课程定位与目标',
type: '视频',
sort: 3,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 5,
name: '第二节 教学安排及学习建议',
type: '作业',
sort: 4,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 6,
name: '第三节 教学安排及学习建议',
type: '考试',
sort: 5,
createTime: '2025.07.25 09:20',
isParent: false
}
]
},
{
id: 7,
name: '第二章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: [
{
id: 8,
name: '第一节 新开始新征程',
type: '视频',
sort: 1,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 9,
name: '第二节 教学安排及学习建议',
type: '课件',
sort: 2,
createTime: '2025.07.25 09:20',
isParent: false
}
]
},
{
id: 10,
name: '第三章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: [
{
id: 12,
name: '第一节 新开始新征程',
type: '视频',
sort: 1,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 13,
name: '第二节 教学安排及学习建议',
type: '课件',
sort: 2,
createTime: '2025.07.25 09:20',
isParent: false
}
]
},
{
id: 11,
name: '第四章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: []
}
])
// 扁平化章节列表(用于显示和分页)
const flattenedChapters = computed(() => {
const result: Chapter[] = []
const flatten = (chapters: Chapter[]) => {
chapters.forEach(chapter => {
result.push(chapter)
if (chapter.children && chapter.expanded) {
flatten(chapter.children)
}
})
}
flatten(chapterList.value)
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: number[]) => {
selectedChapters.value = rowKeys
}
// 行样式名称
const rowClassName = () => {
return 'chapter-table-row'
}
// 获取章节图标
// const getChapterIcon = (type: string) => {
// return '/images/teacher/章节.png'
// }
// 获取类型图标
// const getTypeIcon = (type: string) => {
// const iconMap: { [key: string]: string } = {
// '视频': '/images/teacher/视频.png',
// '课件': '/images/teacher/课件.png',
// '作业': '/images/teacher/作业.png',
// '考试': '/images/teacher/考试.png'
// }
// return iconMap[type] || '/images/teacher/默认.png'
// }
// 分页方法
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.isParent && chapter.children) {
chapter.expanded = !chapter.expanded
}
}
// 章节操作方法
const addChapter = () => {
message.info('添加章节功能')
}
const importChapters = () => {
message.info('导入章节功能')
}
const exportChapters = () => {
message.info('导出章节功能')
}
const deleteSelected = () => {
if (selectedChapters.value.length === 0) return
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
selectedChapters.value.forEach((id: number) => {
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
if (index > -1) {
chapterList.value.splice(index, 1)
}
})
selectedChapters.value = []
message.success('删除成功')
}
}
const searchChapters = () => {
message.info('搜索章节: ' + searchKeyword.value)
currentPage.value = 1
}
const editChapter = (chapter: Chapter) => {
message.info('编辑章节: ' + chapter.name)
}
const deleteChapter = (chapter: Chapter) => {
if (confirm('确定要删除这个章节吗?')) {
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
if (index > -1) {
chapterList.value.splice(index, 1)
message.success('删除成功')
}
}
}
// 表格列配置
const columns: DataTableColumns<Chapter> = [
{
type: 'selection'
},
{
title: '章节名称',
key: 'name',
minWidth: 350,
render: (row: Chapter) => {
return h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '20px',
cursor: row.isParent ? 'pointer' : 'default',
marginLeft: row.isParent ? '40px' : '12px'
},
onClick: row.isParent ? () => toggleChapter(row) : undefined
}, [
row.isParent ? h('i', {
class: 'n-base-icon',
style: {
transition: 'transform 0.2s',
transform: row.expanded ? 'rotate(90deg)' : 'rotate(0deg)',
cursor: 'pointer'
}
}, [
h('svg', {
viewBox: '0 0 16 16',
fill: 'none',
xmlns: 'http://www.w3.org/2000/svg'
}, [
h('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: 'currentColor'
})
])
]) : h('div', { style: { width: '16px' } }),
h('span', {
style: {
color: row.isParent ? '#062333' : '#666666',
fontSize: '14px',
fontWeight: row.isParent ? '500' : 'normal',
marginLeft: row.isParent ? '0' : '24px'
}
}, row.name)
])
}
},
{
title: '类型',
key: 'type',
width: 156,
render: (row: Chapter) => {
if (row.type === '-') {
return h('span', { style: { color: '#BABABA' } }, '-')
}
return h('div', {
style: {
display: 'inline-block',
padding: '4px 8px',
backgroundColor: 'transparent',
border: '1px solid #BABABA',
borderRadius: '4px',
fontSize: '12px',
color: '#BABABA'
}
}, row.type)
}
},
{
title: '排序',
key: 'sort',
width: 120,
render: (row: Chapter) => {
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
}
},
{
title: '创建时间',
key: 'createTime',
width: 235,
render: (row: Chapter) => {
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.createTime)
}
},
{
title: '操作',
key: 'actions',
width: 200,
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: () => '删除' })
])
}
}
]
</script>
<style scoped>
.chapter-management {
width: 1293px;
background: #fff;
height: 100%;
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;
}
.btn-danger:hover {
background: #ff7875;
}
.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;
border-radius: 8px;
padding: 40px;
height: 100%;
min-height: 500px;
position: relative;
}
/* 表格头部样式 */
:deep(.chapter-data-table .n-data-table-thead) {
background: #fafafa;
}
:deep(.chapter-data-table .n-data-table-th) {
background: #fafafa;
font-weight: 500;
color: #062333;
font-size: 14px;
border-bottom: 1px solid #e8e8e8;
padding: 12px 8px;
text-align: center;
}
/* 表格行样式 */
: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) {
color: #0088D1;
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;
}
.nav-button:hover:not(.disabled) {
color: #0088D1;
}
</style>