file预览窗口增加支持修改后保存
This commit is contained in:
parent
2d4d38ad8a
commit
8b24e56c0f
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="docx-preview">
|
||||
<DocxEditor
|
||||
ref="editorRef"
|
||||
:document-buffer="buffer"
|
||||
:i18n="zhCN"
|
||||
:show-toolbar="true"
|
||||
@ -22,6 +23,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { DocxEditor } from '@eigenpal/docx-editor-vue'
|
||||
import type { DocxEditorRef } from '@eigenpal/docx-editor-vue'
|
||||
import { zhCN } from '@eigenpal/docx-editor-i18n'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import '@eigenpal/docx-editor-vue/styles.css'
|
||||
@ -37,6 +39,8 @@ const emit = defineEmits<{
|
||||
error: [msg: string]
|
||||
}>()
|
||||
|
||||
const editorRef = ref<DocxEditorRef | null>(null)
|
||||
|
||||
const buffer = ref<ArrayBuffer | null>(null)
|
||||
const hasError = ref(false)
|
||||
|
||||
@ -81,6 +85,22 @@ watch(() => props.fileUrl, (newUrl) => {
|
||||
onBeforeUnmount(() => {
|
||||
buffer.value = null
|
||||
})
|
||||
|
||||
/** Expose save method so parent can trigger document save */
|
||||
async function saveFile(): Promise<Blob | null> {
|
||||
if (!editorRef.value) return null
|
||||
try {
|
||||
const arrayBuffer = await editorRef.value.save()
|
||||
if (!arrayBuffer) return null
|
||||
return new Blob([arrayBuffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ saveFile })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -163,6 +163,69 @@ onBeforeUnmount(() => {
|
||||
univer = null
|
||||
univerAPI = null
|
||||
})
|
||||
|
||||
/** Expose save method so parent can trigger save */
|
||||
async function saveFile(): Promise<Blob | null> {
|
||||
if (!univerAPI) return null
|
||||
try {
|
||||
// Get the current workbook via facade API
|
||||
const fWorkbook = (univerAPI as any).getActiveWorkbook()
|
||||
if (!fWorkbook) return null
|
||||
|
||||
const fSheet = fWorkbook.getActiveSheet()
|
||||
if (!fSheet) return null
|
||||
|
||||
// Extract data from the sheet — scan up to 2000 rows x 200 cols
|
||||
const MAX_ROWS = 2_000
|
||||
const MAX_COLS = 200
|
||||
const range = fSheet.getRange(0, 0, MAX_ROWS, MAX_COLS)
|
||||
const rawData: any[][] = range.getValues()
|
||||
|
||||
// Convert ICellData → primitive values and trim trailing empty rows
|
||||
const data: any[][] = []
|
||||
for (let r = 0; r < rawData.length; r++) {
|
||||
const row = rawData[r]
|
||||
const values: any[] = []
|
||||
let rowHasData = false
|
||||
for (let c = 0; c < MAX_COLS; c++) {
|
||||
const cell = row[c]
|
||||
const v = cell?.v ?? ''
|
||||
values.push(v)
|
||||
if (v !== '' && v !== null && v !== undefined) rowHasData = true
|
||||
}
|
||||
if (rowHasData) {
|
||||
data.push(values)
|
||||
} else {
|
||||
break // stop at first completely empty row
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length === 0) return null
|
||||
|
||||
// Trim trailing empty columns
|
||||
const maxCols = Math.max(...data.map(r => {
|
||||
let i = r.length
|
||||
while (i > 0 && r[i - 1] === '') i--
|
||||
return i
|
||||
}))
|
||||
for (const row of data) row.length = maxCols
|
||||
|
||||
// Build XLSX workbook with SheetJS
|
||||
const wb = XLSX.utils.book_new()
|
||||
const ws = XLSX.utils.aoa_to_sheet(data)
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
|
||||
const out = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
|
||||
|
||||
return new Blob([out], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[SpreadsheetPreview] save error:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ saveFile })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -156,6 +156,7 @@
|
||||
@close="closePreview"
|
||||
@download="handleDownload"
|
||||
@delete="deleteSingle"
|
||||
@saved="onFileSaved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -518,6 +519,10 @@ function onUploaded() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onFileSaved() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// ===== 拖拽面板 =====
|
||||
function startResize(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
<!-- DOCX 文档预览 -->
|
||||
<div v-else-if="isDocxFile" class="visual-document-wrapper">
|
||||
<DocumentPreview
|
||||
ref="documentPreviewRef"
|
||||
:file-url="fileDownloadUrl"
|
||||
:file-name="row?.file_name || ''"
|
||||
@loaded="onDocxLoaded"
|
||||
@ -37,6 +38,7 @@
|
||||
<!-- 电子表格预览 -->
|
||||
<div v-else-if="isSpreadsheetFile" class="visual-spreadsheet-wrapper">
|
||||
<SpreadsheetPreview
|
||||
ref="spreadsheetPreviewRef"
|
||||
:file-url="fileDownloadUrl"
|
||||
:file-name="row?.file_name || ''"
|
||||
@loaded="onSpreadsheetLoaded"
|
||||
@ -120,6 +122,10 @@
|
||||
|
||||
<!-- ⑥ 底部操作栏 -->
|
||||
<div class="preview-actions">
|
||||
<button v-if="canSave" class="action-btn action-save" :disabled="saving" @click="handleSave" :title="t('Save')">
|
||||
<Icon :icon="saving ? 'tabler:loader-2' : 'tabler:check'" :size="16" :class="{ 'spin-icon': saving }" />
|
||||
<span>{{ saving ? t('Saving...') : t('Save') }}</span>
|
||||
</button>
|
||||
<button class="action-btn action-primary" @click="$emit('download', row)" :title="t('Download')">
|
||||
<Icon icon="tabler:download" :size="16" />
|
||||
<span>{{ t('Download') }}</span>
|
||||
@ -143,6 +149,7 @@
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { uploadFileToJingrow } from '@/shared/api/common'
|
||||
|
||||
const SpreadsheetPreview = defineAsyncComponent(() => import('./SpreadsheetPreview.vue'))
|
||||
const DocumentPreview = defineAsyncComponent(() => import('./DocumentPreview.vue'))
|
||||
@ -151,7 +158,7 @@ const props = defineProps<{
|
||||
row: any | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close', 'download', 'delete'])
|
||||
const emit = defineEmits(['close', 'download', 'delete', 'saved'])
|
||||
|
||||
const DOCUMENT_EXTENSIONS = ['doc', 'docx']
|
||||
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico']
|
||||
@ -164,6 +171,14 @@ const isSpreadsheetFile = computed(() => SPREADSHEET_EXTENSIONS.includes((props.
|
||||
|
||||
const canPreview = computed(() => isImageFile.value || isPdfFile.value || isDocxFile.value || isSpreadsheetFile.value)
|
||||
|
||||
const EDITABLE_EXTENSIONS = ['docx', 'xls', 'xlsx', 'csv']
|
||||
|
||||
const documentPreviewRef = ref<any>(null)
|
||||
const spreadsheetPreviewRef = ref<any>(null)
|
||||
|
||||
const canSave = computed(() => EDITABLE_EXTENSIONS.includes((props.row?.file_type || '').toLowerCase()))
|
||||
const saving = ref(false)
|
||||
|
||||
const showInfo = ref(false)
|
||||
|
||||
// Auto-show info for non-previewable files (e.g. zip, code),
|
||||
@ -261,6 +276,44 @@ function onDocxLoaded() {
|
||||
function onDocxError(msg: string) {
|
||||
console.warn('[FilePreview] document preview error:', msg)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const ext = (props.row?.file_type || '').toLowerCase()
|
||||
let blob: Blob | null = null
|
||||
|
||||
if (ext === 'docx') {
|
||||
blob = (await documentPreviewRef.value?.saveFile()) ?? null
|
||||
} else if (['xls', 'xlsx', 'csv'].includes(ext)) {
|
||||
blob = (await spreadsheetPreviewRef.value?.saveFile()) ?? null
|
||||
}
|
||||
|
||||
if (!blob) {
|
||||
console.warn('[FilePreview] save: no data returned')
|
||||
return
|
||||
}
|
||||
|
||||
const fileName = props.row?.file_name || `untitled.${ext}`
|
||||
const file = new File([blob], fileName, { type: blob.type })
|
||||
const result = await uploadFileToJingrow(
|
||||
file,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
props.row?.folder || undefined,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
emit('saved')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[FilePreview] save failed:', e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -621,4 +674,44 @@ function onDocxError(msg: string) {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.action-save {
|
||||
background: #e6f8f0 !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #0d684b !important;
|
||||
}
|
||||
|
||||
.action-save:hover:not(:disabled) {
|
||||
background: #dcfce7 !important;
|
||||
border-color: #1fc76f !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #166534 !important;
|
||||
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
|
||||
}
|
||||
|
||||
.action-save:active:not(:disabled) {
|
||||
background: #1fc76f !important;
|
||||
border-color: #1fc76f !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2) !important;
|
||||
}
|
||||
|
||||
.action-save:disabled {
|
||||
background: #f1f5f9 !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
color: #94a3b8 !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.spin-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user