938 lines
25 KiB
Vue
938 lines
25 KiB
Vue
<template>
|
||
<div class="chapter-management">
|
||
<!-- 顶部操作栏 -->
|
||
<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>
|
||
</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" />
|
||
|
||
<!-- 自定义分页器 -->
|
||
<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>
|
||
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
|
||
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, h } from 'vue'
|
||
import { NButton, useMessage, NDataTable, NInput, NSpace, NIcon, useDialog } from 'naive-ui'
|
||
import type { DataTableColumns } from 'naive-ui'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { ChevronForwardOutline } from '@vicons/ionicons5'
|
||
import ImportModal from '@/components/common/ImportModal.vue'
|
||
import TeachCourseApi from '@/api/modules/teachCourse'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const dialog = useDialog()
|
||
|
||
// 获取当前课程ID
|
||
const courseId = ref(route.params.id as string)
|
||
|
||
const message = useMessage()
|
||
|
||
// 章节类型定义
|
||
interface Chapter {
|
||
id: string
|
||
name: string
|
||
type: string
|
||
sort: string | number
|
||
createTime?: string
|
||
isParent: boolean
|
||
children?: Chapter[]
|
||
expanded?: boolean
|
||
level?: number
|
||
parentId?: string
|
||
sortOrder?: number | null // 添加sortOrder字段用于排序
|
||
}
|
||
|
||
const showImportModal = ref(false)
|
||
|
||
// 加载状态
|
||
const loading = ref(false)
|
||
// const error = ref('')
|
||
|
||
const handleImportSuccess = () => {
|
||
message.success('章节导入成功')
|
||
// 重新加载章节列表
|
||
fetchCourseChapters()
|
||
}
|
||
|
||
const handleTemplateDownload = () => {
|
||
message.success('模板下载成功')
|
||
}
|
||
|
||
// 搜索关键词
|
||
const searchKeyword = ref('')
|
||
|
||
// 选中的章节
|
||
const selectedChapters = ref<string[]>([])
|
||
|
||
// 章节列表数据
|
||
const chapterList = ref<Chapter[]>([])
|
||
|
||
// 章节排序输入值(用于双向绑定)
|
||
const chapterSortValues = ref<Record<string, string>>({})
|
||
|
||
// 扁平化章节列表(用于显示和分页)
|
||
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)
|
||
|
||
// 对章节按sortOrder排序,null值排在最后
|
||
const sortedChapters = chapters.sort((a, b) => {
|
||
if (a.sortOrder === null && b.sortOrder === null) return 0
|
||
if (a.sortOrder === null) return 1
|
||
if (b.sortOrder === null) return -1
|
||
return (a.sortOrder || 0) - (b.sortOrder || 0)
|
||
})
|
||
|
||
// 为每个章节添加其子节
|
||
sortedChapters.forEach(chapter => {
|
||
const chapterSections = sections.filter(section => section.parentId === chapter.id)
|
||
|
||
// 对子节按sortOrder排序,null值排在最后
|
||
const sortedSections = chapterSections.sort((a, b) => {
|
||
if (a.sortOrder === null && b.sortOrder === null) return 0
|
||
if (a.sortOrder === null) return 1
|
||
if (b.sortOrder === null) return -1
|
||
return (a.sortOrder || 0) - (b.sortOrder || 0)
|
||
})
|
||
|
||
chapter.children = sortedSections
|
||
result.push(chapter)
|
||
|
||
// 如果章节展开,添加其子节
|
||
if (chapter.expanded && chapter.children) {
|
||
chapter.children.forEach(section => {
|
||
result.push(section)
|
||
})
|
||
}
|
||
})
|
||
|
||
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[]) => {
|
||
selectedChapters.value = rowKeys
|
||
}
|
||
|
||
// 行样式名称
|
||
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) {
|
||
chapter.expanded = !chapter.expanded
|
||
}
|
||
}
|
||
|
||
// 章节操作方法
|
||
const addChapter = () => {
|
||
// 跳转到当前课程下的章节编辑器,传递mode=add参数表示新增模式
|
||
router.push(`/teacher/chapter-editor-teacher/${courseId.value}?mode=add`)
|
||
}
|
||
|
||
const importChapters = () => {
|
||
showImportModal.value = true
|
||
}
|
||
|
||
const exportChapters = () => {
|
||
message.info('导出章节功能')
|
||
}
|
||
|
||
// const deleteSelected = async () => {
|
||
// if (selectedChapters.value.length === 0) return
|
||
// // 删除逻辑
|
||
// message.info('删除选中章节')
|
||
// }
|
||
|
||
const searchChapters = async () => {
|
||
// 搜索逻辑
|
||
message.info('搜索章节')
|
||
}
|
||
|
||
const editChapter = async (chapter: Chapter) => {
|
||
console.log('编辑章节:', chapter)
|
||
|
||
const courseId = route.params.id as string
|
||
if (!courseId) {
|
||
message.error('课程ID不存在')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 获取该章节下的所有小节数据
|
||
const sectionsResponse = await TeachCourseApi.getCourseSections(courseId)
|
||
const allSections = sectionsResponse.data.result || []
|
||
|
||
// 筛选出属于当前章节的小节
|
||
const chapterSections = allSections.filter((section: any) =>
|
||
section.level === 2 && section.parentId === chapter.id
|
||
)
|
||
|
||
// 构建完整的章节数据结构
|
||
const editChapterData = {
|
||
id: chapter.id,
|
||
name: chapter.name,
|
||
type: chapter.type,
|
||
level: chapter.level,
|
||
parentId: chapter.parentId,
|
||
sortOrder: chapter.sortOrder, // 添加章节的sortOrder
|
||
sections: chapterSections.map((section: any) => ({
|
||
id: section.id,
|
||
name: section.name,
|
||
type: section.type,
|
||
sortOrder: section.sortOrder,
|
||
parentId: section.parentId,
|
||
level: section.level,
|
||
createTime: section.createTime
|
||
}))
|
||
}
|
||
|
||
console.log('编辑章节完整数据:', editChapterData)
|
||
|
||
// 通过路由state传递数据,而不是URL参数
|
||
router.push({
|
||
path: `/teacher/chapter-editor-teacher/${courseId}`,
|
||
query: { mode: 'edit' },
|
||
state: { editChapterData }
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('获取章节数据失败:', error)
|
||
message.error('获取章节数据失败,请重试')
|
||
}
|
||
}
|
||
|
||
const deleteChapter = async (chapter: Chapter) => {
|
||
const isChapterLevel = chapter.level === 1; // 是否为章节(level=1)
|
||
const chapterName = chapter.name;
|
||
|
||
// 如果是章节,需要检查是否有下属小节
|
||
let confirmMessage = `确定要删除"${chapterName}"吗?`;
|
||
if (isChapterLevel) {
|
||
// 查找该章节下的所有小节
|
||
const childSections = chapterList.value.filter(item =>
|
||
item.level === 2 && item.parentId === chapter.id
|
||
);
|
||
|
||
if (childSections.length > 0) {
|
||
confirmMessage = `确定要删除章节"${chapterName}"吗?\n\n删除章节将同时删除其下的 ${childSections.length} 个小节:\n${childSections.map(s => s.name).join('、')}`;
|
||
}
|
||
}
|
||
|
||
// 二次确认弹窗
|
||
dialog.warning({
|
||
title: '删除确认',
|
||
content: confirmMessage,
|
||
positiveText: '确定删除',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
try {
|
||
loading.value = true;
|
||
|
||
if (isChapterLevel) {
|
||
// 如果是章节,需要先删除其下的所有小节,再删除章节本身
|
||
const childSections = chapterList.value.filter(item =>
|
||
item.level === 2 && item.parentId === chapter.id
|
||
);
|
||
|
||
// 先删除所有子小节
|
||
for (const section of childSections) {
|
||
console.log('删除小节:', section.name, 'ID:', section.id);
|
||
await TeachCourseApi.deleteCourseSection(section.id);
|
||
}
|
||
|
||
// 再删除章节本身
|
||
console.log('删除章节:', chapter.name, 'ID:', chapter.id);
|
||
await TeachCourseApi.deleteCourseSection(chapter.id);
|
||
|
||
message.success(`章节"${chapterName}"及其下属小节删除成功`);
|
||
} else {
|
||
// 如果是小节,直接删除
|
||
console.log('删除小节:', chapter.name, 'ID:', chapter.id);
|
||
await TeachCourseApi.deleteCourseSection(chapter.id);
|
||
|
||
message.success(`小节"${chapterName}"删除成功`);
|
||
}
|
||
|
||
// 重新加载章节列表
|
||
await fetchCourseChapters();
|
||
|
||
} catch (error: any) {
|
||
console.error('删除失败:', error);
|
||
message.error(`删除失败:${error.message || '未知错误'}`);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
},
|
||
onNegativeClick: () => {
|
||
message.info('已取消删除');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新章节排序
|
||
const updateChapterSort = async (chapter: Chapter, newSortOrder: number | null) => {
|
||
console.log('更新章节排序:', chapter.name, '新排序:', newSortOrder)
|
||
|
||
if (chapter.sortOrder === newSortOrder) {
|
||
// 排序没有变化,不需要更新
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 构建更新数据
|
||
const updateData = {
|
||
id: chapter.id,
|
||
courseId: courseId.value,
|
||
name: chapter.name,
|
||
type: chapter.type === '-' ? null : parseInt(chapter.type) || null,
|
||
sortOrder: newSortOrder,
|
||
parentId: chapter.parentId || null,
|
||
level: chapter.level || 1
|
||
}
|
||
|
||
console.log('发送章节排序更新请求:', updateData)
|
||
|
||
// 调用编辑接口更新排序
|
||
const response = await TeachCourseApi.editCourseSection(updateData)
|
||
|
||
if (response.data && (response.data.success === true || response.data.code === 200 || response.data.code === 0)) {
|
||
// 更新成功,更新本地数据
|
||
chapter.sortOrder = newSortOrder
|
||
chapterSortValues.value[chapter.id] = newSortOrder?.toString() || ''
|
||
message.success(`章节 "${chapter.name}" 排序更新成功`)
|
||
|
||
// 重新加载数据以确保排序正确
|
||
fetchCourseChapters()
|
||
} else {
|
||
console.error('章节排序更新失败:', response.data)
|
||
message.error('章节排序更新失败:' + (response.data?.message || '未知错误'))
|
||
}
|
||
} catch (error: any) {
|
||
console.error('更新章节排序失败:', error)
|
||
message.error('更新章节排序失败:' + (error.message || '网络错误'))
|
||
}
|
||
}
|
||
|
||
// 表格列配置 - 使用 minWidth 实现响应式
|
||
const columns: DataTableColumns<Chapter> = [
|
||
{
|
||
type: 'selection',
|
||
minWidth: 50
|
||
},
|
||
{
|
||
title: '章节名称',
|
||
key: 'name',
|
||
width: 400,
|
||
minWidth: 400,
|
||
ellipsis: {
|
||
tooltip: true
|
||
},
|
||
render: (row: Chapter) => {
|
||
const isChapter = row.level === 1; // level=1 表示章
|
||
return h('div', {
|
||
style: {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
cursor: (isChapter && row.children && row.children.length > 0) ? 'pointer' : 'default',
|
||
marginLeft: isChapter ? '0px' : '-3px'
|
||
},
|
||
onClick: (isChapter && row.children && row.children.length > 0) ? () => toggleChapter(row) : undefined
|
||
}, [
|
||
// 只有章节且有子节时才显示箭头
|
||
(isChapter && row.children && row.children.length > 0) ? h(NIcon, {
|
||
size: 14,
|
||
style: {
|
||
marginRight: '8px',
|
||
color: '#999',
|
||
transition: 'transform 0.2s',
|
||
transform: row.expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
||
cursor: 'pointer'
|
||
}
|
||
}, {
|
||
default: () => h(ChevronForwardOutline)
|
||
}) : (isChapter ? h('span', { style: { marginRight: '22px' } }) : null),
|
||
h('span', {
|
||
style: { color: '#062333', fontSize: '13px' }
|
||
}, row.name)
|
||
])
|
||
}
|
||
},
|
||
{
|
||
title: '类型',
|
||
key: 'type',
|
||
minWidth: 60,
|
||
render: (row: Chapter) => {
|
||
const isChapter = row.level === 1; // level=1 表示章
|
||
if (isChapter || row.type === '-') {
|
||
return h('span', { style: { color: '#BABABA' } }, '-')
|
||
}
|
||
return h('div', {
|
||
style: {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: '#062333',
|
||
fontSize: '12px'
|
||
}
|
||
}, row.type)
|
||
}
|
||
},
|
||
{
|
||
title: '排序',
|
||
key: 'sort',
|
||
minWidth: 100,
|
||
render: (row: Chapter) => {
|
||
const isChapter = row.level === 1; // level=1 表示章
|
||
if (isChapter) {
|
||
// 章节显示可编辑的排序输入框
|
||
return h('div', {
|
||
style: {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center'
|
||
}
|
||
}, [
|
||
h(NInput, {
|
||
value: chapterSortValues.value[row.id] ?? (row.sortOrder?.toString() || ''),
|
||
size: 'small',
|
||
style: { width: '60px', textAlign: 'center' },
|
||
placeholder: '排序',
|
||
'onUpdate:value': (value: string) => {
|
||
// 实时更新输入框的值
|
||
chapterSortValues.value[row.id] = value;
|
||
},
|
||
onBlur: () => {
|
||
const inputValue = chapterSortValues.value[row.id];
|
||
const newSortOrder = inputValue ? parseInt(inputValue) || null : null;
|
||
updateChapterSort(row, newSortOrder);
|
||
},
|
||
onKeydown: (e: KeyboardEvent) => {
|
||
if (e.key === 'Enter') {
|
||
const inputValue = chapterSortValues.value[row.id];
|
||
const newSortOrder = inputValue ? parseInt(inputValue) || null : null;
|
||
updateChapterSort(row, newSortOrder);
|
||
(e.target as HTMLInputElement).blur(); // 失去焦点
|
||
}
|
||
}
|
||
})
|
||
])
|
||
} else {
|
||
// 小节显示固定的排序值
|
||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
||
}
|
||
}
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
key: 'createTime',
|
||
minWidth: 180,
|
||
render: (row: Chapter) => {
|
||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.createTime)
|
||
}
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
minWidth: 160,
|
||
render: (row: Chapter) => {
|
||
const isChapter = row.level === 1; // level=1 表示章
|
||
|
||
if (isChapter) {
|
||
// 章节显示编辑和删除按钮
|
||
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: () => '删除' })
|
||
])
|
||
} else {
|
||
// 小节只显示删除按钮
|
||
return h('div', { style: { display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'center' } }, [
|
||
h(NButton, {
|
||
size: 'small',
|
||
type: 'error',
|
||
secondary: true,
|
||
onClick: () => deleteChapter(row)
|
||
}, { default: () => '删除' })
|
||
])
|
||
}
|
||
}
|
||
}
|
||
]
|
||
|
||
const fetchCourseChapters = () => {
|
||
loading.value = true
|
||
TeachCourseApi.getCourseSections(courseId.value).then(res => {
|
||
console.log('章节数据:', res.data)
|
||
// 将API返回的CourseSection数据映射为本地Chapter格式
|
||
const sections = res.data.result || []
|
||
chapterList.value = sections.map((section: any): Chapter => ({
|
||
id: section.id || '0',
|
||
name: section.name || '',
|
||
type: section.type?.toString() || '-',
|
||
sort: section.sortOrder?.toString() || '-', // 根据API数据使用sortOrder
|
||
createTime: section.createTime || '', // 根据API数据使用createTime
|
||
isParent: (section.level || 0) === 1,
|
||
expanded: false,
|
||
level: section.level || 0, // 添加level字段
|
||
parentId: section.parentId || '', // 根据API数据使用parentId
|
||
sortOrder: section.sortOrder || null, // 添加sortOrder字段用于排序功能
|
||
children: []
|
||
}))
|
||
console.log('处理后的章节数据:', chapterList.value)
|
||
|
||
// 初始化章节排序输入值
|
||
chapterSortValues.value = {}
|
||
chapterList.value.forEach(chapter => {
|
||
if (chapter.level === 1) {
|
||
chapterSortValues.value[chapter.id] = chapter.sortOrder?.toString() || ''
|
||
}
|
||
})
|
||
}).catch(error => {
|
||
console.error('获取章节数据失败:', error)
|
||
message.error('获取章节数据失败')
|
||
}).finally(() => {
|
||
loading.value = false
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchCourseChapters()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.chapter-management {
|
||
width: 100%;
|
||
background: #fff;
|
||
overflow: auto;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 顶部工具栏 */
|
||
.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;
|
||
}
|
||
|
||
/* 表格行样式 */
|
||
: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;
|
||
}
|
||
|
||
.nav-button:hover:not(.disabled) {
|
||
color: #0088D1;
|
||
}
|
||
</style> |