Files
X-Financial/web/src/views/scripts/useAuditSpreadsheetEditor.js
2026-05-29 09:44:03 +08:00

424 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}