feat: 支持打印 / 导出 PDF

This commit is contained in:
pipipi-pikachu 2022-05-21 20:38:00 +08:00
parent 47450b7b91
commit 4de6d5234e
4 changed files with 265 additions and 2 deletions

View File

@ -22,11 +22,12 @@ npm run serve
# 📚 功能列表 # 📚 功能列表
### 通用功能 ### 基础功能
- 历史记录(撤销、重做) - 历史记录(撤销、重做)
- 快捷键 - 快捷键
- 右键菜单 - 右键菜单
- 导出本地文件PPTX、JSON、图片 - 导出本地文件PPTX、JSON、图片、PDF
- 打印
### 幻灯片页面编辑 ### 幻灯片页面编辑
- 页面添加、删除 - 页面添加、删除
- 页面顺序调整 - 页面顺序调整

81
src/utils/print.ts Normal file
View File

@ -0,0 +1,81 @@
interface PageSize {
width: number;
height: number;
margin: number;
}
const createIframe = () => {
const iframe = document.createElement('iframe')
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.position = 'absolute'
iframe.style.right = '0'
iframe.style.top = '0'
iframe.style.border = '0'
document.body.appendChild(iframe)
return iframe
}
const writeContent = (doc: Document, printNode: HTMLElement, size: PageSize) => {
const docType = '<!DOCTYPE html>'
let style = ''
const styleSheets = document.styleSheets
if (styleSheets) {
for (const styleSheet of styleSheets) {
if (!styleSheet.cssRules) continue
for (const rule of styleSheet.cssRules) {
style += rule.cssText
}
}
}
const { width, height, margin } = size
const head = `
<head>
<style type="text/css">
${style}
html, body {
height: auto;
overflow: auto;
}
@media print {
@page {
size: ${width + 2 * margin}px ${height + 2 * margin}px;
margin: ${margin}px;
}
}
</style>
</head>
`
const body = '<body>' + printNode.innerHTML + '</body>'
doc.open()
doc.write(`
${docType}
<html>
${head}
${body}
</html>
`)
doc.close()
}
export const print = (printNode: HTMLElement, size: PageSize) => {
const iframe = createIframe()
const iframeContentWindow = iframe.contentWindow
if (!iframe.contentDocument || !iframeContentWindow) return
writeContent(iframe.contentDocument, printNode, size)
const handleLoadIframe = () => {
iframeContentWindow.focus()
iframeContentWindow.print()
document.body.removeChild(iframe)
}
iframe.addEventListener('load', handleLoadIframe)
}

View File

@ -0,0 +1,165 @@
<template>
<div class="export-pdf-dialog">
<div class="thumbnails-view">
<div class="thumbnails" ref="pdfThumbnailsRef">
<ThumbnailSlide
class="thumbnail"
:slide="currentSlide"
:size="1600"
v-if="range === 'current'"
/>
<template v-else>
<ThumbnailSlide
class="thumbnail"
:class="{ 'break-page': (index + 1) % count === 0 }"
v-for="(slide, index) in slides"
:key="slide.id"
:slide="slide"
:size="1600"
/>
</template>
</div>
</div>
<div class="configs">
<div class="row">
<div class="title">导出范围</div>
<RadioGroup
class="config-item"
v-model:value="range"
>
<RadioButton style="width: 50%;" value="all">全部幻灯片</RadioButton>
<RadioButton style="width: 50%;" value="current">当前幻灯片</RadioButton>
</RadioGroup>
</div>
<div class="row">
<div class="title">每页数量</div>
<Select
class="config-item"
v-model:value="count"
>
<SelectOption :value="1">1</SelectOption>
<SelectOption :value="2">2</SelectOption>
<SelectOption :value="3">3</SelectOption>
</Select>
</div>
<div class="row">
<div class="title">边缘留白</div>
<div class="config-item">
<Switch v-model:checked="padding" />
</div>
</div>
<div class="btns">
<Button class="btn export" type="primary" @click="expPDF()">打印 / 导出 PDF</Button>
<Button class="btn close" @click="close()">关闭</Button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { print } from '@/utils/print'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
export default defineComponent({
name: 'export-pdf-dialog',
components: {
ThumbnailSlide,
},
setup(props, { emit }) {
const { slides, currentSlide } = storeToRefs(useSlidesStore())
const pdfThumbnailsRef = ref<HTMLElement>()
const range = ref<'all' | 'current'>('all')
const count = ref(1)
const padding = ref(true)
const close = () => emit('close')
const expPDF = () => {
if (!pdfThumbnailsRef.value) return
const pageSize = {
width: 1600,
height: range.value === 'all' ? 900 * count.value : 900,
margin: padding.value ? 50 : 0,
}
print(pdfThumbnailsRef.value, pageSize)
}
return {
pdfThumbnailsRef,
slides,
currentSlide,
range,
count,
padding,
expPDF,
close,
}
},
})
</script>
<style lang="scss" scoped>
.export-pdf-dialog {
height: 400px;
display: flex;
justify-content: center;
position: relative;
overflow: hidden;
}
.thumbnails-view {
@include absolute-0();
&::after {
content: '';
background-color: #fff;
@include absolute-0();
}
}
.thumbnail {
&.break-page {
break-after: page;
}
}
.configs {
width: 300px;
display: flex;
flex-direction: column;
justify-content: center;
z-index: 1;
.row {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
}
.title {
width: 100px;
}
.config-item {
flex: 1;
}
.btns {
display: flex;
justify-content: center;
align-items: center;
margin-top: 40px;
.export {
flex: 1;
}
.close {
width: 100px;
margin-left: 10px;
}
}
}
</style>

View File

@ -8,6 +8,7 @@
<MenuItem @click="exportJSON()">导出 JSON</MenuItem> <MenuItem @click="exportJSON()">导出 JSON</MenuItem>
<MenuItem @click="exportPPTX()">导出 PPTX</MenuItem> <MenuItem @click="exportPPTX()">导出 PPTX</MenuItem>
<MenuItem @click="exportImgDialogVisible = true">导出图片</MenuItem> <MenuItem @click="exportImgDialogVisible = true">导出图片</MenuItem>
<MenuItem @click="exportPDFDialogVisible = true">打印 / 导出 PDF</MenuItem>
</Menu> </Menu>
</template> </template>
</Dropdown> </Dropdown>
@ -76,6 +77,17 @@
<ExportImgDialog @close="exportImgDialogVisible = false"/> <ExportImgDialog @close="exportImgDialogVisible = false"/>
</Modal> </Modal>
<Modal
v-model:visible="exportPDFDialogVisible"
:footer="null"
centered
:closable="false"
:width="680"
destroyOnClose
>
<ExportPDFDialog @close="exportPDFDialogVisible = false"/>
</Modal>
<FullscreenSpin :loading="exporting" tip="正在导出..." /> <FullscreenSpin :loading="exporting" tip="正在导出..." />
</div> </div>
</template> </template>
@ -91,12 +103,14 @@ import useExport from '@/hooks/useExport'
import HotkeyDoc from './HotkeyDoc.vue' import HotkeyDoc from './HotkeyDoc.vue'
import ExportImgDialog from './ExportImgDialog.vue' import ExportImgDialog from './ExportImgDialog.vue'
import ExportPDFDialog from './ExportPDFDialog.vue'
export default defineComponent({ export default defineComponent({
name: 'editor-header', name: 'editor-header',
components: { components: {
HotkeyDoc, HotkeyDoc,
ExportImgDialog, ExportImgDialog,
ExportPDFDialog,
}, },
setup() { setup() {
const mainStore = useMainStore() const mainStore = useMainStore()
@ -117,6 +131,7 @@ export default defineComponent({
const hotkeyDrawerVisible = ref(false) const hotkeyDrawerVisible = ref(false)
const exportImgDialogVisible = ref(false) const exportImgDialogVisible = ref(false)
const exportPDFDialogVisible = ref(false)
const goIssues = () => { const goIssues = () => {
window.open('https://github.com/pipipi-pikachu/PPTist/issues') window.open('https://github.com/pipipi-pikachu/PPTist/issues')
@ -129,6 +144,7 @@ export default defineComponent({
showRuler, showRuler,
hotkeyDrawerVisible, hotkeyDrawerVisible,
exportImgDialogVisible, exportImgDialogVisible,
exportPDFDialogVisible,
exporting, exporting,
enterScreening, enterScreening,
enterScreeningFromStart, enterScreeningFromStart,