424 lines
14 KiB
JavaScript
424 lines
14 KiB
JavaScript
import { computed, nextTick, ref } from 'vue'
|
||
|
||
import {
|
||
fetchAgentAssetSpreadsheetBlob,
|
||
fetchAgentAssetSpreadsheetChangeRecords,
|
||
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
|
||
importAgentAssetSpreadsheetContent
|
||
} from '../../services/agentAssets.js'
|
||
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
||
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
||
import { buildSpreadsheetChangeRecordKey } from './auditViewRuntimeModel.js'
|
||
import {
|
||
formatDateTime,
|
||
formatSpreadsheetChangeSummary,
|
||
normalizeText,
|
||
resolveDiffChangeMeta
|
||
} from './auditViewModel.js'
|
||
|
||
export function useAuditSpreadsheetEditor({
|
||
selectedSkill,
|
||
selectedSkillUsesSpreadsheet,
|
||
canEditSpreadsheetInline,
|
||
canUploadSpreadsheet,
|
||
canDownloadSpreadsheet,
|
||
selectedSpreadsheetFileName,
|
||
actionState,
|
||
refreshCurrentAssets,
|
||
loadSelectedAssetDetail,
|
||
resolveActor,
|
||
toast
|
||
}) {
|
||
const spreadsheetUploadInput = ref(null)
|
||
const spreadsheetOnlyOfficeLoading = ref(false)
|
||
const spreadsheetOnlyOfficeError = ref('')
|
||
const spreadsheetOnlyOfficeEditor = ref(null)
|
||
const spreadsheetOnlyOfficeReady = ref(false)
|
||
const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
|
||
const spreadsheetChangeRecordsByAsset = ref({})
|
||
const spreadsheetChangeDetailOpen = ref(false)
|
||
const selectedSpreadsheetChangeRecord = ref(null)
|
||
|
||
let spreadsheetOnlyOfficeMountSeq = 0
|
||
let spreadsheetOnlyOfficeLoadTimer = null
|
||
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||
let spreadsheetOnlyOfficeSyncSeq = 0
|
||
let spreadsheetOnlyOfficeChangePollTimer = null
|
||
|
||
const selectedSpreadsheetChangeRecords = computed(() => {
|
||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
|
||
return []
|
||
}
|
||
return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
|
||
.filter((item) => item?.changed_at)
|
||
.map((item) => {
|
||
const sheetNames = [
|
||
...(Array.isArray(item.sheet_changes)
|
||
? item.sheet_changes.map((change) => normalizeText(change.sheet_name))
|
||
: []),
|
||
...(Array.isArray(item.cell_changes)
|
||
? item.cell_changes.map((change) => normalizeText(change.sheet_name))
|
||
: [])
|
||
].filter(Boolean)
|
||
const changedSheetNames = [...new Set(sheetNames)]
|
||
const previewChanges = Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : []
|
||
return {
|
||
...item,
|
||
time: formatDateTime(item.changed_at),
|
||
summary: formatSpreadsheetChangeSummary(item.summary),
|
||
changeCountLabel: item.changed_cell_count
|
||
? `${item.changed_cell_count} 处改动`
|
||
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
||
changedSheetNames,
|
||
sheetPreview: changedSheetNames.slice(0, 4),
|
||
remainingSheetCount: Math.max(changedSheetNames.length - 4, 0),
|
||
previewChanges,
|
||
remainingChangeCount: Math.max((item.changed_cell_count || 0) - previewChanges.length, 0)
|
||
}
|
||
})
|
||
})
|
||
|
||
const selectedSpreadsheetChangeSheetRows = computed(() =>
|
||
Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
|
||
? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
|
||
...item,
|
||
meta: resolveDiffChangeMeta(item.change_type)
|
||
}))
|
||
: []
|
||
)
|
||
|
||
const selectedSpreadsheetChangeCellRows = computed(() =>
|
||
Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
|
||
? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
|
||
...item,
|
||
meta: resolveDiffChangeMeta(item.change_type)
|
||
}))
|
||
: []
|
||
)
|
||
|
||
function stopSpreadsheetOnlyOfficeChangeSync() {
|
||
if (spreadsheetOnlyOfficeChangePollTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
|
||
spreadsheetOnlyOfficeChangePollTimer = null
|
||
}
|
||
}
|
||
|
||
function destroySpreadsheetOnlyOfficeEditor() {
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||
spreadsheetOnlyOfficeSyncSeq += 1
|
||
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
||
spreadsheetOnlyOfficeEditor.value.destroyEditor()
|
||
}
|
||
spreadsheetOnlyOfficeEditor.value = null
|
||
spreadsheetOnlyOfficeReady.value = false
|
||
}
|
||
|
||
function getLatestSpreadsheetChangeKey(assetId) {
|
||
return buildSpreadsheetChangeRecordKey(spreadsheetChangeRecordsByAsset.value[assetId] || [])
|
||
}
|
||
|
||
async function loadSpreadsheetChangeRecords(assetId) {
|
||
if (!assetId) {
|
||
return
|
||
}
|
||
const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
|
||
spreadsheetChangeRecordsByAsset.value = {
|
||
...spreadsheetChangeRecordsByAsset.value,
|
||
[assetId]: Array.isArray(payload) ? payload : []
|
||
}
|
||
}
|
||
|
||
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
|
||
const normalizedAssetId = normalizeText(assetId)
|
||
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return false
|
||
}
|
||
|
||
await loadSpreadsheetChangeRecords(normalizedAssetId)
|
||
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
|
||
return true
|
||
}
|
||
if (attempt >= 9) {
|
||
return false
|
||
}
|
||
await new Promise((resolve) => window.setTimeout(resolve, 800))
|
||
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
||
}
|
||
|
||
function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
|
||
const normalizedAssetId = normalizeText(assetId)
|
||
if (!normalizedAssetId) {
|
||
return
|
||
}
|
||
|
||
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||
|
||
const runSync = async () => {
|
||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
||
normalizedAssetId,
|
||
previousLatestChangeKey
|
||
)
|
||
if (changeRecordRefreshed) {
|
||
await refreshCurrentAssets()
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
return
|
||
}
|
||
} catch {
|
||
// 临时轮询失败不打断编辑器,继续在窗口期内重试。
|
||
}
|
||
|
||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return
|
||
}
|
||
if (attempt >= 29) {
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
|
||
}, 2000)
|
||
}
|
||
|
||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||
runSync().catch(() => {})
|
||
}, attempt === 0 ? 800 : 2000)
|
||
}
|
||
|
||
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
|
||
return (
|
||
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
||
!selectedSkillUsesSpreadsheet.value ||
|
||
selectedSkill.value?.id !== assetId ||
|
||
selectedSkill.value?.loading
|
||
)
|
||
}
|
||
|
||
async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
|
||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
return
|
||
}
|
||
|
||
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
||
const assetId = selectedSkill.value.id
|
||
const editable = canEditSpreadsheetInline.value
|
||
|
||
spreadsheetOnlyOfficeLoading.value = true
|
||
spreadsheetOnlyOfficeError.value = ''
|
||
spreadsheetOnlyOfficeReady.value = false
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
|
||
try {
|
||
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
|
||
await loadOnlyOfficeApi(payload.documentServerUrl)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (!window.DocsAPI?.DocEditor) {
|
||
throw new Error('表格编辑器未正确加载。')
|
||
}
|
||
|
||
// ONLYOFFICE 会改写宿主节点;每次挂载使用新 id 避免复用脏容器。
|
||
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
|
||
await nextTick()
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
|
||
const config = buildOnlyOfficeEditorConfig(payload.config, {
|
||
viewportHeight: window.innerHeight,
|
||
editable,
|
||
fillContainer: true
|
||
})
|
||
const upstreamEvents = config.events || {}
|
||
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (retryAttempt < 1) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
spreadsheetOnlyOfficeLoading.value = true
|
||
window.setTimeout(() => {
|
||
mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
|
||
}, 600)
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeError.value = '表格加载超时,请退出详情后重试。'
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
}, 15000)
|
||
config.events = {
|
||
...upstreamEvents,
|
||
onAppReady(event) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
spreadsheetOnlyOfficeReady.value = true
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
upstreamEvents.onAppReady?.(event)
|
||
},
|
||
onError(event) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
const errorCode = event?.data?.errorCode
|
||
const errorDescription = event?.data?.errorDescription
|
||
spreadsheetOnlyOfficeError.value = errorDescription
|
||
? `表格加载失败:${errorDescription}`
|
||
: `表格加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
upstreamEvents.onError?.(event)
|
||
},
|
||
onDocumentStateChange(event) {
|
||
const hasChanges = Boolean(event?.data)
|
||
if (hasChanges) {
|
||
spreadsheetOnlyOfficeHadLocalEdits = true
|
||
if (!spreadsheetOnlyOfficeChangePollTimer) {
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||
}
|
||
} else if (
|
||
spreadsheetOnlyOfficeHadLocalEdits &&
|
||
editable &&
|
||
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
|
||
) {
|
||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||
}
|
||
upstreamEvents.onDocumentStateChange?.(event)
|
||
}
|
||
}
|
||
spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
|
||
spreadsheetOnlyOfficeHostId.value,
|
||
config
|
||
)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
}
|
||
} catch (error) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
toast(spreadsheetOnlyOfficeError.value)
|
||
}
|
||
}
|
||
|
||
function triggerSpreadsheetUpload() {
|
||
if (!canUploadSpreadsheet.value) {
|
||
return
|
||
}
|
||
spreadsheetUploadInput.value?.click?.()
|
||
}
|
||
|
||
async function downloadSpreadsheetFile() {
|
||
if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
|
||
return
|
||
}
|
||
|
||
actionState.value = 'download-spreadsheet'
|
||
try {
|
||
const blob = await fetchAgentAssetSpreadsheetBlob(
|
||
selectedSkill.value.id,
|
||
'attachment'
|
||
)
|
||
const objectUrl = URL.createObjectURL(blob)
|
||
const anchor = document.createElement('a')
|
||
anchor.href = objectUrl
|
||
anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
|
||
document.body.appendChild(anchor)
|
||
anchor.click()
|
||
anchor.remove()
|
||
URL.revokeObjectURL(objectUrl)
|
||
} catch (error) {
|
||
toast(error?.message || '规则表下载失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function uploadSpreadsheetFile(file) {
|
||
if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
|
||
return
|
||
}
|
||
|
||
actionState.value = 'upload-spreadsheet'
|
||
try {
|
||
await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
|
||
actor: resolveActor()
|
||
})
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
||
toast(`已导入 ${file.name} 的表格内容,右侧会记录本次修改。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
spreadsheetUploadInput.value?.reset?.()
|
||
}
|
||
}
|
||
|
||
async function handleSpreadsheetFileInput(event) {
|
||
await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
|
||
}
|
||
|
||
function openSpreadsheetChangeDetail(item) {
|
||
if (!item?.changed_at) {
|
||
return
|
||
}
|
||
selectedSpreadsheetChangeRecord.value = item
|
||
spreadsheetChangeDetailOpen.value = true
|
||
}
|
||
|
||
function closeSpreadsheetChangeDetail() {
|
||
spreadsheetChangeDetailOpen.value = false
|
||
}
|
||
|
||
return {
|
||
spreadsheetUploadInput,
|
||
spreadsheetOnlyOfficeLoading,
|
||
spreadsheetOnlyOfficeError,
|
||
spreadsheetOnlyOfficeReady,
|
||
spreadsheetOnlyOfficeHostId,
|
||
spreadsheetChangeDetailOpen,
|
||
selectedSpreadsheetChangeRecord,
|
||
selectedSpreadsheetChangeRecords,
|
||
selectedSpreadsheetChangeSheetRows,
|
||
selectedSpreadsheetChangeCellRows,
|
||
destroySpreadsheetOnlyOfficeEditor,
|
||
mountSpreadsheetOnlyOfficeEditor,
|
||
triggerSpreadsheetUpload,
|
||
downloadSpreadsheetFile,
|
||
uploadSpreadsheetFile,
|
||
handleSpreadsheetFileInput,
|
||
loadSpreadsheetChangeRecords,
|
||
openSpreadsheetChangeDetail,
|
||
closeSpreadsheetChangeDetail
|
||
}
|
||
}
|