import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { deleteKnowledgeDocument, fetchKnowledgeDocument, fetchKnowledgeDocumentBlob, fetchKnowledgeLibrary, fetchKnowledgeOnlyOfficeConfig, uploadKnowledgeDocument } from '../../services/knowledge.js' import { loadOnlyOfficeApi } from '../../services/onlyoffice.js' import { isManagerUser } from '../../utils/accessControl.js' import { buildExcelPreviewTable, buildPreviewMetaLine, buildPreviewSecondaryMetaLine } from './policiesPreviewFormatters.js' function triggerFileDownload(blob, filename) { const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = filename anchor.click() URL.revokeObjectURL(url) } const ONLYOFFICE_EXTENSIONS = new Set(['docx', 'xlsx', 'pptx']) function supportsOnlyOfficePreview(document) { return ONLYOFFICE_EXTENSIONS.has(String(document?.extension || '').toLowerCase()) } export default { name: 'PoliciesView', emits: ['summary-change'], setup(_, { emit }) { const { currentUser } = useSystemState() const { toast } = useToast() const documentSearch = ref('') const activeFolder = ref('差旅规范') const folders = ref([]) const documents = ref([]) const selectedDocument = ref(null) const pageSizeOpen = ref(false) const currentPage = ref(1) const pageSize = ref(10) const pageSizes = [10, 20, 50] const loading = ref(false) const uploadInput = ref(null) const uploading = ref(false) const deletingId = ref('') const previewLoading = ref(false) const previewBlobUrl = ref('') const previewError = ref('') const onlyOfficeLoading = ref(false) const onlyOfficeError = ref('') const onlyOfficeEditor = ref(null) const onlyOfficeHostId = ref('knowledge-onlyoffice-preview') const currentPreviewPageIndex = ref(0) const isAdmin = computed(() => isManagerUser(currentUser.value)) const uploadHint = computed(() => isAdmin.value ? '支持 PDF / Word / Excel / PPT / 图片 / 文本文件,重复同名文件将自动覆盖并升级版本' : '当前账号只有查阅权限,上传、删除和修改仅管理员可用' ) const filteredFolders = computed(() => folders.value) const filteredDocuments = computed(() => { const key = documentSearch.value.trim() return documents.value.filter((doc) => { const inFolder = activeFolder.value ? doc.folder === activeFolder.value : true const matchesSearch = key ? doc.name.includes(key) : true return inFolder && matchesSearch }) }) const totalCount = computed(() => filteredDocuments.value.length) const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value))) const visibleDocuments = computed(() => { const start = (currentPage.value - 1) * pageSize.value return filteredDocuments.value.slice(start, start + pageSize.value) }) const activePreviewPage = computed(() => { const pages = selectedDocument.value?.previewPages || [] return pages[currentPreviewPageIndex.value] || pages[0] || null }) const previewMetaLine = computed(() => buildPreviewMetaLine(selectedDocument.value)) const previewSecondaryMetaLine = computed(() => buildPreviewSecondaryMetaLine(selectedDocument.value, activePreviewPage.value) ) const shouldUseOnlyOffice = computed(() => supportsOnlyOfficePreview(selectedDocument.value)) const excelPreviewTable = computed(() => selectedDocument.value?.previewKind === 'table' ? buildExcelPreviewTable(activePreviewPage.value) : { headers: [], rows: [] } ) function revokePreviewBlob() { if (previewBlobUrl.value) { URL.revokeObjectURL(previewBlobUrl.value) previewBlobUrl.value = '' } } function destroyOnlyOfficeEditor() { if (onlyOfficeEditor.value?.destroyEditor) { onlyOfficeEditor.value.destroyEditor() } onlyOfficeEditor.value = null } async function mountOnlyOfficeEditor(documentId) { onlyOfficeLoading.value = true onlyOfficeError.value = '' destroyOnlyOfficeEditor() try { const payload = await fetchKnowledgeOnlyOfficeConfig(documentId) await loadOnlyOfficeApi(payload.documentServerUrl) await nextTick() if (!window.DocsAPI?.DocEditor) { throw new Error('ONLYOFFICE 编辑器未正确加载。') } onlyOfficeHostId.value = `knowledge-onlyoffice-preview-${documentId}` await nextTick() onlyOfficeEditor.value = new window.DocsAPI.DocEditor(onlyOfficeHostId.value, payload.config) } catch (error) { onlyOfficeError.value = error.message || 'ONLYOFFICE 预览加载失败。' } finally { onlyOfficeLoading.value = false } } async function loadLibrary(options = {}) { loading.value = true try { const payload = await fetchKnowledgeLibrary() folders.value = payload.folders || [] documents.value = payload.documents || [] emit('summary-change', { totalDocuments: documents.value.length }) const activeExists = folders.value.some((folder) => folder.name === activeFolder.value) if (!activeExists) { activeFolder.value = folders.value[0]?.name || '' } if (options.preserveSelection && selectedDocument.value?.id) { const exists = documents.value.some((doc) => doc.id === selectedDocument.value.id) if (!exists) { selectedDocument.value = null revokePreviewBlob() } } } catch (error) { emit('summary-change', { totalDocuments: 0 }) toast(error.message || '知识库加载失败。') } finally { loading.value = false } } async function selectDocument(documentId) { previewLoading.value = true previewError.value = '' onlyOfficeError.value = '' revokePreviewBlob() destroyOnlyOfficeEditor() try { const payload = await fetchKnowledgeDocument(documentId) selectedDocument.value = payload currentPreviewPageIndex.value = 0 if (supportsOnlyOfficePreview(payload)) { await mountOnlyOfficeEditor(documentId) } else if (payload.previewKind === 'pdf' || payload.previewKind === 'image') { const blob = await fetchKnowledgeDocumentBlob(documentId, 'inline') previewBlobUrl.value = URL.createObjectURL(blob) } } catch (error) { previewError.value = error.message || '预览加载失败。' toast(previewError.value) } finally { previewLoading.value = false } } async function handleDownload(document) { try { const blob = await fetchKnowledgeDocumentBlob(document.id, 'attachment') triggerFileDownload(blob, document.name) } catch (error) { toast(error.message || '下载失败。') } } function triggerUpload() { if (!isAdmin.value || uploading.value) { return } uploadInput.value?.click() } async function uploadFiles(fileList) { const files = Array.from(fileList || []).filter(Boolean) if (!files.length || !activeFolder.value || !isAdmin.value) { return } uploading.value = true try { let latestDocumentId = '' for (const file of files) { const payload = await uploadKnowledgeDocument({ folder: activeFolder.value, file }) latestDocumentId = payload.id } await loadLibrary({ preserveSelection: true }) toast(files.length > 1 ? `已上传 ${files.length} 个知识库文件。` : '知识库文件已上传。') if (latestDocumentId) { await selectDocument(latestDocumentId) } } catch (error) { toast(error.message || '上传失败。') } finally { uploading.value = false if (uploadInput.value) { uploadInput.value.value = '' } } } async function handleFileInput(event) { await uploadFiles(event.target.files) } async function handleDrop(event) { if (!isAdmin.value) { return } await uploadFiles(event.dataTransfer?.files) } async function handleDelete(document) { if (!isAdmin.value || deletingId.value) { return } const confirmed = window.confirm(`确认删除文件“${document.name}”吗?`) if (!confirmed) { return } deletingId.value = document.id try { await deleteKnowledgeDocument(document.id) if (selectedDocument.value?.id === document.id) { selectedDocument.value = null revokePreviewBlob() } await loadLibrary() toast('知识库文件已删除。') } catch (error) { toast(error.message || '删除失败。') } finally { deletingId.value = '' } } function changePageSize(size) { pageSize.value = size pageSizeOpen.value = false currentPage.value = 1 } function closePreview() { selectedDocument.value = null previewError.value = '' currentPreviewPageIndex.value = 0 revokePreviewBlob() destroyOnlyOfficeEditor() onlyOfficeError.value = '' } function selectPreviewPage(index) { currentPreviewPageIndex.value = index } watch(filteredDocuments, () => { currentPage.value = 1 pageSizeOpen.value = false if (selectedDocument.value && !filteredDocuments.value.some((doc) => doc.id === selectedDocument.value.id)) { closePreview() } }) watch(activeFolder, () => { closePreview() }) onMounted(() => { loadLibrary() }) onBeforeUnmount(() => { revokePreviewBlob() }) return { activeFolder, activePreviewPage, changePageSize, closePreview, excelPreviewTable, currentPage, currentPreviewPageIndex, deletingId, documentSearch, filteredFolders, handleDelete, handleDownload, handleDrop, handleFileInput, isAdmin, loading, pageSize, pageSizeOpen, pageSizes, onlyOfficeError, onlyOfficeHostId, onlyOfficeLoading, previewMetaLine, previewSecondaryMetaLine, previewBlobUrl, previewError, previewLoading, shouldUseOnlyOffice, selectDocument, selectPreviewPage, selectedDocument, totalCount, totalPages, triggerUpload, uploadHint, uploadInput, uploading, visibleDocuments } } }