2026-05-09 04:25:30 +00:00
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
2026-05-09 04:25:30 +00:00
|
|
|
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())
|
|
|
|
|
}
|
2026-05-09 09:29:34 +08:00
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
export default {
|
2026-05-09 04:25:30 +00:00
|
|
|
name: 'PoliciesView',
|
|
|
|
|
emits: ['summary-change'],
|
|
|
|
|
setup(_, { emit }) {
|
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
|
|
|
|
|
const documentSearch = ref('')
|
2026-05-06 11:00:38 +08:00
|
|
|
const activeFolder = ref('差旅规范')
|
2026-05-09 04:25:30 +00:00
|
|
|
const folders = ref([])
|
|
|
|
|
const documents = ref([])
|
2026-05-06 11:00:38 +08:00
|
|
|
const selectedDocument = ref(null)
|
2026-05-09 04:25:30 +00:00
|
|
|
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)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
2026-05-09 04:25:30 +00:00
|
|
|
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
|
|
|
|
const uploadHint = computed(() =>
|
|
|
|
|
isAdmin.value
|
|
|
|
|
? '支持 PDF / Word / Excel / PPT / 图片 / 文本文件,重复同名文件将自动覆盖并升级版本'
|
|
|
|
|
: '当前账号只有查阅权限,上传、删除和修改仅管理员可用'
|
|
|
|
|
)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
2026-05-09 04:25:30 +00:00
|
|
|
const filteredFolders = computed(() => folders.value)
|
|
|
|
|
|
|
|
|
|
const filteredDocuments = computed(() => {
|
|
|
|
|
const key = documentSearch.value.trim()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
2026-05-09 04:25:30 +00:00
|
|
|
return documents.value.filter((doc) => {
|
2026-05-06 11:00:38 +08:00
|
|
|
const inFolder = activeFolder.value ? doc.folder === activeFolder.value : true
|
2026-05-09 04:25:30 +00:00
|
|
|
const matchesSearch = key ? doc.name.includes(key) : true
|
|
|
|
|
return inFolder && matchesSearch
|
2026-05-06 11:00:38 +08:00
|
|
|
})
|
2026-05-09 04:25:30 +00:00
|
|
|
})
|
2026-05-09 09:29:34 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
})
|
2026-05-09 04:25:30 +00:00
|
|
|
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 = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-09 09:29:34 +08:00
|
|
|
|
|
|
|
|
function changePageSize(size) {
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
pageSizeOpen.value = false
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 04:25:30 +00:00
|
|
|
function closePreview() {
|
|
|
|
|
selectedDocument.value = null
|
|
|
|
|
previewError.value = ''
|
|
|
|
|
currentPreviewPageIndex.value = 0
|
|
|
|
|
revokePreviewBlob()
|
|
|
|
|
destroyOnlyOfficeEditor()
|
|
|
|
|
onlyOfficeError.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectPreviewPage(index) {
|
|
|
|
|
currentPreviewPageIndex.value = index
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
watch(filteredDocuments, () => {
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
pageSizeOpen.value = false
|
2026-05-09 04:25:30 +00:00
|
|
|
|
|
|
|
|
if (selectedDocument.value && !filteredDocuments.value.some((doc) => doc.id === selectedDocument.value.id)) {
|
|
|
|
|
closePreview()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(activeFolder, () => {
|
|
|
|
|
closePreview()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadLibrary()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
revokePreviewBlob()
|
2026-05-09 09:29:34 +08:00
|
|
|
})
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
return {
|
|
|
|
|
activeFolder,
|
2026-05-09 04:25:30 +00:00
|
|
|
activePreviewPage,
|
|
|
|
|
changePageSize,
|
|
|
|
|
closePreview,
|
|
|
|
|
excelPreviewTable,
|
2026-05-09 09:29:34 +08:00
|
|
|
currentPage,
|
2026-05-09 04:25:30 +00:00
|
|
|
currentPreviewPageIndex,
|
|
|
|
|
deletingId,
|
|
|
|
|
documentSearch,
|
|
|
|
|
filteredFolders,
|
|
|
|
|
handleDelete,
|
|
|
|
|
handleDownload,
|
|
|
|
|
handleDrop,
|
|
|
|
|
handleFileInput,
|
|
|
|
|
isAdmin,
|
|
|
|
|
loading,
|
2026-05-09 09:29:34 +08:00
|
|
|
pageSize,
|
|
|
|
|
pageSizeOpen,
|
2026-05-09 04:25:30 +00:00
|
|
|
pageSizes,
|
|
|
|
|
onlyOfficeError,
|
|
|
|
|
onlyOfficeHostId,
|
|
|
|
|
onlyOfficeLoading,
|
|
|
|
|
previewMetaLine,
|
|
|
|
|
previewSecondaryMetaLine,
|
|
|
|
|
previewBlobUrl,
|
|
|
|
|
previewError,
|
|
|
|
|
previewLoading,
|
|
|
|
|
shouldUseOnlyOffice,
|
|
|
|
|
selectDocument,
|
|
|
|
|
selectPreviewPage,
|
|
|
|
|
selectedDocument,
|
2026-05-09 09:29:34 +08:00
|
|
|
totalCount,
|
|
|
|
|
totalPages,
|
2026-05-09 04:25:30 +00:00
|
|
|
triggerUpload,
|
|
|
|
|
uploadHint,
|
|
|
|
|
uploadInput,
|
|
|
|
|
uploading,
|
|
|
|
|
visibleDocuments
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|