2026-05-20 14:21:56 +08:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
|
|
|
|
|
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
2026-05-17 08:38:41 +00:00
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
deleteKnowledgeDocument,
|
|
|
|
|
|
fetchKnowledgeDocument,
|
|
|
|
|
|
fetchKnowledgeDocumentBlob,
|
|
|
|
|
|
fetchKnowledgeLibrary,
|
|
|
|
|
|
fetchKnowledgeOnlyOfficeConfig,
|
|
|
|
|
|
syncKnowledgeLibrary,
|
|
|
|
|
|
uploadKnowledgeDocument
|
|
|
|
|
|
} from '../../services/knowledge.js'
|
2026-05-09 05:59:46 +00:00
|
|
|
|
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
|
|
|
|
|
import { isManagerUser } from '../../utils/accessControl.js'
|
2026-05-17 08:38:41 +00:00
|
|
|
|
import {
|
|
|
|
|
|
buildExcelPreviewTable,
|
|
|
|
|
|
buildPreviewMetaLine,
|
|
|
|
|
|
buildPreviewSecondaryMetaLine
|
|
|
|
|
|
} from './policiesPreviewFormatters.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
canUseOnlyOfficePreview,
|
|
|
|
|
|
resolveKnowledgePreviewMode,
|
|
|
|
|
|
shouldRenderOnlyOfficeHost,
|
|
|
|
|
|
shouldRenderOnlyOfficePreview
|
|
|
|
|
|
} from './knowledgePreviewMode.js'
|
|
|
|
|
|
import { resolveKnowledgePreviewLayoutState } from './knowledgePreviewLayout.js'
|
2026-05-22 16:00:19 +08:00
|
|
|
|
import {
|
|
|
|
|
|
resolveInitialKnowledgeFolder,
|
|
|
|
|
|
resolveKnowledgeFolderIcon
|
|
|
|
|
|
} from './knowledgeFolderSelection.js'
|
2026-05-17 08:38:41 +00:00
|
|
|
|
import { buildOnlyOfficePreviewConfig } from './onlyOfficePreviewConfig.js'
|
|
|
|
|
|
|
|
|
|
|
|
const KNOWLEDGE_POLL_INTERVAL_MS = 5000
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let bodyOverflowSnapshot = ''
|
|
|
|
|
|
let bodyOverscrollBehaviorSnapshot = ''
|
|
|
|
|
|
let libraryPollTimer = 0
|
|
|
|
|
|
|
|
|
|
|
|
function setBodyScrollLocked(isLocked) {
|
|
|
|
|
|
if (typeof document === 'undefined') {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { body } = document
|
|
|
|
|
|
if (!body) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isLocked) {
|
|
|
|
|
|
if (body.dataset.knowledgePreviewLocked === 'true') {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bodyOverflowSnapshot = body.style.overflow
|
|
|
|
|
|
bodyOverscrollBehaviorSnapshot = body.style.overscrollBehavior
|
|
|
|
|
|
body.style.overflow = 'hidden'
|
|
|
|
|
|
body.style.overscrollBehavior = 'contain'
|
|
|
|
|
|
body.dataset.knowledgePreviewLocked = 'true'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (body.dataset.knowledgePreviewLocked !== 'true') {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body.style.overflow = bodyOverflowSnapshot
|
|
|
|
|
|
body.style.overscrollBehavior = bodyOverscrollBehaviorSnapshot
|
|
|
|
|
|
delete body.dataset.knowledgePreviewLocked
|
|
|
|
|
|
bodyOverflowSnapshot = ''
|
|
|
|
|
|
bodyOverscrollBehaviorSnapshot = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
2026-05-20 14:21:56 +08:00
|
|
|
|
name: 'PoliciesView',
|
|
|
|
|
|
components: {
|
|
|
|
|
|
ConfirmDialog,
|
|
|
|
|
|
TableLoadingState
|
|
|
|
|
|
},
|
2026-05-17 08:38:41 +00:00
|
|
|
|
emits: ['summary-change'],
|
|
|
|
|
|
setup(_, { emit }) {
|
2026-05-09 05:59:46 +00:00
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
|
|
|
|
|
|
|
const documentSearch = ref('')
|
2026-05-17 08:38:41 +00:00
|
|
|
|
const activeFolder = ref('')
|
2026-05-09 05:59:46 +00:00
|
|
|
|
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)
|
2026-05-17 08:38:41 +00:00
|
|
|
|
const uploading = ref(false)
|
|
|
|
|
|
const deletingId = ref('')
|
|
|
|
|
|
const syncingFolder = ref(false)
|
|
|
|
|
|
const deleteDialogOpen = ref(false)
|
|
|
|
|
|
const deleteTargetDocument = ref(null)
|
|
|
|
|
|
const previewLoading = ref(false)
|
|
|
|
|
|
const previewBlobUrl = ref('')
|
|
|
|
|
|
const previewError = ref('')
|
|
|
|
|
|
const onlyOfficeLoading = ref(false)
|
|
|
|
|
|
const onlyOfficeError = ref('')
|
|
|
|
|
|
const onlyOfficeAvailable = ref(false)
|
|
|
|
|
|
const onlyOfficeEditor = ref(null)
|
|
|
|
|
|
const onlyOfficeHostId = ref('knowledge-onlyoffice-preview')
|
|
|
|
|
|
const onlyOfficeReadyTimeoutId = ref(0)
|
|
|
|
|
|
const currentPreviewPageIndex = ref(0)
|
|
|
|
|
|
const previewDialogPanel = ref(null)
|
2026-05-09 05:59:46 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-05-17 08:38:41 +00:00
|
|
|
|
const activeFolderDocuments = computed(() =>
|
|
|
|
|
|
documents.value.filter((doc) => (activeFolder.value ? doc.folder === activeFolder.value : true))
|
|
|
|
|
|
)
|
|
|
|
|
|
const activeFolderIngestStats = computed(() => {
|
|
|
|
|
|
const stats = {
|
|
|
|
|
|
total: activeFolderDocuments.value.length,
|
|
|
|
|
|
pending: 0,
|
|
|
|
|
|
syncing: 0,
|
|
|
|
|
|
ingested: 0,
|
|
|
|
|
|
failed: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const doc of activeFolderDocuments.value) {
|
|
|
|
|
|
const stateCode = Number(doc?.stateCode || 0)
|
|
|
|
|
|
if (stateCode === 2) {
|
|
|
|
|
|
stats.syncing += 1
|
|
|
|
|
|
} else if (stateCode === 3) {
|
|
|
|
|
|
stats.ingested += 1
|
|
|
|
|
|
} else if (stateCode === 4) {
|
|
|
|
|
|
stats.failed += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
stats.pending += 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return stats
|
|
|
|
|
|
})
|
|
|
|
|
|
const knowledgeSyncButtonLabel = computed(() => {
|
|
|
|
|
|
if (syncingFolder.value || activeFolderIngestStats.value.syncing > 0) {
|
|
|
|
|
|
return '归纳中...'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '知识归纳'
|
|
|
|
|
|
})
|
|
|
|
|
|
const knowledgeSyncHint = computed(() => {
|
|
|
|
|
|
const stats = activeFolderIngestStats.value
|
|
|
|
|
|
if (!activeFolder.value) {
|
|
|
|
|
|
return '请选择一个固定知识目录后再触发归纳。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!stats.total) {
|
|
|
|
|
|
return '当前目录暂无文档,上传后即可进行知识归纳。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stats.syncing > 0) {
|
|
|
|
|
|
return `当前目录有 ${stats.syncing} 份文档正在归纳,完成后会自动刷新状态。`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stats.pending > 0 || stats.failed > 0) {
|
|
|
|
|
|
return `当前目录待归纳 ${stats.pending} 份,需重试 ${stats.failed} 份。`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `当前目录 ${stats.ingested} 份文档已归纳,可手动触发一次增量检查。`
|
|
|
|
|
|
})
|
|
|
|
|
|
const canTriggerKnowledgeSync = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
isAdmin.value
|
|
|
|
|
|
&& Boolean(activeFolder.value)
|
|
|
|
|
|
&& activeFolderIngestStats.value.total > 0
|
|
|
|
|
|
&& !syncingFolder.value
|
|
|
|
|
|
&& activeFolderIngestStats.value.syncing === 0
|
|
|
|
|
|
)
|
2026-05-09 05:59:46 +00: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)
|
|
|
|
|
|
})
|
|
|
|
|
|
const activePreviewPage = computed(() => {
|
|
|
|
|
|
const pages = selectedDocument.value?.previewPages || []
|
|
|
|
|
|
return pages[currentPreviewPageIndex.value] || pages[0] || null
|
|
|
|
|
|
})
|
|
|
|
|
|
const previewMetaLine = computed(() => buildPreviewMetaLine(selectedDocument.value))
|
2026-05-17 08:38:41 +00:00
|
|
|
|
const previewSecondaryMetaLine = computed(() =>
|
|
|
|
|
|
buildPreviewSecondaryMetaLine(selectedDocument.value, activePreviewPage.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const previewLayoutState = computed(() =>
|
|
|
|
|
|
resolveKnowledgePreviewLayoutState(selectedDocument.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const previewMode = computed(() =>
|
|
|
|
|
|
resolveKnowledgePreviewMode(selectedDocument.value, {
|
|
|
|
|
|
onlyOfficeAvailable: onlyOfficeAvailable.value
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
const shouldRenderOnlyOffice = computed(() =>
|
|
|
|
|
|
shouldRenderOnlyOfficePreview(selectedDocument.value, {
|
|
|
|
|
|
onlyOfficeLoading: onlyOfficeLoading.value,
|
|
|
|
|
|
onlyOfficeAvailable: onlyOfficeAvailable.value,
|
|
|
|
|
|
onlyOfficeError: onlyOfficeError.value
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
const shouldRenderOnlyOfficeHostNode = computed(() =>
|
|
|
|
|
|
shouldRenderOnlyOfficeHost(selectedDocument.value, {
|
|
|
|
|
|
onlyOfficeLoading: onlyOfficeLoading.value,
|
|
|
|
|
|
onlyOfficeAvailable: onlyOfficeAvailable.value,
|
|
|
|
|
|
onlyOfficeError: onlyOfficeError.value
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
const excelPreviewTable = computed(() =>
|
|
|
|
|
|
selectedDocument.value?.previewKind === 'table'
|
|
|
|
|
|
? buildExcelPreviewTable(activePreviewPage.value)
|
|
|
|
|
|
: { headers: [], rows: [] }
|
|
|
|
|
|
)
|
2026-05-09 05:59:46 +00:00
|
|
|
|
|
|
|
|
|
|
function revokePreviewBlob() {
|
|
|
|
|
|
if (previewBlobUrl.value) {
|
|
|
|
|
|
URL.revokeObjectURL(previewBlobUrl.value)
|
|
|
|
|
|
previewBlobUrl.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
function destroyOnlyOfficeEditor() {
|
|
|
|
|
|
if (onlyOfficeReadyTimeoutId.value) {
|
|
|
|
|
|
window.clearTimeout(onlyOfficeReadyTimeoutId.value)
|
|
|
|
|
|
onlyOfficeReadyTimeoutId.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
if (onlyOfficeEditor.value?.destroyEditor) {
|
|
|
|
|
|
onlyOfficeEditor.value.destroyEditor()
|
|
|
|
|
|
}
|
|
|
|
|
|
onlyOfficeEditor.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function mountOnlyOfficeEditor(documentId) {
|
|
|
|
|
|
onlyOfficeLoading.value = true
|
|
|
|
|
|
onlyOfficeError.value = ''
|
|
|
|
|
|
onlyOfficeAvailable.value = false
|
|
|
|
|
|
destroyOnlyOfficeEditor()
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchKnowledgeOnlyOfficeConfig(documentId)
|
|
|
|
|
|
await loadOnlyOfficeApi(payload.documentServerUrl)
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
|
2026-05-09 05:59:46 +00:00
|
|
|
|
if (!window.DocsAPI?.DocEditor) {
|
|
|
|
|
|
throw new Error('ONLYOFFICE 编辑器未正确加载。')
|
|
|
|
|
|
}
|
2026-05-17 08:38:41 +00:00
|
|
|
|
|
|
|
|
|
|
onlyOfficeHostId.value = `knowledge-onlyoffice-preview-${documentId}`
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
const config = buildOnlyOfficePreviewConfig(payload.config, {
|
|
|
|
|
|
viewportHeight: window.innerHeight
|
|
|
|
|
|
})
|
|
|
|
|
|
const upstreamEvents = config.events || {}
|
|
|
|
|
|
config.events = {
|
|
|
|
|
|
...upstreamEvents,
|
|
|
|
|
|
onAppReady(event) {
|
|
|
|
|
|
if (onlyOfficeReadyTimeoutId.value) {
|
|
|
|
|
|
window.clearTimeout(onlyOfficeReadyTimeoutId.value)
|
|
|
|
|
|
onlyOfficeReadyTimeoutId.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
onlyOfficeAvailable.value = true
|
|
|
|
|
|
onlyOfficeLoading.value = false
|
|
|
|
|
|
upstreamEvents.onAppReady?.(event)
|
|
|
|
|
|
},
|
|
|
|
|
|
onError(event) {
|
|
|
|
|
|
if (onlyOfficeReadyTimeoutId.value) {
|
|
|
|
|
|
window.clearTimeout(onlyOfficeReadyTimeoutId.value)
|
|
|
|
|
|
onlyOfficeReadyTimeoutId.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
const errorCode = event?.data?.errorCode
|
|
|
|
|
|
const errorDescription = event?.data?.errorDescription
|
|
|
|
|
|
const message = errorDescription
|
|
|
|
|
|
? `ONLYOFFICE 预览失败:${errorDescription}`
|
|
|
|
|
|
: `ONLYOFFICE 预览失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
|
|
|
|
|
onlyOfficeError.value = message
|
|
|
|
|
|
onlyOfficeLoading.value = false
|
|
|
|
|
|
console.error('ONLYOFFICE onError', event)
|
|
|
|
|
|
toast(message)
|
|
|
|
|
|
upstreamEvents.onError?.(event)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
onlyOfficeEditor.value = new window.DocsAPI.DocEditor(onlyOfficeHostId.value, config)
|
|
|
|
|
|
onlyOfficeReadyTimeoutId.value = window.setTimeout(() => {
|
|
|
|
|
|
if (!onlyOfficeAvailable.value && !onlyOfficeError.value) {
|
|
|
|
|
|
onlyOfficeError.value = 'ONLYOFFICE 预览初始化超时。请检查浏览器是否拦截了 iframe 或混合内容。'
|
|
|
|
|
|
onlyOfficeLoading.value = false
|
|
|
|
|
|
toast(onlyOfficeError.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10000)
|
|
|
|
|
|
return true
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
onlyOfficeError.value = error.message || 'ONLYOFFICE 预览加载失败。'
|
|
|
|
|
|
toast(onlyOfficeError.value)
|
|
|
|
|
|
return false
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (onlyOfficeError.value) {
|
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
|
|
activeFolder.value = resolveInitialKnowledgeFolder(folders.value, activeFolder.value)
|
|
|
|
|
|
|
|
|
|
|
|
if (options.preserveSelection && selectedDocument.value?.id) {
|
|
|
|
|
|
const nextDocument = documents.value.find((doc) => doc.id === selectedDocument.value.id)
|
|
|
|
|
|
const exists = Boolean(nextDocument)
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
|
closePreview()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedDocument.value = {
|
|
|
|
|
|
...selectedDocument.value,
|
|
|
|
|
|
...nextDocument
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
syncLibraryPolling()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
emit('summary-change', { totalDocuments: 0 })
|
|
|
|
|
|
toast(error.message || '知识库加载失败。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
2026-05-09 05:59:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectDocument(documentId) {
|
|
|
|
|
|
previewLoading.value = true
|
2026-05-17 08:38:41 +00:00
|
|
|
|
previewError.value = ''
|
|
|
|
|
|
onlyOfficeError.value = ''
|
|
|
|
|
|
onlyOfficeAvailable.value = false
|
|
|
|
|
|
revokePreviewBlob()
|
|
|
|
|
|
destroyOnlyOfficeEditor()
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchKnowledgeDocument(documentId)
|
|
|
|
|
|
selectedDocument.value = payload
|
|
|
|
|
|
currentPreviewPageIndex.value = 0
|
|
|
|
|
|
|
|
|
|
|
|
if (canUseOnlyOfficePreview(payload)) {
|
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
|
await mountOnlyOfficeEditor(documentId)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.previewKind === 'pdf' || payload.previewKind === 'image') {
|
|
|
|
|
|
const blob = await fetchKnowledgeDocumentBlob(documentId, 'inline')
|
|
|
|
|
|
previewBlobUrl.value = URL.createObjectURL(blob)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
previewError.value = error.message || '预览加载失败。'
|
2026-05-09 05:59:46 +00:00
|
|
|
|
toast(previewError.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
async function handleDownload(document) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = await fetchKnowledgeDocumentBlob(document.id, 'attachment')
|
|
|
|
|
|
triggerFileDownload(blob, document.name)
|
2026-05-09 05:59:46 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error.message || '下载失败。')
|
2026-05-17 08:38:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function patchDocumentState(documentId, patch) {
|
|
|
|
|
|
documents.value = documents.value.map((doc) =>
|
|
|
|
|
|
doc.id === documentId ? { ...doc, ...patch } : doc
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedDocument.value?.id === documentId) {
|
|
|
|
|
|
selectedDocument.value = {
|
|
|
|
|
|
...selectedDocument.value,
|
|
|
|
|
|
...patch
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
syncLibraryPolling()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasSyncingDocuments() {
|
|
|
|
|
|
return documents.value.some((doc) => Number(doc?.stateCode || 0) === 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopLibraryPolling() {
|
|
|
|
|
|
if (libraryPollTimer) {
|
|
|
|
|
|
window.clearInterval(libraryPollTimer)
|
|
|
|
|
|
libraryPollTimer = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startLibraryPolling() {
|
|
|
|
|
|
stopLibraryPolling()
|
|
|
|
|
|
libraryPollTimer = window.setInterval(() => {
|
|
|
|
|
|
loadLibrary({ preserveSelection: true })
|
|
|
|
|
|
}, KNOWLEDGE_POLL_INTERVAL_MS)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncLibraryPolling() {
|
|
|
|
|
|
if (hasSyncingDocuments()) {
|
|
|
|
|
|
startLibraryPolling()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
stopLibraryPolling()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleKnowledgeSync() {
|
|
|
|
|
|
if (!canTriggerKnowledgeSync.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
syncingFolder.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await syncKnowledgeLibrary({
|
|
|
|
|
|
folder: activeFolder.value,
|
|
|
|
|
|
documentIds: [],
|
|
|
|
|
|
force: false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const queuedIds = Array.isArray(payload?.document_ids) ? payload.document_ids : []
|
|
|
|
|
|
for (const documentId of queuedIds) {
|
|
|
|
|
|
patchDocumentState(documentId, {
|
|
|
|
|
|
stateCode: 2,
|
|
|
|
|
|
state: '\u5f52\u7eb3\u4e2d',
|
|
|
|
|
|
stateTone: 'warning',
|
|
|
|
|
|
ingestTime: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await loadLibrary({ preserveSelection: true })
|
|
|
|
|
|
toast(payload?.summary || '\u77e5\u8bc6\u5f52\u7eb3\u4efb\u52a1\u5df2\u63d0\u4ea4\u3002')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await loadLibrary({ preserveSelection: true })
|
|
|
|
|
|
toast(error.message || '\u77e5\u8bc6\u5f52\u7eb3\u89e6\u53d1\u5931\u8d25\u3002')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
syncingFolder.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function triggerUpload() {
|
|
|
|
|
|
if (!isAdmin.value || uploading.value) {
|
|
|
|
|
|
return
|
2026-05-09 05:59:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
async function handleDelete(document) {
|
|
|
|
|
|
if (!isAdmin.value || deletingId.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deleteTargetDocument.value = document
|
|
|
|
|
|
deleteDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeDeleteDialog() {
|
|
|
|
|
|
if (deletingId.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deleteDialogOpen.value = false
|
|
|
|
|
|
deleteTargetDocument.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmDeleteDocument() {
|
|
|
|
|
|
const document = deleteTargetDocument.value
|
|
|
|
|
|
if (!document || !isAdmin.value || deletingId.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deletingId.value = document.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteKnowledgeDocument(document.id)
|
|
|
|
|
|
deleteDialogOpen.value = false
|
|
|
|
|
|
deleteTargetDocument.value = null
|
|
|
|
|
|
if (selectedDocument.value?.id === document.id) {
|
|
|
|
|
|
closePreview()
|
|
|
|
|
|
}
|
|
|
|
|
|
await loadLibrary()
|
|
|
|
|
|
toast('知识库文件已删除。')
|
2026-05-09 05:59:46 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error.message || '删除失败。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
deletingId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changePageSize(size) {
|
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
|
pageSizeOpen.value = false
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
function closePreview() {
|
|
|
|
|
|
selectedDocument.value = null
|
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
|
previewError.value = ''
|
|
|
|
|
|
currentPreviewPageIndex.value = 0
|
|
|
|
|
|
revokePreviewBlob()
|
|
|
|
|
|
destroyOnlyOfficeEditor()
|
|
|
|
|
|
onlyOfficeLoading.value = false
|
|
|
|
|
|
onlyOfficeError.value = ''
|
|
|
|
|
|
onlyOfficeAvailable.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleWindowKeydown(event) {
|
|
|
|
|
|
if (event.key === 'Escape' && selectedDocument.value) {
|
|
|
|
|
|
closePreview()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 05:59:46 +00:00
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
|
watch(activeFolder, () => {
|
|
|
|
|
|
closePreview()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => previewLayoutState.value.isPreviewModalOpen,
|
|
|
|
|
|
async (isAnyOverlayOpen) => {
|
|
|
|
|
|
setBodyScrollLocked(isAnyOverlayOpen)
|
|
|
|
|
|
|
|
|
|
|
|
if (previewLayoutState.value.isPreviewModalOpen) {
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
previewDialogPanel.value?.focus?.()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadLibrary()
|
|
|
|
|
|
window.addEventListener('keydown', handleWindowKeydown)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
revokePreviewBlob()
|
|
|
|
|
|
destroyOnlyOfficeEditor()
|
|
|
|
|
|
setBodyScrollLocked(false)
|
|
|
|
|
|
stopLibraryPolling()
|
|
|
|
|
|
window.removeEventListener('keydown', handleWindowKeydown)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-09 05:59:46 +00:00
|
|
|
|
activeFolder,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
activeFolderIngestStats,
|
2026-05-09 05:59:46 +00:00
|
|
|
|
activePreviewPage,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
canTriggerKnowledgeSync,
|
2026-05-09 05:59:46 +00:00
|
|
|
|
changePageSize,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
closePreview,
|
|
|
|
|
|
closeDeleteDialog,
|
|
|
|
|
|
confirmDeleteDocument,
|
|
|
|
|
|
excelPreviewTable,
|
|
|
|
|
|
currentPage,
|
|
|
|
|
|
currentPreviewPageIndex,
|
|
|
|
|
|
deleteDialogOpen,
|
|
|
|
|
|
deleteTargetDocument,
|
|
|
|
|
|
deletingId,
|
|
|
|
|
|
documentSearch,
|
|
|
|
|
|
filteredFolders,
|
|
|
|
|
|
handleDelete,
|
|
|
|
|
|
handleDownload,
|
|
|
|
|
|
handleDrop,
|
|
|
|
|
|
handleFileInput,
|
|
|
|
|
|
handleKnowledgeSync,
|
|
|
|
|
|
isAdmin,
|
|
|
|
|
|
knowledgeSyncButtonLabel,
|
|
|
|
|
|
knowledgeSyncHint,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
pageSizeOpen,
|
|
|
|
|
|
pageSizes,
|
|
|
|
|
|
onlyOfficeError,
|
|
|
|
|
|
onlyOfficeHostId,
|
|
|
|
|
|
onlyOfficeLoading,
|
|
|
|
|
|
previewDialogPanel,
|
|
|
|
|
|
previewLayoutState,
|
|
|
|
|
|
previewMode,
|
|
|
|
|
|
previewMetaLine,
|
|
|
|
|
|
previewSecondaryMetaLine,
|
|
|
|
|
|
previewBlobUrl,
|
|
|
|
|
|
previewError,
|
|
|
|
|
|
previewLoading,
|
|
|
|
|
|
shouldRenderOnlyOffice,
|
|
|
|
|
|
shouldRenderOnlyOfficeHostNode,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
selectDocument,
|
|
|
|
|
|
selectPreviewPage,
|
|
|
|
|
|
selectedDocument,
|
|
|
|
|
|
resolveKnowledgeFolderIcon,
|
|
|
|
|
|
syncingFolder,
|
|
|
|
|
|
totalCount,
|
2026-05-17 08:38:41 +00:00
|
|
|
|
totalPages,
|
|
|
|
|
|
triggerUpload,
|
|
|
|
|
|
uploadHint,
|
2026-05-09 05:59:46 +00:00
|
|
|
|
uploadInput,
|
|
|
|
|
|
uploading,
|
|
|
|
|
|
visibleDocuments
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|