Prechádzať zdrojové kódy

add: 导入文档;删除页面

wzl 1 rok pred
rodič
commit
b255a7f7e2

+ 25 - 6
packages/core/src/index.js

@@ -2810,13 +2810,32 @@ class ComPDFKitViewer {
     for (let i = 0; i < operationList.length; i++) {
       const op = operationList[i]
       if (op.type === 'insert') {
-        const result = await this.messageHandler.sendWithPromise('InsertPage', {
-          doc: this.doc,
-          pageIndex: op.index,
-          width: op.size.width,
-          height: op.size.height
+        if (op.file) {
+          const res = await this.messageHandler.sendWithPromise('ImportPagesAtIndex', {
+            doc: this.doc,
+            file: op.file,
+            range: op.range,
+            index: op.index
+          })
+          console.log(res)
+        } else {
+          const res = await this.messageHandler.sendWithPromise('InsertPage', {
+            doc: this.doc,
+            index: op.index,
+            width: op.size.width,
+            height: op.size.height
+          })
+          console.log(res)
+        }
+      }
+      if (op.type === 'delete') {
+        op.selectedIndexArray.forEach(async index => {
+          const res = await this.messageHandler.sendWithPromise('RemovePage', {
+            doc: this.doc,
+            index
+          })
+          console.log(res)
         })
-        console.log(result)
       }
     }
 

+ 62 - 3
packages/core/src/worker/compdfkit_worker.js

@@ -1044,11 +1044,70 @@ class CPDFWorker {
     })
 
     messageHandler.on('InsertPage', (data) => {
-      const { doc, pageIndex, width, height } = data
-      return Module._InsertPage(doc, pageIndex, width, height)
+      const { doc, index, width, height } = data
+      return Module._InsertPage(doc, index, width, height)
     })
-  }
 
+    messageHandler.on('ImportPagesAtIndex', async (data) => {
+      const { doc, file, range, index } = data
+
+      const buffer = await convertFileToBuffer(file)
+      const importDoc = Module._InitDocument()
+      ComPDFKitJS.opened_files = []
+      ComPDFKitJS.opened_files[0] = buffer
+      // const password = stringToNewUTF8(password)
+      Module._LoadDocumentByStream(importDoc, 0, buffer.length)
+      const pagesCount = Module._GetPageCount(importDoc)
+      let importRange
+      if (range === 'all') {
+        importRange = '1-' + pagesCount
+        // importRange = ''
+      } else if (range === 'odd') {
+        importRange = Array.from({ length: pagesCount }, (_, i) => i + 1).filter(num => num % 2 !== 0).join(",")
+      } else if (range === 'even') {
+        importRange = Array.from({ length: pagesCount }, (_, i) => i + 1).filter(num => num % 2 === 0).join(",")
+      } else {
+        const max = pagesCount
+        let str = range.replace(/\s/g, '')
+        let parts = str.split(',')
+        let result = []
+
+        parts.forEach(part => {
+          if (part.includes('-')) {
+            let [start, end] = part.split('-').map(num => parseInt(num))
+            if (start > max) {
+              return
+            } else if (end > max) {
+              end = max
+            }
+            result.push((start === max) ? start.toString() : `${start}-${end}`)
+          } else {
+            let num = parseInt(part)
+            if (num <= max) {
+              result.push(num.toString())
+            }
+          }
+        })
+        importRange = result.join(',')
+      }
+      console.log('导入文档的页面范围:' + importRange)
+      if (!importRange) return 0
+
+      importRange = stringToNewUTF8(importRange)
+
+      return Module._ImportPagesAtIndex(
+        doc,
+        importDoc,
+        importRange,
+        index
+      )
+    })
+
+    messageHandler.on('RemovePage', (data) => {
+      const { doc, index } = data
+      return Module._RemovePage(doc, index)
+    })
+  }
 }
 
 async function initDoc() {

+ 65 - 0
packages/webview/src/components/Dialogs/DeletePageDialog.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="delete-page-popup" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName" :close="false">
+      <!-- content -->
+      <Warning />
+      <p>{{ $t('documentEditor.deleteConfirmText') }}{{ locale === 'en' ? '?' : '' }}</p>
+
+      <!-- footer -->
+      <template #footer>
+        <div class="rect-button white" @click="closeDialog">{{ $t('cancel') }}</div>
+        <div class="rect-button blue" @click="handleDelete">{{ $t('documentEditor.delete') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useI18n } from 'vue-i18n'
+const { locale } = useI18n()
+
+const emits = defineEmits(['deletePage'])
+
+const useViewer = useViewerStore()
+
+const dialogName = 'deletePageDialog'
+const show = computed(() => useViewer.isElementOpen(dialogName))
+
+const closeDialog = () => {
+  useViewer.closeElement(dialogName)
+}
+
+const handleDelete = async () => {
+  emits('deletePage')
+  closeDialog()
+}
+</script>
+
+<style lang="scss">
+.delete-page-popup {
+
+  .dialog-container {
+    width: 381px;
+
+    main {
+      margin-top: 0;
+      margin-bottom: 20px;
+      display: flex;
+      align-items: center;
+
+      svg {
+        margin-right: 8px;
+        width: 20px;
+        height: 20px;
+      }
+
+      p {
+        font-size: 16px;
+        line-height: 24px;
+      }
+    }
+  }
+}
+</style>

+ 90 - 11
packages/webview/src/components/Dialogs/InsertPageSettingDialog.vue

@@ -29,13 +29,18 @@
           </div>
           <div v-if="type === 'pdf'" class="addition from-pdf">
             <span>{{ $t('documentEditor.dialog.pageRange') }}</span>
-            <select name="inputFileRange" v-model="inputFileRange">
-              <option value="all">{{ $t('documentEditor.dialog.allPages') }}</option>
-              <option value="odd">{{ $t('documentEditor.dialog.oddPage') }}</option>
-              <option value="even">{{ $t('documentEditor.dialog.evenPage') }}</option>
-              <option value="custom">{{ $t('documentEditor.dialog.customRange') }}</option>
-            </select>
-            <ArrowDown />
+            <div>
+              <div class="addition">
+                <select name="inputFileRange" v-model="inputFileRange">
+                  <option value="all">{{ $t('documentEditor.dialog.allPages') }}</option>
+                  <option value="odd">{{ $t('documentEditor.dialog.oddPage') }}</option>
+                  <option value="even">{{ $t('documentEditor.dialog.evenPage') }}</option>
+                  <option value="custom">{{ $t('documentEditor.dialog.customRange') }}</option>
+                </select>
+                <ArrowDown />
+              </div>
+              <input v-if="inputFileRange === 'custom'" type="text" v-model="customRange" :placeholder="$t('documentEditor.dialog.pageTip')" @blur="validateCustomRange">
+            </div>
           </div>
         </div>
       </div>
@@ -73,7 +78,8 @@
       <!-- footer -->
       <template #footer>
         <div class="rect-button white" @click="closeDialog">{{ $t('cancel') }}</div>
-        <div class="rect-button blue" :class="{ 'disabled': type === 'pdf' && !inputFile }" @click="confirm">{{ $t('documentEditor.insert') }}</div>
+        <div class="rect-button blue" :class="{ 'disabled': type === 'pdf' && (!inputFile || (inputFileRange === 'custom' && !validCustomRange)) }" @click="confirm">{{ $t('documentEditor.insert') }}</div>
+        {{ customRange }}
       </template>
     </Dialog>
   </div>
@@ -84,7 +90,6 @@ import { ref, computed, watch, h } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import core from '@/core'
-import MessageError from '@/assets/icons/icon-message-error.svg'
 
 const props = defineProps([ 'selectedPageIndex', 'totalPages' ])
 const emits = defineEmits(['insertPage'])
@@ -100,6 +105,8 @@ const customPageSize = ref('A3')
 let inputFile = null
 const fileName = ref('')
 const inputFileRange = ref('all')
+const customRange = ref('')
+const validCustomRange = ref('')
 
 const place = ref('first')
 const targetIndex = ref(1)
@@ -123,6 +130,7 @@ const closeDialog = () => {
   targetIndex.value = 1
   targetPlace.value = 0
 }
+
 // 确认
 const confirm = async () => {
   const data = {
@@ -159,6 +167,8 @@ const confirm = async () => {
 
   if (type.value === 'pdf' && inputFile) {
     data.name = fileName.value
+    data.file = inputFile
+    data.range = inputFileRange.value === 'custom' ? customRange.value : inputFileRange.value
   }
 
   if (place.value === 'custom') {
@@ -192,6 +202,57 @@ const validatePageInput = (e) => {
 const onblurPageInput = () => {
   if (!targetIndex.value) targetIndex.value = 1
 }
+
+// 输入自定义页面范围校验 格式化
+const validateCustomRange = () => {
+  validCustomRange.value = formatText(customRange.value)
+  customRange.value = validCustomRange.value
+}
+function formatText(inputText) {
+  const text = inputText.replace(/\s/g, '')
+  const matches = text.match(/\d+-\d+|\d+|\d+/g)
+
+  if (matches) {
+    const sortedNums = matches
+      .flatMap(match => {
+        if (match.includes('-')) {
+          const [start, end] = match.split('-')
+          const smallest = Math.min(start, end)
+          const largest = Math.max(start, end)
+          return Array.from({ length: largest - smallest + 1 }, (_, i) =>
+            String(Number(smallest) + i)
+          )
+        } else {
+          return match
+        }
+      })
+      .sort((a, b) => a - b)
+
+    const formattedText = []
+
+    let start = sortedNums[0]
+    let prev = sortedNums[0]
+
+    for (let i = 1; i < sortedNums.length; i++) {
+      const current = sortedNums[i]
+
+      const prevNum = Number(prev)
+      const currentNum = Number(current)
+
+      if (currentNum - prevNum > 1) {
+        formattedText.push(formatRange(start, prev))
+        start = current
+      }
+      prev = current
+    }
+    formattedText.push(formatRange(start, prev))
+    return formattedText.join(', ')
+  }
+  return '1'
+}
+function formatRange(start, end) {
+  return (start === end) ? start.toString() : `${start}-${end}`
+}
 </script>
 
 <style lang="scss">
@@ -270,6 +331,8 @@ const onblurPageInput = () => {
         color: #999;
         .uploaded {
           color: var(--c-text);
+          overflow: hidden;
+          text-overflow: ellipsis;
         }
       }
 
@@ -281,8 +344,24 @@ const onblurPageInput = () => {
         cursor: pointer;
       }
 
-      &.from-pdf span {
-        margin-right: 8px;
+      &.from-pdf {
+        align-items: baseline;
+
+        span {
+          margin-right: 8px;
+        }
+
+        > div {
+          flex: 1;
+
+          input[type="text"] {
+            margin-top: 4px;
+          }
+        }
+      }
+
+      input[type="text"]:focus {
+        border-color: #0078D7;
       }
     }
 

+ 47 - 15
packages/webview/src/components/DocumentEditorContainer/DocumentEditorContainer.vue

@@ -6,7 +6,7 @@
         <InsertPage />
         <span>{{ $t('documentEditor.insert') }}</span>
       </Button>
-      <Button class="with-text" :class="{ disabled: !selectedPageList.length }" @click="">
+      <Button class="with-text" :class="{ disabled: !selectedPageList.length || selectedPageList.length == pageList.length  }" @click="openDeletePageDialog">
         <DeletePage />
         <span>{{ $t('documentEditor.delete') }}</span>
       </Button>
@@ -56,6 +56,7 @@
 
   </div>
   <InsertPageSettingDialog @insertPage="handleInsertPage" :selectedPageIndex="selectedPageList.length === 1 ? selectedPageList[0] + 1 : -1" :totalPages="pageList.length" />
+  <DeletePageDialog @deletePage="handleDeletePage" />
 </template>
 
 <script setup>
@@ -70,7 +71,7 @@ const useDocument = useDocumentStore()
 let originalPages = [] // 操作前的全部页面
 const data = reactive({
   pageList: [], // 展示的所有页面
-  selectedPageList: [], // 选中的页面列表
+  selectedPageList: [], // 选中的页面index列表
   newPageCount: 0, // 新插入的空白页面序号(递增)
 })
 let { pageList, selectedPageList, newPageCount } = data
@@ -103,33 +104,45 @@ const getAllPages = (data) => {
 }
 core.addEvent('getAllPages', getAllPages)
 
-// 插入页面选项弹窗
-const openInsertPageDialog = () => {
-  useViewer.openElement('insertPageSettingDialog')
+// 打开插入页面选项弹窗
+const openDeletePageDialog = () => {
+  useViewer.openElement('deletePageDialog')
 }
 
 // 插入页面
 const handleInsertPage = (data) => {
-  const insertPageData = {
+  // 添加数据到用于展示的pageList
+  const pageListData = {
     name: data.name ? data.name : 'New Page ' + ++newPageCount,
-    type: data.type,
-    size: data.size || null
+    type: data.type
   }
 
-  if (data.type === 'blank' && !data.size) {
+  if (data.type === 'blank') {
     const adjacentPage = pageList[data.place] || pageList[data.place - 1]
-    insertPageData.size = {
+    pageListData.size = data.size || {
       width: adjacentPage.size.width,
       height: adjacentPage.size.height
     }
+  } else if (data.type === 'pdf') {
+    pageListData.file = data.file
+    pageListData.range = data.range
   }
+  // console.log('pageListData: ', pageListData)
+  pageList.splice(data.place, 0, pageListData)
 
-  pageList.splice(data.place, 0, insertPageData)
-  useDocument.setDocEditorOperationList({
+  // 要传到core处理的数据
+  let insertData = {
     type: 'insert',
-    index: data.place,
-    size: insertPageData.size
-  })
+    index: data.place
+  }
+  if (data.type === 'blank') {
+    insertData.size = pageListData.size
+  } else if (data.type === 'pdf') {
+    insertData.file = pageListData.file
+    insertData.range = pageListData.range
+  }
+  // console.log('insertData: ', insertData)
+  useDocument.setDocEditorOperationList(insertData)
 }
 
 // 选中页面
@@ -149,6 +162,25 @@ const selectAll = () => {
   }
 }
 
+// 打开删除页面弹窗
+const openInsertPageDialog = () => {
+  useViewer.openElement('insertPageSettingDialog')
+}
+
+// 删除页面
+const handleDeletePage = () => {
+  selectedPageList.sort((a, b) => b - a)
+  selectedPageList.forEach(index => {
+    pageList.splice(index, 1)
+  })
+
+  useDocument.setDocEditorOperationList({
+    type: 'delete',
+    selectedIndexArray: Array.from(selectedPageList)
+  })
+  selectedPageList.length = 0
+}
+
 onUnmounted(() => {
   core.removeEvent('getPages', getPages)
   core.removeEvent('getAllPages', getAllPages)

+ 5 - 4
packages/webview/src/components/DocumentEditorHeader/DocumentEditorHeader.vue

@@ -4,16 +4,17 @@
     <div class="rect-button white" @click="exit">{{ $t('cancel') }}</div>
     <div class="right-container">
       <div class="rect-button blue" :class="{ 'disabled': !docEditorOperationList.length }" @click="save">{{ $t('documentEditor.save') }}</div>
-      <div class="rect-button blue" @click="saveAs">{{ $t('documentEditor.saveAs') }}</div>
+      <div class="rect-button blue" :class="{ 'disabled': !docEditorOperationList.length }" @click="saveFileAs">{{ $t('documentEditor.saveAs') }}</div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { computed } from 'vue'
+import { computed, watch } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import core from '@/core'
+import { saveAs } from 'file-saver';
 
 const useViewer = useViewerStore()
 const useDocument = useDocumentStore()
@@ -33,7 +34,7 @@ const save = async () => {
   useViewer.setUpload(false)
   useViewer.setUploadLoading(true)
 
-  const newUrl = await core.saveDocumentEdit(JSON.parse(JSON.stringify(docEditorOperationList.value)))
+  const { newUrl } = await core.saveDocumentEdit(docEditorOperationList.value)
   
   useDocument.setCurrentPdfData(newUrl)
   useDocument.setDocEditorOperationList('reset')
@@ -44,7 +45,7 @@ const save = async () => {
   useViewer.setUpload(true)
 }
 
-const saveAs = () => {}
+const saveFileAs = async () => {}
 </script>
 
 <style lang="scss">

+ 4 - 2
packages/webview/src/stores/modules/viewer.js

@@ -35,7 +35,8 @@ export const useViewerStore = defineStore({
       printSettingDialog: false,
       editTextPanel: false,
       preventDialog: false,
-      insertPageSettingDialog: false
+      insertPageSettingDialog: false,
+      deletePageDialog: false
     },
     activeElementsTab: {
       leftPanelTab: 'THUMBS',
@@ -356,7 +357,8 @@ export const useViewerStore = defineStore({
         downloadSettingDialog: false,
         printSettingDialog: false,
         editTextPanel: false,
-        insertPageSettingDialog: false
+        insertPageSettingDialog: false,
+        deletePageDialog: false
       },
       this.activeElementsTab = {
         leftPanelTab: 'THUMBS',