|
|
@@ -1,52 +1,500 @@
|
|
|
<!-- 毕业教育考试数据统计报告 -->
|
|
|
<template>
|
|
|
<div class="dataStatisticsReport-addPage">
|
|
|
- <img class="logo-img" :src="logoUrl">
|
|
|
- <p class="title-p">实验室安全教育毕业报告</p>
|
|
|
+ <!-- 操作按钮区 -->
|
|
|
+ <!--<div class="action-bar">-->
|
|
|
+ <!--<p class="page-inquire-common-style-button download-btn" @click="downloadPdf">-->
|
|
|
+ <!--<i class="el-icon-download"></i> 下载PDF-->
|
|
|
+ <!--</p>-->
|
|
|
+ <!--</div>-->
|
|
|
+
|
|
|
+ <!-- 报告主体(用于截图的区域) -->
|
|
|
+ <div class="report-wrap" ref="reportWrap" v-show="showType">
|
|
|
+ <div class="report-wrap-big-box">
|
|
|
+ <div class="report-wrap-min-box">
|
|
|
+ <!--logo-->
|
|
|
+ <div class="logo-box">
|
|
|
+ <img :src="logUrl" />
|
|
|
+ </div>
|
|
|
+ <!--标题-->
|
|
|
+ <p class="title-p">实验室安全教育毕业报告</p>
|
|
|
+ <!--用户信息-->
|
|
|
+ <div class="user-box">
|
|
|
+ <div class="user-min-box">
|
|
|
+ <div>
|
|
|
+ <p>姓名:</p>
|
|
|
+ <p>{{newData.userName}}</p>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p>学号:</p>
|
|
|
+ <p>{{newData.account}}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="user-min-box">
|
|
|
+ <div>
|
|
|
+ <p>学院:</p>
|
|
|
+ <p>{{newData.deptName}}</p>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p>年级:</p>
|
|
|
+ <p>{{newData.grade}}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="user-min-box">
|
|
|
+ <div>
|
|
|
+ <p>用户类型:</p>
|
|
|
+ <p>{{newData.educationTypeName}}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!--学年学时-->
|
|
|
+ <div class="study-hours-max-big-box">
|
|
|
+ <div class="study-hours-title-box">
|
|
|
+ <p></p>
|
|
|
+ <p>学年学时完成情况</p>
|
|
|
+ </div>
|
|
|
+ <div class="study-hours-text-title-box">
|
|
|
+ <p>学年</p>
|
|
|
+ <p>完成学时</p>
|
|
|
+ <p>要求学时</p>
|
|
|
+ </div>
|
|
|
+ <div class="study-hours-for-box" v-for="(item,index) in newData.yearList" :key="index">
|
|
|
+ <p>{{item.academicYear}}</p>
|
|
|
+ <p>{{item.completedStudyHours}}</p>
|
|
|
+ <p>{{item.studyHoursRequirement}}</p>
|
|
|
+ </div>
|
|
|
+ <div class="study-hours-bottom-box">
|
|
|
+ <p>累计</p>
|
|
|
+ <p>{{newData.total}} 学时</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!--考试通过记录-->
|
|
|
+ <div class="study-hours-max-big-box-2">
|
|
|
+ <div class="study-hours-title-box">
|
|
|
+ <p></p>
|
|
|
+ <p>考试通过记录</p>
|
|
|
+ </div>
|
|
|
+ <div class="study-hours-text-title-box">
|
|
|
+ <p>学年</p>
|
|
|
+ <p>考试类型</p>
|
|
|
+ <p>成绩</p>
|
|
|
+ </div>
|
|
|
+ <div class="study-hours-for-box" v-for="(item,index) in newData.examList" :key="index">
|
|
|
+ <p>{{item.academicYear}}</p>
|
|
|
+ <p>{{item.examTypeName}}</p>
|
|
|
+ <p>{{item.score}}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!--描述信息-->
|
|
|
+ <p class="study-text-p">该同学在校期间认真完成实验室安全教育各项学习任务,累计完成 <span>{{newData.total}}</span> 学时安全培训,通过全部年度安全考核,具备良好的实验室安全意识和规范操作能力,特此证明。</p>
|
|
|
+ <!--落款-->
|
|
|
+ <div class="signature-box">
|
|
|
+ <p>{{newData.issuingUnit}}</p>
|
|
|
+ <p>{{newData.issuingTime}}</p>
|
|
|
+ </div>
|
|
|
+ <!-- 水印 -->
|
|
|
+ <!--<div class="watermark-box">-->
|
|
|
+ <!--<span v-for="n in 40" :key="n" class="watermark-text">实验室安全教育</span>-->
|
|
|
+ <!--</div>-->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+ import { examElLabEduRelationGraduationReport } from "@/api/safetyEducationExaminationNew/index";
|
|
|
+ import html2canvas from 'html2canvas'
|
|
|
export default {
|
|
|
- name: 'addPage',
|
|
|
+ name: 'dataStatisticsReport',
|
|
|
props: {
|
|
|
propsData: {}
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
- logoUrl:'192.168.1.8/statics/2026/06/18/90a7ed62-c6c4-43f8-8691-1d8e35e44866.png',
|
|
|
+ logUrl:'https://labcontrol.nwafu.edu.cn/statics/2024/07/22/d1a9d129-ced7-49ac-b258-e5167d85232d.png',
|
|
|
newData: {
|
|
|
- userName:'刘佳琪',
|
|
|
- account:'2022200089',
|
|
|
- deptName:'材料科学学院',
|
|
|
- academicYear:'2022级',
|
|
|
- userType:'本科生',
|
|
|
- total:'12学时',
|
|
|
+ userName:'',
|
|
|
+ account:'',
|
|
|
+ deptName:'',
|
|
|
+ grade:'',
|
|
|
+ userTypeName:'',
|
|
|
+ total:'',
|
|
|
yearList:[],
|
|
|
examList:[],
|
|
|
+ issuingUnit:'',
|
|
|
+ issuingTime:'',
|
|
|
},
|
|
|
text:'该同学在校期间认真完成实验室安全教育各项学习任务,累计完成12学时安全培训,通过全部年度安全考核,具备良好的实验室安全意识和规范操作能力,特此证明。',
|
|
|
- deptName:'实验室安全与条件保障处',
|
|
|
- year:'2026年7月',
|
|
|
+ downloading: false,
|
|
|
+ showType:false,
|
|
|
}
|
|
|
},
|
|
|
- created() {
|
|
|
-
|
|
|
- },
|
|
|
mounted() {
|
|
|
this.initialize()
|
|
|
},
|
|
|
methods: {
|
|
|
- // 初始化
|
|
|
initialize() {
|
|
|
-
|
|
|
+ examElLabEduRelationGraduationReport({}).then(response => {
|
|
|
+ this.$set(this, 'newData', response.data);
|
|
|
+ this.$set(this, 'showType', true);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ async downloadPdf() {
|
|
|
+ if (this.downloading) return
|
|
|
+ this.downloading = true
|
|
|
+ try {
|
|
|
+ const el = this.$refs.reportWrap
|
|
|
+ const canvas = await html2canvas(el, {
|
|
|
+ scale: 2,
|
|
|
+ useCORS: true,
|
|
|
+ backgroundColor: '#ffffff',
|
|
|
+ logging: false,
|
|
|
+ })
|
|
|
+ const imgW = canvas.width
|
|
|
+ const imgH = canvas.height
|
|
|
+ // 将 canvas 转为 JPEG base64(去掉 data:image/jpeg;base64, 前缀)
|
|
|
+ const jpegB64 = canvas.toDataURL('image/jpeg', 0.92).split(',')[1]
|
|
|
+ // atob 解码为二进制字节数组
|
|
|
+ const jpegBytes = Uint8Array.from(atob(jpegB64), c => c.charCodeAt(0))
|
|
|
+ // A4 点数 595×842,按比例缩放图片到页面宽度
|
|
|
+ const pdfW = 595
|
|
|
+ const pdfH = Math.round((imgH / imgW) * pdfW)
|
|
|
+ // 构造最小合法 PDF(内嵌 JPEG,DCTDecode 不需要解码)
|
|
|
+ const pdfBytes = this._buildPdf(jpegBytes, imgW, imgH, pdfW, pdfH)
|
|
|
+ const blob = new Blob([pdfBytes], { type: 'application/pdf' })
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = `实验室安全教育毕业报告_${this.newData.userName || ''}.pdf`
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ document.body.removeChild(a)
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ this.$message && this.$message.error('下载失败,请重试')
|
|
|
+ } finally {
|
|
|
+ this.downloading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 构造内嵌单张 JPEG 图片的最小 PDF
|
|
|
+ _buildPdf(jpegBytes, imgW, imgH, pageW, pageH) {
|
|
|
+ const enc = s => new TextEncoder().encode(s)
|
|
|
+ const concat = (...parts) => {
|
|
|
+ const total = parts.reduce((n, p) => n + p.length, 0)
|
|
|
+ const out = new Uint8Array(total)
|
|
|
+ let off = 0
|
|
|
+ for (const p of parts) { out.set(p, off); off += p.length }
|
|
|
+ return out
|
|
|
+ }
|
|
|
+ const stream = `q ${pageW} 0 0 ${pageH} 0 0 cm /Im0 Do Q\n`
|
|
|
+ const obj = (id, dict, data) => {
|
|
|
+ const d = data ? concat(enc(dict + '\nstream\n'), data, enc('\nendstream\n')) : enc(dict + '\n')
|
|
|
+ return { id, bytes: concat(enc(`${id} 0 obj\n`), d, enc('endobj\n')) }
|
|
|
+ }
|
|
|
+ const o1 = obj(1, '<< /Type /Catalog /Pages 2 0 R >>')
|
|
|
+ const o2 = obj(2, '<< /Type /Pages /Kids [3 0 R] /Count 1 >>')
|
|
|
+ const o3 = obj(3, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>`)
|
|
|
+ const streamEnc = enc(stream)
|
|
|
+ const o4 = obj(4, `<< /Length ${streamEnc.length} >>`, streamEnc)
|
|
|
+ const o5 = obj(5, `<< /Type /XObject /Subtype /Image /Width ${imgW} /Height ${imgH} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${jpegBytes.length} >>`, jpegBytes)
|
|
|
+ const header = enc('%PDF-1.4\n')
|
|
|
+ const objs = [o1, o2, o3, o4, o5]
|
|
|
+ const offsets = []
|
|
|
+ let pos = header.length
|
|
|
+ const bodyParts = []
|
|
|
+ for (const o of objs) {
|
|
|
+ offsets.push(pos)
|
|
|
+ pos += o.bytes.length
|
|
|
+ bodyParts.push(o.bytes)
|
|
|
+ }
|
|
|
+ const xrefPos = pos
|
|
|
+ const xref = enc(
|
|
|
+ `xref\n0 6\n0000000000 65535 f \n` +
|
|
|
+ offsets.map(o => `${String(o).padStart(10, '0')} 00000 n \n`).join('') +
|
|
|
+ `trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n${xrefPos}\n%%EOF\n`
|
|
|
+ )
|
|
|
+ return concat(header, ...bodyParts, xref)
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
- .dataStatisticsReport-addPage {
|
|
|
+.dataStatisticsReport-addPage {
|
|
|
+ background: #f5f7fa;
|
|
|
+ flex:1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow-y: scroll;
|
|
|
|
|
|
+ .action-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ .download-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ i { font-size: 15px; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .report-wrap {
|
|
|
+ /*position: relative;*/
|
|
|
+ /*background: #fff;*/
|
|
|
+ /*border-radius: 8px;*/
|
|
|
+ /*box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);*/
|
|
|
+ /*padding: 40px 50px;*/
|
|
|
+ /*max-width: 900px;*/
|
|
|
+ /*margin: 0 auto;*/
|
|
|
+ /*overflow: hidden;*/
|
|
|
+ flex:1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width:720px;
|
|
|
+ .report-wrap-big-box{
|
|
|
+ width:680px;
|
|
|
+ margin:20px;
|
|
|
+ border: 1px solid #c9a84c;
|
|
|
+ border-radius:4px;
|
|
|
+ position: relative;
|
|
|
+ flex:1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background-color: #fffdf5;
|
|
|
+ .report-wrap-min-box{
|
|
|
+ flex:1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border: 1px solid #c9a84c;
|
|
|
+ border-radius:4px;
|
|
|
+ margin:1px;
|
|
|
+ height: 500px;
|
|
|
+ //logo
|
|
|
+ .logo-box{
|
|
|
+ width:578px;
|
|
|
+ height:88px;
|
|
|
+ border-radius:6px;
|
|
|
+ background-color: #1a6e3a;
|
|
|
+ margin:40px 0 0 48px;
|
|
|
+ overflow: hidden;
|
|
|
+ img{
|
|
|
+ width: 350px;
|
|
|
+ margin: 6px auto;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //标题
|
|
|
+ .title-p{
|
|
|
+ line-height:56px;
|
|
|
+ color:#8b452f;
|
|
|
+ text-align: center;
|
|
|
+ font-size:20px;
|
|
|
+ letter-spacing: 6px;
|
|
|
+ }
|
|
|
+ //用户信息
|
|
|
+ .user-box{
|
|
|
+ width:578px;
|
|
|
+ height:99px;
|
|
|
+ background-color: #faf6ec;
|
|
|
+ border:1px solid #e8dcc8;
|
|
|
+ margin:9px 0 0 48px;
|
|
|
+ border-radius:4px;
|
|
|
+ padding:10px 20px;
|
|
|
+ .user-min-box{
|
|
|
+ display: flex;
|
|
|
+ div{
|
|
|
+ flex:1;
|
|
|
+ display: flex;
|
|
|
+ p{
|
|
|
+ line-height:26px;
|
|
|
+ font-size:14px;
|
|
|
+ color:#4a3c2a;
|
|
|
+ }
|
|
|
+ p:nth-child(1){
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //列表
|
|
|
+ .study-hours-max-big-box{
|
|
|
+ margin:20px 48px 0 48px;
|
|
|
+ .study-hours-title-box{
|
|
|
+ display: flex;
|
|
|
+ p:nth-child(1){
|
|
|
+ height:20px;
|
|
|
+ width:3px;
|
|
|
+ background-color: #c9a84c;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+ line-height:20px;
|
|
|
+ font-size:12px;
|
|
|
+ flex:1;
|
|
|
+ color:#8b4513;
|
|
|
+ margin-left:8px;
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-text-title-box{
|
|
|
+ display: flex;
|
|
|
+ background-color: #f5edd8;
|
|
|
+ border-bottom:1px solid #d4c5a0;
|
|
|
+ margin-top:10px;
|
|
|
+ p{
|
|
|
+ line-height:34px;
|
|
|
+ font-size:12px;
|
|
|
+ color: #5a4320;
|
|
|
+ flex:1;
|
|
|
+ text-align: center;
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-for-box{
|
|
|
+ display: flex;
|
|
|
+ border-bottom:1px solid #ebe3d0;
|
|
|
+ p{
|
|
|
+ line-height:32px;
|
|
|
+ font-size:12px;
|
|
|
+ color: #4a3c2a;
|
|
|
+ flex:1;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-bottom-box{
|
|
|
+ display: flex;
|
|
|
+ background-color: #faf6ec;
|
|
|
+ border-bottom:1px solid #ebe3d0;
|
|
|
+ p{
|
|
|
+ line-height:32px;
|
|
|
+ font-size:12px;
|
|
|
+ color: #4a3c2a;
|
|
|
+ font-weight: 700;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ p:nth-child(1){
|
|
|
+ width:216px;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+ flex:1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-max-big-box-2{
|
|
|
+ margin:20px 48px 0 48px;
|
|
|
+ .study-hours-title-box{
|
|
|
+ display: flex;
|
|
|
+ p:nth-child(1){
|
|
|
+ height:20px;
|
|
|
+ width:3px;
|
|
|
+ background-color: #c9a84c;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+ line-height:20px;
|
|
|
+ font-size:12px;
|
|
|
+ flex:1;
|
|
|
+ color:#8b4513;
|
|
|
+ margin-left:8px;
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-text-title-box{
|
|
|
+ display: flex;
|
|
|
+ background-color: #f5edd8;
|
|
|
+ border-bottom:1px solid #d4c5a0;
|
|
|
+ margin-top:10px;
|
|
|
+ p{
|
|
|
+ line-height:34px;
|
|
|
+ font-size:12px;
|
|
|
+ color: #5a4320;
|
|
|
+ flex:1;
|
|
|
+ text-align: center;
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ p:nth-child(1){
|
|
|
+ width:216px;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+ width:182px;
|
|
|
+ }
|
|
|
+ p:nth-child(3){
|
|
|
+ width:180px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .study-hours-for-box{
|
|
|
+ display: flex;
|
|
|
+ border-bottom:1px solid #ebe3d0;
|
|
|
+ p{
|
|
|
+ line-height:32px;
|
|
|
+ font-size:12px;
|
|
|
+ color: #4a3c2a;
|
|
|
+ }
|
|
|
+ p:nth-child(1){
|
|
|
+ width:126px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+ p:nth-child(2){
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ p:nth-child(3){
|
|
|
+ width:60px;
|
|
|
+ margin-right:66px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //描述信息
|
|
|
+ .study-text-p{
|
|
|
+ margin:25px 48px 0 48px;
|
|
|
+ line-height:24px;
|
|
|
+ font-size:14px;
|
|
|
+ color:#4a3c2a;
|
|
|
+ text-indent: 28px;
|
|
|
+ span{
|
|
|
+ font-weight:700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //落款
|
|
|
+ .signature-box{
|
|
|
+ /*margin:63px 180px 40px 48px;*/
|
|
|
+ margin:103px 180px 80px 48px;
|
|
|
+ p{
|
|
|
+ height:24px;
|
|
|
+ text-align: right;
|
|
|
+ font-size: 14px;
|
|
|
+ color:#4a3c2a;
|
|
|
+ line-height:24px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 水印
|
|
|
+ .watermark-box {
|
|
|
+ position: absolute;
|
|
|
+ top: 0; left: 0; right: 0; bottom: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-content: flex-start;
|
|
|
+ padding: 60px 20px;
|
|
|
+ gap: 40px 60px;
|
|
|
+ z-index: 0;
|
|
|
+ opacity: 0.04;
|
|
|
+ .watermark-text {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #0183fa;
|
|
|
+ transform: rotate(-30deg);
|
|
|
+ white-space: nowrap;
|
|
|
+ display: inline-block;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+}
|
|
|
</style>
|