OL-LearnPlatform-Frontend/src/components/CertificateIssuanceModal.vue

980 lines
22 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>
<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>