mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 支持打印 / 导出 PDF
This commit is contained in:
parent
47450b7b91
commit
4de6d5234e
@ -22,11 +22,12 @@ npm run serve
|
|||||||
|
|
||||||
|
|
||||||
# 📚 功能列表
|
# 📚 功能列表
|
||||||
### 通用功能
|
### 基础功能
|
||||||
- 历史记录(撤销、重做)
|
- 历史记录(撤销、重做)
|
||||||
- 快捷键
|
- 快捷键
|
||||||
- 右键菜单
|
- 右键菜单
|
||||||
- 导出本地文件(PPTX、JSON、图片)
|
- 导出本地文件(PPTX、JSON、图片、PDF)
|
||||||
|
- 打印
|
||||||
### 幻灯片页面编辑
|
### 幻灯片页面编辑
|
||||||
- 页面添加、删除
|
- 页面添加、删除
|
||||||
- 页面顺序调整
|
- 页面顺序调整
|
||||||
|
81
src/utils/print.ts
Normal file
81
src/utils/print.ts
Normal 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)
|
||||||
|
}
|
165
src/views/Editor/EditorHeader/ExportPDFDialog.vue
Normal file
165
src/views/Editor/EditorHeader/ExportPDFDialog.vue
Normal 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>
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user