refactor(audit): split list detail flows
This commit is contained in:
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
@@ -0,0 +1,423 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user