feat: 添加教师端证书首页;添加证书详情页;添加证书颁奖页面及一系列功能;添加新增证书页面;

This commit is contained in:
QDKF 2025-08-28 22:59:34 +08:00
parent 597b9a0d3f
commit dc912c3842
24 changed files with 3482 additions and 54 deletions

View File

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

View File

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 497 B

View File

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 396 B

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 779 B

View File

Before

Width:  |  Height:  |  Size: 962 B

After

Width:  |  Height:  |  Size: 962 B

View File

@ -0,0 +1,979 @@
<template>
<n-modal :show="show" @update:show="handleUpdateShow" preset="card"
style="width: 90%; max-width: 1200px; max-height: 85vh; min-height: 800px;" :mask-closable="false" :closable="false">
<template #header>
<div class="modal-header">
<!-- 步骤指示器 -->
<div class="step-indicator">
<div class="step-item" :class="{ active: currentStep === 1 }">
<div class="step-number">1</div>
<span class="step-text">选择考试/学习项目</span>
</div>
<div class="step-item" :class="{ active: currentStep === 2 }">
<div class="step-number">2</div>
<span class="step-text">设置颁发规则</span>
</div>
<div class="step-item" :class="{ active: currentStep === 3 }">
<div class="step-number">3</div>
<span class="step-text">颁发证书</span>
</div>
</div>
<!-- 关闭按钮 -->
<n-button text @click="handleClose" class="close-button">×</n-button>
</div>
</template>
<div class="modal-content">
<!-- 第一步选择考试/学习项目 -->
<div v-if="currentStep === 1" class="step-content">
<!-- 筛选区域 -->
<div class="filter-section">
<span class="label">使用类别:</span>
<n-select v-model:value="selectedCategory" :options="categoryOptions" placeholder="考试"
class="category-select" />
</div>
<!-- 表格区域 -->
<div class="table-section">
<n-data-table :columns="columns" :data="filteredExams" :row-key="(row) => row.id"
:checked-row-keys="selectedExams" @update:checked-row-keys="handleSelectionChange" :bordered="false"
:single-line="false" />
</div>
</div>
<!-- 第二步设置颁发规则 -->
<div v-if="currentStep === 2" class="step-content">
<div class="rules-section">
<div class="table-section">
<n-data-table :columns="rulesColumns" :data="selectedExamData" :row-key="(row) => row.id"
:bordered="false" :single-line="false" />
</div>
</div>
</div>
<!-- 第三步颁发证书 -->
<div v-if="currentStep === 3" class="step-content">
<div class="issuance-section">
<div class="table-section">
<n-data-table :columns="studentColumns" :data="studentList" :row-key="(row) => row.id"
:checked-row-keys="selectedStudents" @update:checked-row-keys="handleStudentSelectionChange" :bordered="false"
:single-line="false" />
</div>
</div>
</div>
</div>
<template #footer>
<div class="modal-footer">
<div class="footer-content" :class="{ 'no-pagination': currentStep !== 1 && currentStep !== 3 }">
<!-- 分页器区域 -->
<div v-if="currentStep === 1" class="pagination-section">
<n-pagination
v-model:page="pagination.page"
:page-count="pagination.pageCount"
:page-size="pagination.pageSize"
:show-size-picker="pagination.showSizePicker"
@update:page="pagination.onChange"
/>
</div>
<!-- 第三步的分页器和发证人数信息 -->
<div v-if="currentStep === 3" class="pagination-section">
<n-pagination
v-model:page="pagination.page"
:page-count="pagination.pageCount"
:page-size="pagination.pageSize"
:show-size-picker="pagination.showSizePicker"
@update:page="pagination.onChange"
/>
</div>
<!-- 发证人数信息单独显示在中间 -->
<div v-if="currentStep === 3" class="certificate-count-section">
<span class="certificate-count">本次发证人数<span class="certificate-number">{{ selectedStudents.length }}</span></span>
</div>
<div class="button-section">
<n-button v-if="currentStep > 1" class="previous-button" @click="handlePrevious">
上一步
</n-button>
<n-button type="primary" class="primary-button" @click="handleNext"
:disabled="currentStep === 1 && selectedExams.length === 0">
{{ currentStep === 3 ? '颁发证书' : '下一步' }}
</n-button>
</div>
</div>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, h } from 'vue'
import { NModal, NButton, NSelect, NDataTable, NPagination } from 'naive-ui'
interface Exam {
id: number
serialNo: number
examName: string
totalScore: number
startTime: string
endTime: string
creator: string
createTime: string
isExpired: string
}
interface Props {
show: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:show': [value: boolean]
'confirm': [selectedExams: Exam[]]
}>()
//
const currentStep = ref(1)
const selectedCategory = ref('考试')
const selectedExams = ref<number[]>([101, 102, 103, 104]) //
const selectedStudents = ref<number[]>([201, 202, 203, 204, 205, 207]) //
//
const categoryOptions = [
{ label: '考试', value: '考试' },
{ label: '学习项目', value: '学习项目' },
{ label: '练习', value: '练习' }
]
//
const examList = ref<Exam[]>([
{
id: 1,
serialNo: 1,
examName: '数据库系统概论期末考试',
totalScore: 100,
startTime: '2025.01.15 14:00',
endTime: '2025.01.15 16:00',
creator: '张教授',
createTime: '2025.01.10 10:30',
isExpired: '否'
},
{
id: 2,
serialNo: 2,
examName: '数据结构与算法期中考试',
totalScore: 80,
startTime: '2025.01.20 09:00',
endTime: '2025.01.20 11:00',
creator: '李老师',
createTime: '2025.01.12 15:45',
isExpired: '否'
},
{
id: 3,
serialNo: 3,
examName: '计算机网络基础测试',
totalScore: 60,
startTime: '2025.01.25 16:00',
endTime: '2025.01.25 17:00',
creator: '王教授',
createTime: '2025.01.15 08:20',
isExpired: '否'
},
{
id: 4,
serialNo: 4,
examName: '软件工程实践项目',
totalScore: 150,
startTime: '2025.02.01 00:00',
endTime: '2025.02.28 23:59',
creator: '陈老师',
createTime: '2025.01.20 14:15',
isExpired: '否'
},
{
id: 5,
serialNo: 5,
examName: '操作系统原理期末考试',
totalScore: 100,
startTime: '2025.02.05 14:30',
endTime: '2025.02.05 16:30',
creator: '刘教授',
createTime: '2025.01.25 11:00',
isExpired: '否'
},
{
id: 6,
serialNo: 6,
examName: 'Java程序设计基础练习',
totalScore: 50,
startTime: '2025.02.10 10:00',
endTime: '2025.02.10 11:00',
creator: '赵老师',
createTime: '2025.01.30 16:30',
isExpired: '否'
},
{
id: 7,
serialNo: 7,
examName: 'Web前端开发技术考试',
totalScore: 90,
startTime: '2025.02.15 15:00',
endTime: '2025.02.15 17:00',
creator: '孙老师',
createTime: '2025.02.05 09:45',
isExpired: '否'
},
{
id: 8,
serialNo: 8,
examName: '人工智能导论期中考试',
totalScore: 100,
startTime: '2025.02.20 09:30',
endTime: '2025.02.20 11:30',
creator: '周教授',
createTime: '2025.02.10 13:20',
isExpired: '否'
},
{
id: 9,
serialNo: 9,
examName: '机器学习算法实践',
totalScore: 120,
startTime: '2025.02.25 14:00',
endTime: '2025.02.25 16:00',
creator: '吴老师',
createTime: '2025.02.15 10:10',
isExpired: '否'
},
{
id: 10,
serialNo: 10,
examName: '数据库设计项目考核',
totalScore: 200,
startTime: '2025.03.01 00:00',
endTime: '2025.03.15 23:59',
creator: '郑老师',
createTime: '2025.02.20 15:30',
isExpired: '否'
},
{
id: 11,
serialNo: 11,
examName: '计算机组成原理考试',
totalScore: 100,
startTime: '2025.03.05 14:00',
endTime: '2025.03.05 16:00',
creator: '黄教授',
createTime: '2025.02.25 11:45',
isExpired: '否'
},
{
id: 12,
serialNo: 12,
examName: '软件测试技术练习',
totalScore: 60,
startTime: '2025.03.10 10:30',
endTime: '2025.03.10 11:30',
creator: '林老师',
createTime: '2025.03.01 14:20',
isExpired: '否'
},
{
id: 13,
serialNo: 13,
examName: '移动应用开发项目',
totalScore: 180,
startTime: '2025.03.15 00:00',
endTime: '2025.03.30 23:59',
creator: '马老师',
createTime: '2025.03.05 09:15',
isExpired: '否'
},
{
id: 14,
serialNo: 14,
examName: '云计算技术基础考试',
totalScore: 80,
startTime: '2025.03.20 15:30',
endTime: '2025.03.20 17:30',
creator: '徐教授',
createTime: '2025.03.10 16:40',
isExpired: '否'
},
{
id: 15,
serialNo: 15,
examName: '信息安全技术实践',
totalScore: 100,
startTime: '2025.03.25 09:00',
endTime: '2025.03.25 11:00',
creator: '朱老师',
createTime: '2025.03.15 13:50',
isExpired: '否'
}
])
//
const projectList = ref([
{
id: 101,
serialNo: 1,
examName: '第一章课前准备',
category: '1',
startTime: '2025.08.17 09:00',
endTime: '2025.08.17 09:00',
creator: '王建国',
createTime: '2025.08.20 09:20'
},
{
id: 102,
serialNo: 1,
examName: '第二章课前准备',
category: '2',
startTime: '2025.08.18 10:00',
endTime: '2025.08.18 12:00',
creator: '李老师',
createTime: '2025.08.20 09:20'
},
{
id: 103,
serialNo: 1,
examName: '第三章课前准备',
category: '3',
startTime: '2025.08.19 14:00',
endTime: '2025.08.19 16:00',
creator: '张教授',
createTime: '2025.08.20 09:20'
},
{
id: 104,
serialNo: 1,
examName: '第四章课前准备',
category: '4',
startTime: '2025.08.20 09:30',
endTime: '2025.08.20 11:30',
creator: '陈老师',
createTime: '2025.08.20 09:20'
}
])
//
const studentList = ref([
{
id: 201,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 202,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 203,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 204,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 205,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 206,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 207,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
},
{
id: 208,
serialNo: 1,
studentName: '王街道',
studentAccount: '565556622552',
referenceExam: '在数据库的三级模式结构中,内模式有__',
referenceTime: '2025.08.09 09:22',
examScore: 120
}
])
//
const studentColumns = [
{
type: 'selection' as const,
width: 50
},
{
title: '序号',
key: 'serialNo',
width: 60
},
{
title: '学员姓名',
key: 'studentName',
width: 120
},
{
title: '学员帐号',
key: 'studentAccount',
width: 150
},
{
title: '参考考试',
key: 'referenceExam',
width: 400
},
{
title: '参考时间',
key: 'referenceTime',
width: 140
},
{
title: '考试成绩',
key: 'examScore',
width: 100
}
]
//
const columns = computed(() => {
if (selectedCategory.value === '学习项目') {
return [
{
type: 'selection' as const,
width: 50
},
{
title: '序号',
key: 'serialNo',
width: 60
},
{
title: '学习项目名称',
key: 'examName',
width: 300
},
{
title: '分类',
key: 'category',
width: 100
},
{
title: '开始时间',
key: 'startTime',
width: 140
},
{
title: '结束时间',
key: 'endTime',
width: 140
},
{
title: '创建人',
key: 'creator',
width: 100
},
{
title: '创建时间',
key: 'createTime',
width: 140
}
]
} else {
return [
{
type: 'selection' as const,
width: 50
},
{
title: '序号',
key: 'serialNo',
width: 60
},
{
title: '考试名称',
key: 'examName',
width: 300
},
{
title: '总分',
key: 'totalScore',
width: 80
},
{
title: '开始时间',
key: 'startTime',
width: 140
},
{
title: '结束时间',
key: 'endTime',
width: 140
},
{
title: '创建人',
key: 'creator',
width: 100
},
{
title: '创建时间',
key: 'createTime',
width: 140
}
]
}
})
//
const rulesColumns = computed(() => {
if (selectedCategory.value === '学习项目') {
return [
{
type: 'selection' as const,
width: 50
},
{
title: '序号',
key: 'serialNo',
width: 60
},
{
title: '章节名称',
key: 'examName',
width: 300
},
{
title: '创建时间',
key: 'createTime',
width: 140
}
]
} else {
return [
{
type: 'selection' as const,
width: 50
},
{
title: '序号',
key: 'serialNo',
width: 60
},
{
title: '考试名称',
key: 'examName',
width: 300
},
{
title: '总分',
key: 'totalScore',
width: 80
},
{
title: '发放规则',
key: 'issuanceRules',
width: 200,
render: () => {
return h('div', { class: 'rules-input-container' }, [
h('input', {
class: 'rule-input',
placeholder: '最低分',
style: 'width: 80px; min-height: 32px; margin-right: 8px; border: 1px solid #E6E6E6; border-radius: 0; padding: 4px 6px; background-color: #FCFCFC;'
}),
h('span', '-'),
h('input', {
class: 'rule-input',
placeholder: '最高分',
style: 'width: 80px; min-height: 32px; margin-left: 8px; border: 1px solid #E6E6E6; border-radius: 0; padding: 4px 6px; background-color: #FCFCFC;'
})
])
}
}
]
}
})
//
const pagination = {
page: 1,
pageSize: 10,
showSizePicker: false,
pageCount: 43,
onChange: (page: number) => {
console.log('页码变化:', page)
}
}
//
const filteredExams = computed(() => {
if (selectedCategory.value === '学习项目') {
return projectList.value
} else {
return examList.value.filter((exam: Exam) => {
if (selectedCategory.value === '考试') {
return exam.examName.includes('考试')
} else if (selectedCategory.value === '练习') {
return exam.examName.includes('练习')
}
return true
})
}
})
//
const selectedExamData = computed(() => {
if (selectedCategory.value === '学习项目') {
return projectList.value.filter((project: any) =>
selectedExams.value.includes(project.id)
)
} else {
return examList.value.filter((exam: Exam) =>
selectedExams.value.includes(exam.id)
)
}
})
//
const handleUpdateShow = (value: boolean) => {
emit('update:show', value)
}
const handleClose = () => {
emit('update:show', false)
}
const handleSelectionChange = (keys: number[]) => {
selectedExams.value = keys
}
const handleStudentSelectionChange = (keys: number[]) => {
selectedStudents.value = keys
}
const handleNext = () => {
if (currentStep.value < 3) {
currentStep.value++
} else {
//
const selectedExamItems = examList.value.filter((exam: Exam) =>
selectedExams.value.includes(exam.id)
)
emit('confirm', selectedExamItems)
emit('update:show', false)
}
}
const handlePrevious = () => {
if (currentStep.value > 1) {
currentStep.value--
}
}
// const handleCancel = () => {
// selectedExams.value = []
// currentStep.value = 1
emit('update:show', false)
// }
//
watch(() => props.show, (newVal: boolean) => {
if (!newVal) {
selectedExams.value = []
currentStep.value = 1
selectedCategory.value = '考试'
}
})
</script>
<style scoped>
/* 模态框头部样式 */
.modal-header {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding-bottom: 20px;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-header .close-button {
position: absolute;
top: 0px;
right: 0px;
font-size: 34px;
font-weight: bold;
color: #999999;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
/* 步骤指示器样式 */
.step-indicator {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.step-item {
display: flex;
align-items: center;
color: #000;
font-size: 20px;
flex: 1;
justify-content: center;
position: relative;
}
.step-item .step-number {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
color: #999;
background-color: white;
font-size: 14px;
background-color: #EBEBEB;
}
.step-item.active {
color: #000;
}
.step-item.active .step-number {
background-color: #0288D1;
color: white;
border-color: #0288D1;
}
.step-indicator .step-item:not(:last-child)::after {
content: '';
position: absolute;
right: -50%;
transform: translateX(50%);
width: 100%;
height: 1px;
background-color: #D8D8D8;
z-index: -1;
}
/* 筛选区域 */
.filter-section {
display: flex;
align-items: center;
margin-bottom: 20px;
justify-content: flex-start;
}
.filter-section .label {
margin-right: 10px;
font-size: 14px;
color: #333;
}
.filter-section .n-select {
width: 150px;
}
/* 表格样式覆盖 */
:deep(.n-data-table) {
background: white;
border: 1px solid #F1F3F4;
}
:deep(.n-data-table .n-data-table-thead .n-data-table-tr) {
background: #FCFCFC;
}
:deep(.n-data-table .n-data-table-thead .n-data-table-th) {
background: #FCFCFC;
border-bottom: 1px solid #E6E6E6;
font-weight: 600;
color: #000;
padding: 8px 6px;
font-size: 14px;
text-align: center;
white-space: nowrap;
}
:deep(.n-data-table .n-data-table-tbody .n-data-table-td) {
border-bottom: 1px solid #E6E6E6;
padding: 8px 6px;
font-size: 12px;
color: #062333;
text-align: center;
}
:deep(.n-data-table .n-data-table-tbody .n-data-table-tr:hover) {
background: white;
}
/* 分页器区域样式 */
.pagination-section {
display: flex;
justify-content: flex-start;
align-items: center;
}
.certificate-count {
margin-left: 20px;
font-size: 14px;
color: #999;
}
/* 发证人数信息区域样式 */
.certificate-count-section {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.certificate-number {
color: #ED2626;
}
/* 分页样式 */
:deep(.n-pagination) {
justify-content: flex-start !important;
display: flex !important;
border: none !important;
}
:deep(.n-pagination .n-pagination-item) {
border: 1px solid #DBDBDB !important;
background: white !important;
color: #333 !important;
}
:deep(.n-pagination .n-pagination-item--button) {
border: 1px solid #DBDBDB !important;
background: white !important;
color: #333 !important;
}
:deep(.n-pagination .n-pagination-item--active) {
border: none !important;
background: #0288D1 !important;
color: white !important;
}
:deep(.n-pagination .n-pagination-item--disabled) {
border: 1px solid #DBDBDB !important;
background: white !important;
color: #999 !important;
}
/* 颁发规则输入框样式 */
.rules-input-container {
display: flex;
align-items: center;
justify-content: center;
}
.rule-input {
border: 1px solid #E6E6E6 !important;
border-radius: 0 !important;
padding: 4px 6px;
font-size: 12px;
background: #FCFCFC !important;
text-align: center;
color: #999999;
}
.rule-input::placeholder {
color: #999999;
font-size: 12px;
}
/* 底部按钮 */
.modal-footer {
margin-top: 20px;
position: relative;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* 当没有分页器时,按钮区域应该占满整个宽度并右对齐 */
.footer-content.no-pagination {
justify-content: flex-end;
}
.button-section {
display: flex;
justify-content: flex-end;
}
.button-section .n-button {
margin-left: 10px;
}
.modal-footer .n-button.primary-button {
background-color: #0288D1;
color: white;
border-radius: 3px;
padding: 0 20px;
height: 32px;
font-size: 14px;
}
.modal-footer .n-button.previous-button {
border: 1px solid #0288D1;
color: #0288D1;
background-color: #E2F5FF;
border-radius: 2px;
padding: 0 20px;
height: 32px;
font-size: 14px;
}
</style>

View File

@ -270,7 +270,7 @@ watch(() => props.show, (newVal) => {
}
.modal-title {
font-size: 18px;
font-size: 8px;
padding-bottom: 10px;
font-weight: 500;
color: #000;

View File

@ -219,12 +219,25 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/teacher/course/AddQuestion.vue'),
meta: { title: '新增试题' }
},
{
{
path: 'certificate',
name: 'CertificateManagement',
component: CertificateManagement,
meta: { title: '证书管理' }
},
{
path: 'certificate/detail/:id',
name: 'CertificateDetail',
component: () => import('@/views/teacher/certificate/CertificateDetail.vue'),
meta: { title: '证书详情' }
},
{
path: 'certificate/add',
name: 'CertificateCreate',
component: () => import('@/views/teacher/certificate/CertificateCreate.vue'),
meta: { title: '添加证书' }
},
{
path: 'discussion',
name: 'DiscussionManagement',
@ -263,6 +276,12 @@ const routes: RouteRecordRaw[] = [
component: StudentManagement,
meta: { title: '学员管理' }
},
{
path: 'certificate/new',
name: 'CertificateNew',
component: () => import('@/views/teacher/certificate/CertificateNew.vue'),
meta: { title: '新建证书' }
},
{
path: 'chapter-editor-teacher/:courseId',
name: 'ChapterEditor',
@ -335,39 +354,7 @@ const routes: RouteRecordRaw[] = [
}
]
},
{
path: 'certificate',
name: 'CertificateModule',
component: () => import('@/views/teacher/certificate/index.vue'),
meta: { title: '证书管理' },
redirect: '/teacher/certificate/management',
children: [
{
path: 'management',
name: 'CertificateManagementPage',
component: CertificateManagement,
meta: { title: '证书管理' }
},
{
path: 'template',
name: 'CertificateTemplate',
component: () => import('@/views/teacher/certificate/CertificateTemplate.vue'),
meta: { title: '证书模板' }
},
{
path: 'issuance',
name: 'CertificateIssuance',
component: () => import('@/views/teacher/certificate/CertificateIssuance.vue'),
meta: { title: '证书颁发' }
},
{
path: 'query',
name: 'CertificateQuery',
component: () => import('@/views/teacher/certificate/CertificateQuery.vue'),
meta: { title: '证书查询' }
}
]
},
]
},

View File

@ -75,7 +75,7 @@
<!-- 右侧路由视图 -->
<div class="router-view-container" :class="{ 'full-width': hideSidebar }">
<!-- 面包屑 -->
<div class="breadcrumb">
<div class="breadcrumb" v-if="breadcrumbDisplay">
<span class="breadcrumb-side"></span>
<div class="custom-breadcrumb">
<!-- 左侧课程管理 -->
@ -117,6 +117,14 @@ const examMenuExpanded = ref(false); // 考试管理菜单展开状态
const showTopImage = ref(true); // /
const route = useRoute();
const router = useRouter();
const breadcrumbDisplay = computed(() => {
const currentPath = route.path;
//
if (currentPath.includes('certificate/new')) {
return false;
}
return true;
});
const setActiveNavItem = (index: number) => {
activeNavItem.value = index;
@ -161,20 +169,82 @@ const handleClose = () => {
const hideSidebar = computed(() => {
const currentPath = route.path
//
return currentPath.includes('course-editor') || currentPath.includes('chapter-editor-teacher')
return currentPath.includes('course-editor') || currentPath.includes('chapter-editor-teacher') || currentPath.includes('certificate')
})
//
const breadcrumbPathItems = computed(() => {
const currentPath = route.path;
//
if (currentPath.includes('certificate')) {
console.log('证书页面路径:', currentPath);
let breadcrumbs: Array<{ title: string, path: string }> = [];
// IDcourse-editor
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/);
const courseId = courseIdMatch ? courseIdMatch[1] : '1';
if (currentPath.includes('/certificate/detail/')) {
console.log('匹配到证书详情页面');
// > >
breadcrumbs = [
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: '证书管理',
path: `/teacher/course-editor/${courseId}/certificate`
},
{
title: '证书详情',
path: currentPath
}
];
} else if (currentPath.includes('/certificate/add')) {
console.log('匹配到添加证书页面');
// > >
breadcrumbs = [
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: '证书管理',
path: `/teacher/course-editor/${courseId}/certificate`
},
{
title: '添加证书',
path: currentPath
}
];
} else if (currentPath.endsWith('/certificate')) {
console.log('匹配到证书管理页面');
// >
breadcrumbs = [
{
title: '课程管理',
path: '/teacher/course-management'
},
{
title: '证书',
path: `/teacher/course-editor/${courseId}/certificate`
}
];
} else {
console.log('证书页面但未匹配到具体条件');
}
console.log('证书页面面包屑:', breadcrumbs);
return breadcrumbs;
}
//
if (currentPath.includes('course-editor')) {
// ID
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/);
const courseId = courseIdMatch ? courseIdMatch[1] : '未知';
let breadcrumbs = [];
let breadcrumbs: Array<{ title: string, path: string }> = [];
//
if (currentPath.includes('courseware')) {
@ -267,7 +337,7 @@ const breadcrumbPathItems = computed(() => {
}
);
} else {
//
//
breadcrumbs.push(
{
title: '作业管理',
@ -303,12 +373,16 @@ const breadcrumbPathItems = computed(() => {
} else if (currentPath.includes('certificate')) {
breadcrumbs.push(
{
title: '证书管理',
path: currentPath
title: '课程管理',
path: '/teacher/course-management'
},
{
title: `课程${courseId}`,
path: `/teacher/course-editor/${courseId}`
title: '证书管理',
path: '/teacher/course-editor/1/certificate'
},
{
title: '证书名称详情',
path: currentPath
}
);
} else if (currentPath.includes('discussion')) {
@ -391,6 +465,8 @@ const breadcrumbPathItems = computed(() => {
return breadcrumbs;
}
//
const matchedRoutes = route.matched;
let breadcrumbs = matchedRoutes
@ -429,7 +505,7 @@ const updateActiveNavItem = () => {
} else if (path.includes('exam-management')) {
activeNavItem.value = 4; //
examMenuExpanded.value = true;
const arr = ['question-management', 'exam-library', 'marking-center'];
const found = arr.find(item => path.includes(item));
activeSubNavItem.value = found || '';

View File

@ -0,0 +1,520 @@
<template>
<div class="certificate-create">
<!-- 主要内容 -->
<div class="content">
<!-- 创建证书区域 -->
<div class="create-certificate-section">
<h3 class="section-title">创建证书</h3>
<div class="upload-area" @click="showModal = true">
<div class="upload-placeholder">
<img src="/images/teacher/添加-灰.png" alt="添加" class="upload-icon" />
<span class="upload-text">点击上传证书</span>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="divider">
<span class="divider-text">证书模板</span>
</div>
<!-- 通用证书区域 -->
<div class="general-certificate-section">
<h3 class="section-title">通用证书</h3>
<div class="certificate-templates">
<div class="certificate-template">
<img src="/images/teacher/certificate.png" alt="证书模板" class="template-image" />
<span class="template-label">证书模板</span>
</div>
<div class="certificate-template">
<img src="/images/teacher/certificate.png" alt="证书模板" class="template-image" />
<span class="template-label">证书模板</span>
</div>
<div class="certificate-template">
<img src="/images/teacher/certificate.png" alt="证书模板" class="template-image" />
<span class="template-label">证书模板</span>
</div>
<div class="certificate-template">
<img src="/images/teacher/certificate.png" alt="证书模板" class="template-image" />
<span class="template-label">证书模板</span>
</div>
</div>
</div>
</div>
<!-- 选择证书尺寸模态框 -->
<div v-if="showModal" class="modal-overlay" @click="showModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3 class="modal-title">选择证书尺寸</h3>
</div>
<div class="modal-body">
<!-- 方向选择 -->
<div class="section">
<h4 class="section-title">方向</h4>
<div class="orientation-options">
<div class="orientation-option" :class="{ active: selectedOrientation === 'landscape' }"
@click="selectedOrientation = 'landscape'">
<img src="/images/teacher/horizontal-version.png" alt="横版" class="orientation-icon" />
<div class="orientation-text">
<div>A4 (横版)</div>
<div class="dimensions">30cm*21cm</div>
</div>
</div>
<div class="orientation-option" :class="{ active: selectedOrientation === 'portrait' }"
@click="selectedOrientation = 'portrait'">
<img src="/images/teacher/vertical-version.png" alt="竖版" class="orientation-icon" />
<div class="orientation-text">
<div>A4 (竖版)</div>
<div class="dimensions">21cm*30cm</div>
</div>
</div>
</div>
</div>
<!-- 设置证书有效期 -->
<div class="section">
<h4 class="section-title">设置证书有效期:</h4>
<div class="validity-options">
<label class="validity-option">
<input type="radio" v-model="validityType" value="duration" class="radio-input" />
<span class="radio-label">自颁发日起</span>
<div class="duration-input">
<input type="number" v-model="validityDuration" class="number-input" />
<span class="duration-text">月内有效<span> (设置0则代表证书发出终生有效)</span></span>
</div>
</label>
<label class="validity-option">
<input type="radio" v-model="validityType" value="date" class="radio-input" />
<span class="radio-label">有效期至</span>
<div class="date-input">
<input type="datetime-local" v-model="validityDate" class="date-field" />
</div>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-cancel" @click="showModal = false">取消</button>
<button class="btn btn-next" @click="handleNext">下一步</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// const message = useMessage()
//
const showModal = ref(false)
//
// const formData = reactive({
// template: ''
// })
//
const selectedOrientation = ref('landscape')
//
const validityType = ref('duration')
const validityDuration = ref(60)
const validityDate = ref('')
//
const handleNext = () => {
console.log('选择的配置:', {
orientation: selectedOrientation.value,
validityType: validityType.value,
validityDuration: validityDuration.value,
validityDate: validityDate.value
})
//
const queryParams = {
orientation: selectedOrientation.value,
validityType: validityType.value,
validityDuration: validityDuration.value.toString(),
validityDate: validityDate.value
}
//
showModal.value = false
//
const currentPath = window.location.pathname
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/)
const courseId = courseIdMatch ? courseIdMatch[1] : '1'
router.push({
path: '/teacher/certificate/new',
query: {
...queryParams,
courseId: courseId
}
})
}
</script>
<style scoped>
.certificate-create {
min-height: 100vh;
background: #fff;
padding: 30px;
}
/* 主要内容 */
.content {
margin: 0 auto;
}
.section-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0 0 20px 0;
}
/* 创建证书区域 */
.create-certificate-section {
background: #fff;
padding: 0;
}
.upload-area {
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
width: 160px;
height: 160px;
background: #F7F8FA;
display: flex;
justify-content: center;
align-items: center;
}
.upload-area:hover {
border-color: #0288D1;
background: #f0f8ff;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.upload-icon {
width: 22px;
height: 22px;
object-fit: contain;
}
.upload-text {
font-size: 14px;
color: #333;
}
/* 分割线 */
.divider {
position: relative;
margin: 20px 0;
text-align: center;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #E6E6E6;
}
.divider-text {
background: #fff;
padding: 0 15px;
color: #666;
font-size: 16px;
position: relative;
z-index: 1;
}
/* 通用证书区域 */
.general-certificate-section {
background: #fff;
padding: 0;
}
.certificate-templates {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.certificate-template {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
min-width: 120px;
}
.template-image {
width: 213px;
height: 160px;
object-fit: contain;
cursor: pointer;
transition: all 0.3s ease;
}
.template-label {
font-size: 18px;
color: #333;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
padding: 20px;
background: #fff;
border-radius: 2px;
width: 90%;
max-width: 700px;
max-height: 80vh;
overflow: hidden;
min-height: 600px;
display: flex;
flex-direction: column;
}
.modal-header {
padding-bottom: 20px;
border-bottom: 1.5px solid #E6E6E6;
}
.modal-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0;
}
.modal-body {
padding-top: 20px;
flex: 1;
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
background: #fff;
}
/* 方向选择 */
.section {
margin-bottom: 24px;
}
.section h4 {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0 0 16px 0;
}
.orientation-options {
display: flex;
gap: 16px;
}
.orientation-option {
width: 160px;
height: 160px;
background: #F7F8FA;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.orientation-option:hover {
border-color: #0288D1;
}
.orientation-option.active {
border-color: #0288D1;
background: #f0f8ff;
border: 1px solid #0288D1;
}
.orientation-icon {
width: 34px;
height: 38px;
margin: 0 auto;
object-fit: contain;
}
.orientation-text {
margin-top: 6px;
font-size: 14px;
color: #333;
}
.dimensions {
font-size: 10px;
color: #666;
}
/* 有效期设置 */
.validity-options {
display: flex;
flex-direction: column;
gap: 16px;
}
.validity-option {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.radio-input {
width: 16px;
height: 16px;
accent-color: #0288D1;
}
.radio-label {
font-size: 14px;
color: #333;
min-width: 80px;
}
.duration-input {
display: flex;
align-items: center;
gap: 8px;
}
.number-input {
width: 60px;
padding: 8px 8px;
border: 1px solid #E6E6E6;
background-color: #FCFCFC;
font-size: 14px;
color: #666666;
}
.duration-text {
font-size: 14px;
color: #333;
}
.duration-text span {
margin-left: 8px;
color: #999999;
}
.date-input {
flex: 1;
}
.date-field {
padding: 8px 8px;
border: 1px solid #E6E6E6;
background-color: #FCFCFC;
font-size: 14px;
width: 200px;
background-image: url('/images/teacher/日历-选中.png');
background-repeat: no-repeat;
background-position: left 8px center;
background-size: 16px 16px;
padding-left: 32px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* 隐藏WebKit浏览器的默认日历图标 */
.date-field::-webkit-calendar-picker-indicator {
display: none;
}
/* 隐藏WebKit浏览器的默认清除按钮 */
.date-field::-webkit-clear-button {
display: none;
}
/* 隐藏WebKit浏览器的默认内部阴影 */
.date-field::-webkit-inner-spin-button {
display: none;
}
/* 按钮样式 */
.btn {
padding: 6px 20px;
border: none;
border-radius: 2px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 90px;
}
.btn-cancel {
background: #E2F5FF;
border: 1px solid #0288D1;
color: #0288D1;
}
.btn-next {
background: #0288D1;
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.certificate-create {
padding: 20px;
}
.orientation-options {
flex-direction: column;
}
.modal-content {
width: 95%;
margin: 20px;
}
}
</style>

View File

@ -0,0 +1,611 @@
<template>
<div class="certificate-detail">
<!-- 左侧证书样式和基本信息 -->
<div class="left-section">
<!-- 证书样式 -->
<div class="certificate-style">
<h2 class="section-title">证书样式</h2>
<div class="certificate-preview">
<img :src="certificate.thumbnail" :alt="certificate.name" class="certificate-image" />
</div>
<!-- 证书信息 -->
<div class="certificate-info">
<h3 class="info-title">证书信息</h3>
<div class="info-item">
<span class="label">证书名称:</span>
<span class="value">{{ certificate.name }}</span>
</div>
<div class="info-item">
<span class="label">证书分类:</span>
<span class="value">{{ certificate.category }}</span>
</div>
<div class="info-item">
<span class="label">证书内容:</span>
<div class="content-list">
<div class="content-item">考试分数</div>
<div class="content-item">考生姓名</div>
<div class="content-item">认证时间</div>
<div class="content-item">考试名称</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="action-btn edit-btn">
<img src="/images/teacher/edit.png" alt="编辑" class="btn-icon">
<span>编辑</span>
</button>
<button class="action-btn download-btn">
<img src="/images/teacher/download.png" alt="下载" class="btn-icon">
<span>下载</span>
</button>
</div>
</div>
</div>
<!-- 右侧证书颁发记录 -->
<div class="right-section">
<div class="issuance-record">
<div class="toolbar">
<h2>证书颁发记录</h2>
<div class="toolbar-actions">
<button class="btn btn-primary" @click="showIssuanceModal = true">颁发证书</button>
<button class="btn btn-danger">删除</button>
<div class="filter-dropdown">
<select class="filter-select">
<option value="">班级名称</option>
<option value="class1">班级名称1</option>
<option value="class2">班级名称2</option>
</select>
</div>
<div class="search-box">
<input type="text" placeholder="请输入关键词" class="search-input" />
<button class="btn btn-search">搜索</button>
</div>
</div>
</div>
<!-- 证书颁发记录表格 -->
<div class="record-table">
<n-data-table
:columns="columns"
:data="issuanceRecords"
:pagination="false"
:bordered="false"
:single-line="false"
:row-key="(row) => row.id"
/>
</div>
</div>
</div>
<!-- 证书颁发模态框 -->
<CertificateIssuanceModal
v-model:show="showIssuanceModal"
@confirm="handleIssuanceConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { useRoute } from 'vue-router'
import { useMessage, NDataTable, NButton } from 'naive-ui'
import CertificateIssuanceModal from '@/components/CertificateIssuanceModal.vue'
const route = useRoute()
// const router = useRouter()
const message = useMessage()
//
const certificate = ref({
id: 0,
name: '',
category: '',
thumbnail: ''
})
//
interface IssuanceRecord {
id: number
selected: boolean
certificateName: string
certificateNumber: string
issuanceTime: string
issuer: string
issuanceMethod: string
usageCategory: string
studentName: string
studentAccount: string
className: string
isExpired: string
validUntil: string
}
//
const issuanceRecords = ref<IssuanceRecord[]>([
{
id: 1,
selected: true,
certificateName: '证书名称',
certificateNumber: '2665585665',
issuanceTime: '2025.08.12 9:22',
issuer: '王建国',
issuanceMethod: '系统颁发',
usageCategory: '考试证书',
studentName: '某某某',
studentAccount: '556655455',
className: '班级名称1',
isExpired: '否',
validUntil: '永久有效'
},
{
id: 2,
selected: true,
certificateName: '证书名称',
certificateNumber: '2665585665',
issuanceTime: '2025.08.12 9:22',
issuer: '王建国',
issuanceMethod: '系统颁发',
usageCategory: '学习项目',
studentName: '某某某',
studentAccount: '556655455',
className: '班级名称1',
isExpired: '否',
validUntil: '永久有效'
}
])
//
// const selectAll = ref(true)
//
const showIssuanceModal = ref(false)
//
const columns = [
{
type: 'selection' as const,
width: 40
},
{
title: '证书名称',
key: 'certificateName',
width: 100
},
{
title: '证书编号',
key: 'certificateNumber',
width: 100
},
{
title: '颁发时间',
key: 'issuanceTime',
width: 120
},
{
title: '颁发人',
key: 'issuer',
width: 80
},
{
title: '颁发方式',
key: 'issuanceMethod',
width: 80
},
{
title: '使用类别',
key: 'usageCategory',
width: 80
},
{
title: '学员姓名',
key: 'studentName',
width: 80
},
{
title: '学员帐号',
key: 'studentAccount',
width: 100
},
{
title: '班级名称',
key: 'className',
width: 100
},
{
title: '是否过期',
key: 'isExpired',
width: 80
},
{
title: '有效期至',
key: 'validUntil',
width: 80
},
{
title: '操作',
key: 'actions',
width: 100,
render: (row: IssuanceRecord) => {
return h('div', { class: 'operations' }, [
h(NButton, {
size: 'small',
type: 'primary',
class: 'revoke-btn',
onClick: () => handleRevoke(row)
}, { default: () => '撤回' }),
h(NButton, {
size: 'small',
type: 'error',
class: 'delete-btn',
onClick: () => handleDelete(row)
}, { default: () => '删除' })
])
}
}
]
// ID
const certificateId = route.params.id
//
onMounted(() => {
certificate.value = {
id: Number(certificateId),
name: '名称名称名称',
category: '分类分类',
thumbnail: '/images/teacher/certificate.png'
}
})
//
// const handleSelectAll = () => {
// issuanceRecords.value.forEach(record => {
// record.selected = selectAll.value
// })
// }
//
// const handleItemSelect = () => {
// selectAll.value = issuanceRecords.value.every(record => record.selected)
// }
//
const handleRevoke = (record: IssuanceRecord) => {
console.log('撤回证书:', record)
message.success('撤回成功')
}
//
const handleDelete = (record: IssuanceRecord) => {
console.log('删除证书:', record)
message.success('删除成功')
}
//
const handleIssuanceConfirm = (selectedExams: any[]) => {
console.log('选中的考试/学习项目:', selectedExams)
message.success('证书颁发成功')
}
</script>
<style scoped>
.certificate-detail {
display: flex;
gap: 5px;
min-height: 100vh;
}
/* 左侧区域 */
.left-section {
flex: 1;
max-width: 275px;
background-color: #fff;
}
.certificate-style {
background: #fff;
padding: 20px;
}
.section-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0 0 20px 0;
}
.certificate-preview {
text-align: center;
margin-bottom: 20px;
}
.certificate-image {
max-width: 100%;
height: auto;
}
.certificate-info {
margin-bottom: 20px;
}
.info-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0 0 15px 0;
}
.info-item {
margin-bottom: 12px;
font-size: 16px;
}
.label {
font-weight: 500;
color: #666;
margin-right: 8px;
}
.value {
color: #666;
}
.content-list {
margin-top: 8px;
font-size: 14px;
}
.content-item {
text-align: center;
padding: 4px 0;
color: #666;
}
.action-buttons {
display: flex;
justify-content: center;
}
.action-btn {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
width: 45%;
padding: 8px 16px;
border: none;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
border: 1.5px solid #E6E6E6;
background: none;
}
.btn-icon {
width: 13px;
height: 13px;
object-fit: contain;
}
/* 右侧区域 */
.right-section {
background-color: #fff;
flex: 2;
}
.issuance-record {
width: 100%;
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-danger {
background: white;
color: #F44336;
border: 1px solid #F44336;
}
.btn-danger:hover {
background: #F44336;
color: white;
}
.btn-search {
background: #0288D1;
color: white;
border: none;
padding: 6px 16px;
cursor: pointer;
font-size: 14px;
}
.btn-search:hover {
background: #0277bd;
}
/* 筛选下拉框 */
.filter-dropdown {
position: relative;
}
.filter-select {
padding: 6px 12px;
border: 1px solid #F1F3F4;
border-radius: 2px;
font-size: 14px;
background: white;
cursor: pointer;
outline: none;
}
.filter-select:focus {
border-color: #0288D1;
}
/* 搜索框 */
.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;
}
/* 表格样式 */
.record-table {
overflow-x: auto;
padding: 30px 30px 30px 30px;
max-width: 100%;
}
/* Naive UI 表格样式覆盖 */
:deep(.n-data-table) {
background: white;
border: 1px solid #F1F3F4;
}
:deep(.n-data-table .n-data-table-table) {
border: none;
}
:deep(.n-data-table .n-data-table-thead .n-data-table-tr) {
background: #f5f5f5;
}
:deep(.n-data-table .n-data-table-thead .n-data-table-th) {
background: #f5f5f5;
border-bottom: 1px solid #eee;
font-weight: 600;
color: #062333;
padding: 5px 6px;
font-size: 14px;
text-align: center;
white-space: nowrap;
}
:deep(.n-data-table .n-data-table-tbody .n-data-table-td) {
border-bottom: 1px solid #eee;
padding: 16px 6px;
font-size: 12px;
color: #062333;
text-align: center;
}
:deep(.n-data-table .n-data-table-tbody .n-data-table-tr:hover) {
background: #f8f9fa;
}
:deep(.operations) {
display: flex;
justify-content: center;
gap: 8px;
}
:deep(.revoke-btn) {
background: transparent !important;
color: #0288D1 !important;
border: 1px solid #0288D1 !important;
border-radius: 2px !important;
font-size: 12px !important;
padding: 6px 12px !important;
}
:deep(.delete-btn) {
background: transparent !important;
color: #FF4D4F !important;
border: 1px solid #FF4D4F !important;
border-radius: 2px !important;
font-size: 12px !important;
padding: 6px 12px !important;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.certificate-detail {
flex-direction: column;
}
.left-section {
max-width: none;
}
}
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.toolbar-actions {
flex-wrap: wrap;
gap: 8px;
}
.search-box input {
width: 150px;
}
.record-table {
font-size: 12px;
}
th, td {
padding: 8px 4px;
}
}
</style>

View File

@ -1,18 +1,18 @@
<template>
<div class="certificate-management">
<div class="certificate-issuance">
<div class="content-placeholder">
<h2>证书管理</h2>
<p>证书管理功能正在开发中...</p>
<h2>证书颁发</h2>
<p>证书颁发功能正在开发中...</p>
</div>
</div>
</template>
<script setup lang="ts">
//
//
</script>
<style scoped>
.certificate-management {
.certificate-issuance {
padding: 20px;
background: #fff;
height: 100%;

View File

@ -0,0 +1,600 @@
<template>
<div class="certificate-management">
<!-- 顶部操作栏 -->
<div class="toolbar">
<h2>全部证书</h2>
<div class="toolbar-actions">
<button class="btn btn-primary" @click="addCertificate">添加证书</button>
<div class="filter-dropdown">
<select v-model="selectedExam" class="filter-select">
<option value="">考试</option>
<option value="exam1">期末考试</option>
<option value="exam2">期中考试</option>
<option value="exam3">模拟考试</option>
</select>
</div>
<div class="search-box">
<input type="text" placeholder="请输入关键词" v-model="searchKeyword" />
<button class="btn btn-search" @click="searchCertificates">搜索</button>
</div>
</div>
</div>
<!-- 证书网格展示 -->
<div class="certificate-grid">
<div v-for="certificate in filteredCertificates" :key="certificate.id" class="certificate-card" @click="viewCertificateDetail(certificate)">
<div class="certificate-thumbnail">
<img :src="certificate.thumbnail" :alt="certificate.name" class="certificate-image" />
</div>
<div class="certificate-info">
<div class="certificate-name">{{ certificate.name }}</div>
<div class="certificate-category">证书分类: {{ certificate.category }}</div>
</div>
<div class="file-menu">
<button class="file-menu-btn" @click.stop="toggleFileMenu(certificate.id)">
<img src="/images/teacher/more.png" alt="更多操作" class="more-icon" />
</button>
<div v-if="activeFileMenu === certificate.id" class="file-menu-dropdown">
<div class="menu-item" @click.stop="downloadCertificate(certificate)">
<img src="/images/teacher/download.png" alt="下载" class="menu-icon" />
<span>下载</span>
</div>
<div class="menu-item" @click.stop="editCertificate(certificate)">
<img src="/images/teacher/edit.png" alt="编辑" class="menu-icon" />
<span>编辑</span>
</div>
<div class="menu-item" @click.stop="deleteCertificate(certificate)">
<img src="/images/teacher/delete.png" alt="删除" class="menu-icon" />
<span>删除</span>
</div>
</div>
</div>
</div>
</div>
<!-- 添加证书模态框 -->
<div v-if="showAddModal" class="modal-overlay" @click="closeAddModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>添加证书</h3>
<button class="modal-close" @click="closeAddModal">×</button>
</div>
<div class="modal-body">
<p>添加证书功能正在开发中...</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useMessage } from 'naive-ui'
import { useRouter, useRoute } from 'vue-router'
const message = useMessage()
const router = useRouter()
const route = useRoute()
//
const searchKeyword = ref('')
const selectedExam = ref('')
//
const showAddModal = ref(false)
const activeFileMenu = ref<number | null>(null)
//
const certificates = ref([
{
id: 1,
name: '证书名称证书名称',
category: '分类名称',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '第一名',
thumbnail: '/images/teacher/certificate.png'
},
{
id: 2,
name: '优秀学员证书',
category: '学习成果',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '优秀',
thumbnail: '/images/teacher/certificate.png'
},
{
id: 3,
name: '技能认证证书',
category: '技能认证',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '通过',
thumbnail: '/images/teacher/certificate.png'
},
{
id: 4,
name: '竞赛获奖证书',
category: '竞赛奖励',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '二等奖',
thumbnail: '/images/teacher/certificate.png'
},
{
id: 5,
name: '培训结业证书',
category: '培训认证',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '结业',
thumbnail: '/images/teacher/certificate.png'
},
{
id: 6,
name: '项目完成证书',
category: '项目认证',
title: '荣誉证书',
issuer: '某某2021年大学生某某协会',
badge: '完成',
thumbnail: '/images/teacher/certificate.png'
}
])
//
const filteredCertificates = computed(() => {
let filtered = certificates.value
if (searchKeyword.value) {
filtered = filtered.filter((cert: any) =>
cert.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
cert.category.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
if (selectedExam.value) {
//
filtered = filtered.filter((cert: any) => cert.category === selectedExam.value)
}
return filtered
})
//
const addCertificate = () => {
// ID
const currentPath = route.path;
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/);
const courseId = courseIdMatch ? courseIdMatch[1] : '1';
//
const targetPath = `/teacher/course-editor/${courseId}/certificate/add`;
router.push(targetPath);
}
const closeAddModal = () => {
showAddModal.value = false
}
const searchCertificates = () => {
message.info('搜索证书: ' + searchKeyword.value)
}
const toggleFileMenu = (id: number) => {
console.log('点击了更多操作按钮ID:', id)
activeFileMenu.value = activeFileMenu.value === id ? null : id
console.log('当前激活的菜单:', activeFileMenu.value)
}
const downloadCertificate = (certificate: any) => {
message.success(`下载证书: ${certificate.name}`)
activeFileMenu.value = null
}
const editCertificate = (certificate: any) => {
message.info(`编辑证书: ${certificate.name}`)
activeFileMenu.value = null
}
const deleteCertificate = (certificate: any) => {
if (confirm(`确定要删除证书 "${certificate.name}" 吗?`)) {
const index = certificates.value.findIndex((c: any) => c.id === certificate.id)
if (index > -1) {
certificates.value.splice(index, 1)
message.success('删除成功')
}
}
activeFileMenu.value = null
}
const viewCertificateDetail = (certificate: any) => {
console.log('点击了证书卡片:', certificate);
// ID
const currentPath = route.path;
console.log('当前路径:', currentPath);
const courseIdMatch = currentPath.match(/\/course-editor\/(\d+)/);
const courseId = courseIdMatch ? courseIdMatch[1] : '1';
console.log('提取的课程ID:', courseId);
const targetPath = `/teacher/course-editor/${courseId}/certificate/detail/${certificate.id}`;
console.log('目标路径:', targetPath);
router.push(targetPath);
}
//
const closeFileMenu = () => {
activeFileMenu.value = null
}
//
document.addEventListener('click', closeFileMenu)
</script>
<style scoped>
.certificate-management {
width: 100%;
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-search {
background: #0288D1;
color: white;
border: none;
padding: 6px 16px;
cursor: pointer;
font-size: 14px;
}
.btn-search:hover {
background: #0277bd;
}
/* 筛选下拉框 */
.filter-dropdown {
position: relative;
}
.filter-select {
padding: 6px 12px;
border: 1px solid #F1F3F4;
border-radius: 2px;
font-size: 14px;
background: white;
cursor: pointer;
outline: none;
}
.filter-select:focus {
border-color: #0288D1;
}
/* 搜索框 */
.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;
}
/* 证书网格 */
.certificate-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
padding: 30px;
}
/* 证书卡片 */
.certificate-card {
padding: 50px 20px 10px 20px;
position: relative;
background: white;
border: 1.5px solid #D8D8D8;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
min-height: 215px;
}
.certificate-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background-color: #F3F5F8;
}
/* 证书缩略图 */
.certificate-thumbnail {
position: relative;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.certificate-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.certificate-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* 证书信息 */
.certificate-info {
padding: 5px 20px;
}
.certificate-name {
font-size: 14px;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.certificate-category {
font-size: 11px;
color: #999;
}
/* 文件菜单 */
.file-menu {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}
.file-menu-btn {
width: 30px;
height: 30px;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.more-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.file-menu-dropdown {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 4px;
z-index: 20;
min-width: 60px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.menu-item {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
font-size: 10px;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.menu-icon {
width: 12px;
height: 12px;
object-fit: contain;
flex-shrink: 0;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item:first-child {
border-radius: 4px 4px 0 0;
}
.menu-item:last-child {
border-radius: 0 0 4px 4px;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e6e6e6;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 1400px) {
.certificate-grid {
grid-template-columns: repeat(4, 1fr);
gap: 15px;
padding: 20px;
}
}
@media (max-width: 1200px) {
.certificate-grid {
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 20px;
}
}
@media (max-width: 900px) {
.certificate-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
padding: 20px;
}
}
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.toolbar-actions {
flex-wrap: wrap;
gap: 8px;
}
.search-box input {
width: 150px;
}
.certificate-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
padding: 15px;
}
.certificate-thumbnail {
height: 150px;
}
}
@media (max-width: 480px) {
.toolbar-actions {
flex-direction: column;
width: 100%;
}
.search-box {
width: 100%;
}
.search-box input {
width: 100%;
}
.certificate-grid {
grid-template-columns: 1fr;
padding: 10px;
}
}
</style>

View File

@ -0,0 +1,475 @@
<template>
<div class="certificate-new">
<div class="top-section">
<h1 class="page-title">新建证书11</h1>
<!-- 右侧区域 -->
<div class="right-section" :class="{ 'collapsed': isCollapsed }">
<div class="content">
<!-- 顶部按钮 -->
<div class="header-buttons">
<button class="btn btn-preview">证书预览</button>
<button class="btn btn-save">保存</button>
</div>
<!-- 证书背景 -->
<div class="section">
<h3 class="section-title">证书背景</h3>
<div class="color-palette">
<div class="color-item rainbow" :class="{ 'selected': selectedColor === customColor }" @click="showColorPicker = !showColorPicker">
<input
v-if="showColorPicker"
type="color"
:value="customColor"
@input="selectCustomColor($event.target.value)"
class="color-input-inline"
@click.stop
/>
</div>
<div class="color-item black" :class="{ 'selected': selectedColor === '#000000' }" @click="selectColor('#000000')"></div>
<div class="color-item dark-gray" :class="{ 'selected': selectedColor === '#333333' }" @click="selectColor('#333333')"></div>
<div class="color-item gray" :class="{ 'selected': selectedColor === '#666666' }" @click="selectColor('#666666')"></div>
<div class="color-item light-gray" :class="{ 'selected': selectedColor === '#999999' }" @click="selectColor('#999999')"></div>
<div class="color-item white" :class="{ 'selected': selectedColor === '#ffffff' }" @click="selectColor('#ffffff')"></div>
<div class="color-item red" :class="{ 'selected': selectedColor === '#ff0000' }" @click="selectColor('#ff0000')"></div>
<div class="color-item orange" :class="{ 'selected': selectedColor === '#ff8000' }" @click="selectColor('#ff8000')"></div>
<div class="color-item pink" :class="{ 'selected': selectedColor === '#ff80ff' }" @click="selectColor('#ff80ff')"></div>
<div class="color-item purple" :class="{ 'selected': selectedColor === '#8000ff' }" @click="selectColor('#8000ff')"></div>
<div class="color-item green" :class="{ 'selected': selectedColor === '#00ff00' }" @click="selectColor('#00ff00')"></div>
<div class="color-item teal" :class="{ 'selected': selectedColor === '#00ffff' }" @click="selectColor('#00ffff')"></div>
<div class="color-item light-blue" :class="{ 'selected': selectedColor === '#80ffff' }" @click="selectColor('#80ffff')"></div>
<div class="color-item blue" :class="{ 'selected': selectedColor === '#0080ff' }" @click="selectColor('#0080ff')"></div>
</div>
<div class="template-preview">
<div class="certificate-template">
<img src="/images/teacher/certificate.png" alt="证书模板" />
</div>
</div>
<button class="btn btn-replace btn-full-width">替换背景图</button>
</div>
<!-- 添加元素 -->
<div class="section">
<h3 class="section-title">添加元素</h3>
<div class="element-buttons">
<button class="btn btn-add">添加文字</button>
<button class="btn btn-add">添加图片</button>
</div>
</div>
<!-- 证书信息 -->
<div class="section">
<h3 class="section-title">证书信息</h3>
<div class="info-fields">
<div class="field">证书编号</div>
<div class="field">发证日期</div>
<div class="field">颁发机构</div>
<div class="field">有效期至</div>
<div class="field">核验二维码</div>
</div>
</div>
<!-- 学员信息 -->
<div class="section">
<h3 class="section-title">学员信息</h3>
<div class="info-fields">
<div class="field">学员姓名</div>
<div class="field">学员帐号</div>
<div class="field">班级名称</div>
<div class="field">自定义文字</div>
</div>
</div>
<!-- 考试信息 -->
<div class="section">
<h3 class="section-title">考试信息</h3>
<div class="info-fields">
<div class="field">考试名称</div>
<div class="field">考试分数</div>
<div class="field">考试评语</div>
</div>
</div>
</div>
<!-- 收起/展开按钮 -->
<div class="collapse-button" @click="toggleCollapse">
<img src="/images/teacher/上传2.png" alt="收起/展开" class="arrow" :class="{ 'collapsed': isCollapsed }" />
</div>
</div>
</div>
<div class="certificate-content" :style="{ backgroundColor: selectedColor }">
<img src="/images/teacher/certificate.png" alt="">
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
//
let originalPadding = ''
//
const isCollapsed = ref(false)
//
const selectedColor = ref('#ffffff')
//
const customColor = ref('#ff0000')
//
const showColorPicker = ref(false)
// /
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
//
const selectColor = (color: string) => {
selectedColor.value = color
}
//
const selectCustomColor = (color: string) => {
customColor.value = color
selectedColor.value = color
showColorPicker.value = false
}
// URL
onMounted(() => {
console.log('新建证书页面参数:', route.query)
//
const routerViewContainer = document.querySelector('.router-view-container') as HTMLElement
if (routerViewContainer) {
//
originalPadding = routerViewContainer.style.padding
//
routerViewContainer.style.padding = '0'
}
})
//
onUnmounted(() => {
const routerViewContainer = document.querySelector('.router-view-container') as HTMLElement
if (routerViewContainer) {
routerViewContainer.style.padding = originalPadding
}
})
</script>
<style scoped>
/* 只在当前页面覆盖上级容器的内边距 */
.certificate-new:deep(.router-view-container) {
padding: 0 !important;
}
.certificate-new:deep(.router-view-container.full-width) {
padding: 0 !important;
}
/* 确保页面内容不受上级容器影响 */
.certificate-new {
margin: 0;
padding: 0;
width: 100%;
min-height: 100vh;
background: #F6F6F6;
display: flex;
flex-direction: column;
}
.top-section {
width: 100%;
height: 80px;
background-color: #fff;
position: relative;
margin-bottom: 20px;
}
.right-section {
position: absolute;
top: 0;
right: 0;
width: 240px;
height: 100vh;
background: #fff;
transition: transform 0.3s ease;
}
.right-section.collapsed {
transform: translateX(240px);
}
.collapse-button {
position: absolute;
left: -40px;
top: 50%;
transform: translateY(-50%);
height: 0;
width: 60px;
border-bottom: 20px solid #fff;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
transform: rotate(-90deg);
transform-origin: center;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
}
.collapse-button:hover {
background: #f5f5f5;
}
.arrow {
margin-top: 20px;
width: 10px;
height: 6px;
transition: transform 0.3s ease;
transform: rotate(180deg);
}
.arrow.collapsed {
transform: rotate(0);
}
.content {
margin: auto;
background: #fff;
padding: 20px;
height: 100%;
overflow-y: auto;
}
.header-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.btn {
min-width: 90px;
padding: 6px 16px;
border-radius: 2px;
border: 1px solid #E6E6E6;
background: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn-preview {
background: #E3F2FD;
color: #0288D1;
border-color: #0288D1;
}
.btn-save {
background: #0288D1;
color: #fff;
border-color: #0288D1;
}
.btn-add,
.btn-replace {
background: #F5F5F5;
color: #666666;
font-size: 12px;
border: none;
padding: 8px 0;
}
.btn-full-width {
width: 100%;
border: none;
color: #666666;
font-size: 12px;
}
.btn:hover {
opacity: 0.8;
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
}
.color-palette {
padding: 10px;
background-color: #F7F8FA;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 15px;
}
.color-item {
width: 20px;
height: 20px;
border-radius: 2px;
cursor: pointer;
border: 1px solid #E6E6E6;
}
.color-item.rainbow {
background: linear-gradient(45deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff00ff, #ff0080);
}
.color-item.black {
background: #000;
}
.color-item.dark-gray {
background: #333;
}
.color-item.gray {
background: #666;
}
.color-item.light-gray {
background: #999;
}
.color-item.white {
background: #fff;
}
.color-item.red {
background: #ff0000;
}
.color-item.orange {
background: #ff8000;
}
.color-item.pink {
background: #ff80ff;
}
.color-item.purple {
background: #8000ff;
}
.color-item.green {
background: #00ff00;
}
.color-item.teal {
background: #00ffff;
}
.color-item.light-blue {
background: #80ffff;
}
.color-item.blue {
background: #0080ff;
}
.color-item.selected {
border: 2px solid #0288D1;
transform: scale(1.1);
}
.template-preview {
margin-bottom: 15px;
}
.certificate-template {
width: 100%;
height: 100px;
background-color: #F7F8FA;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.certificate-template img {
width: 111px;
height: 83px;
object-fit: cover;
}
.element-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.info-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
text-align: center;
}
.field {
padding: 7px 12px;
background: #F7F8FA;
border: none;
border-radius: 4px;
font-size: 12px;
color: #666666;
cursor: pointer;
}
.field:hover {
background: #E8E8E8;
}
/* 内联颜色选择器样式 */
.color-input-inline {
width: 100%;
height: 100%;
border: none;
border-radius: 2px;
cursor: pointer;
opacity: 0;
position: absolute;
top: 0;
left: 0;
}
.color-item.rainbow {
position: relative;
}
.certificate-content {
width: 1000px;
height: 89vh;
margin: 0 auto;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.certificate-content img {
width: 800px;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="certificate-query">
<div class="content-placeholder">
<h2>证书查询</h2>
<p>证书查询功能正在开发中...</p>
</div>
</div>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.certificate-query {
padding: 20px;
background: #fff;
height: 100%;
}
.content-placeholder {
text-align: center;
padding: 60px 20px;
}
.content-placeholder h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
}
.content-placeholder p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="certificate-template">
<div class="content-placeholder">
<h2>证书模板管理1</h2>
<p>证书模板管理功能正在开发中...</p>
</div>
</div>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.certificate-template {
padding: 20px;
background: #fff;
height: 100%;
}
.content-placeholder {
text-align: center;
padding: 60px 20px;
}
.content-placeholder h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
}
.content-placeholder p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<div class="certificate-index">
<div class="content-placeholder">
<h2>证书管理</h2>
<p>欢迎使用证书管理系统</p>
<div class="certificate-menu">
<div class="menu-item" @click="navigateTo('/teacher/certificate/management')">
<h3>证书管理</h3>
<p>管理已颁发的证书</p>
</div>
<div class="menu-item" @click="navigateTo('/teacher/certificate/template')">
<h3>证书模板</h3>
<p>管理证书模板</p>
</div>
<div class="menu-item" @click="navigateTo('/teacher/certificate/issuance')">
<h3>证书颁发</h3>
<p>颁发新证书</p>
</div>
<div class="menu-item" @click="navigateTo('/teacher/certificate/query')">
<h3>证书查询</h3>
<p>查询证书信息</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const navigateTo = (path: string) => {
router.push(path)
}
</script>
<style scoped>
.certificate-index {
padding: 20px;
background: #fff;
height: 100%;
}
.content-placeholder {
text-align: center;
padding: 40px 20px;
}
.content-placeholder h2 {
font-size: 28px;
color: #333;
margin-bottom: 10px;
}
.content-placeholder p {
font-size: 16px;
color: #666;
margin-bottom: 40px;
}
.certificate-menu {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
max-width: 800px;
margin: 0 auto;
}
.menu-item {
padding: 30px 20px;
border: 1px solid #e6e6e6;
border-radius: 8px;
background: #fafafa;
cursor: pointer;
transition: all 0.3s ease;
}
.menu-item:hover {
border-color: #0288D1;
background: #E2F5FF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(2, 136, 209, 0.15);
}
.menu-item h3 {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.menu-item p {
font-size: 14px;
color: #666;
margin: 0;
}
</style>

View File

@ -226,6 +226,7 @@ const handleConfirm = () => {
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.button-text1{
color: #0288D1 ;
@ -398,6 +399,7 @@ const handleConfirm = () => {
/* 字体居中 */
display: flex;
align-items: center;
justify-content: center;
/* 小手 */
cursor: pointer;
border-radius: 4px;

View File

@ -146,7 +146,9 @@ const hideSidebar = computed(() => {
'add-question', //
'add-homework', //
'template-import',
'review/'
'review/',
'certificate/detail/',
'certificate/add'
]
//

View File

@ -145,6 +145,7 @@ const handleConfirm = () => {
background-size: 100% 100%;
margin: 0 auto;
border-radius: 8px;
padding-top: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
@ -169,7 +170,7 @@ const handleConfirm = () => {
background-size: 100% 100%;
margin: 46px 0 0 22px;
border: 1px solid rgba(204, 204, 204, 1);
display: flex;
}
.upload-label {
@ -354,7 +355,9 @@ const handleConfirm = () => {
.action-buttons {
width: 147px;
height: 32px;
margin: 103px 0 23px 905px;
margin: 103px 0 0 905px;
display: flex;
gap: 20px;
}
.cancel-btn-container {
@ -370,6 +373,9 @@ const handleConfirm = () => {
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn:hover {
@ -387,7 +393,6 @@ const handleConfirm = () => {
text-align: left;
white-space: nowrap;
line-height: 22px;
margin: 5px 0 0 17px;
}
.confirm-btn {
@ -398,6 +403,9 @@ const handleConfirm = () => {
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.confirm-btn:hover {
@ -415,7 +423,6 @@ const handleConfirm = () => {
text-align: left;
white-space: nowrap;
line-height: 22px;
margin: 5px 0 0 17px;
}
/* 添加按钮悬停效果 */