file预览窗口增加支持修改后保存

This commit is contained in:
jingrow 2026-05-28 03:19:57 +08:00
parent 2d4d38ad8a
commit 8b24e56c0f
4 changed files with 182 additions and 1 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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()

View File

@ -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>