feat(web): 设置中心缓存管理与文件预览资产工具

- 新增 documentPreviewAssets 工具,统一从 URL/Blob/File 推断预览类型(image/pdf/file/unsupported)
- SettingsView/SettingsView.js/settingsModelHelper 新增系统缓存管理区块,调用 /settings/cache/clear 并展示清理结果;useSettings/services 适配
- WorkbenchAiFilePreviewDialog/useWorkbenchAiFilePreview 接入预览资产工具,workbenchAiComposerModel 调整文件处理
- ReceiptFolder/LogDetailView/DigitalEmployeeWorkRecords/travelReimbursementAttachmentModel 配套适配
- 新增 settings-cache-management-section 测试,更新 settings-llm/rendering/receipt-folder-view/composer-components/attachment-association 测试
This commit is contained in:
caoxiaozhu
2026-06-24 12:35:59 +08:00
parent 9a5ed0e94a
commit 8417a9f542
20 changed files with 815 additions and 102 deletions

View File

@@ -1,5 +1,13 @@
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { buildSelectedFileCards } from './workbenchAiComposerModel.js'
import { fetchReceiptFolderAsset } from '../../services/receiptFolder.js'
import {
inferPreviewKindFromBlob,
inferPreviewKindFromFile,
isInlinePreviewUrl,
isTemporaryPreviewUrl,
resolveDocumentPreviewAsset
} from '../../utils/documentPreviewAssets.js'
function normalizePreviewText(value) {
return String(value ?? '').replace(/\s+/g, ' ').trim()
@@ -40,10 +48,6 @@ function normalizePreviewField(field = {}) {
}
}
function resolveDocumentPreviewUrl(document = null) {
return normalizePreviewText(document?.preview_data_url || document?.previewDataUrl)
}
function resolveSourceKind(sourceUrl, rawFile = {}) {
const type = normalizePreviewText(rawFile?.type).toLowerCase()
const name = normalizePreviewText(rawFile?.name).toLowerCase()
@@ -72,8 +76,18 @@ export function useWorkbenchAiFilePreview({
scrollInlineConversationToBottom,
selectedFiles
}) {
const filePreviewState = ref({ open: false, key: '', objectUrl: '' })
const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value).map((card, index) => ({
const filePreviewState = ref({
open: false,
key: '',
objectUrl: '',
objectKind: '',
objectSource: '',
loading: false
})
const selectedFileCards = computed(() => buildSelectedFileCards(
selectedFiles.value,
(file) => attachmentFlow.resolveAiModeReceiptRecognitionState(file)
).map((card, index) => ({
...card,
ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index])
})))
@@ -97,22 +111,94 @@ export function useWorkbenchAiFilePreview({
}
}
function resolveTargetDocument(target) {
if (!target?.rawFile) {
return null
}
const recognitionState = attachmentFlow.resolveAiModeReceiptRecognitionState(target.rawFile) || target.card.ocrState || null
return recognitionState?.document || null
}
function resolveRemotePreviewAsset(target) {
const asset = resolveDocumentPreviewAsset(resolveTargetDocument(target))
if (!asset?.url || isInlinePreviewUrl(asset.url) || isTemporaryPreviewUrl(asset.url)) {
return null
}
return asset
}
async function loadRemotePreviewAsset(fileKey) {
const target = findSelectedFile(fileKey)
const asset = resolveRemotePreviewAsset(target)
if (!target?.rawFile || !asset?.url) {
return
}
try {
const blob = await fetchReceiptFolderAsset(asset.url)
const objectUrl = createObjectUrl(blob)
const objectKind = inferPreviewKindFromBlob(blob) || asset.kind
if (!objectUrl || !['image', 'pdf'].includes(objectKind)) {
return
}
if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) {
URL.revokeObjectURL(objectUrl)
return
}
clearFilePreviewObjectUrl()
filePreviewState.value = {
...filePreviewState.value,
objectUrl,
objectKind,
objectSource: asset.source,
loading: false
}
} catch (error) {
console.warn('AI mode remote attachment preview unavailable:', error)
if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) {
return
}
clearFilePreviewObjectUrl()
filePreviewState.value = {
...filePreviewState.value,
objectUrl: createObjectUrl(target.rawFile),
objectKind: inferPreviewKindFromFile(target.rawFile),
objectSource: 'file',
loading: false
}
}
}
function openAiModeFilePreview(fileKey) {
const target = findSelectedFile(fileKey)
if (!target?.rawFile) {
return
}
clearFilePreviewObjectUrl()
const remoteAsset = resolveRemotePreviewAsset(target)
filePreviewState.value = {
open: true,
key: fileKey,
objectUrl: createObjectUrl(target.rawFile)
objectUrl: remoteAsset ? '' : createObjectUrl(target.rawFile),
objectKind: remoteAsset ? '' : inferPreviewKindFromFile(target.rawFile),
objectSource: remoteAsset ? remoteAsset.source : 'file',
loading: Boolean(remoteAsset)
}
if (remoteAsset) {
void loadRemotePreviewAsset(fileKey)
}
}
function closeAiModeFilePreview() {
clearFilePreviewObjectUrl()
filePreviewState.value = { open: false, key: '', objectUrl: '' }
filePreviewState.value = {
open: false,
key: '',
objectUrl: '',
objectKind: '',
objectSource: '',
loading: false
}
}
const activeAiModeFilePreview = computed(() => {
@@ -128,9 +214,16 @@ export function useWorkbenchAiFilePreview({
const document = recognitionState?.document || null
const documentFields = Array.isArray(document?.document_fields) ? document.document_fields : document?.fields || []
const ocrFields = documentFields.map((field) => normalizePreviewField(field)).filter(Boolean)
const documentPreviewUrl = resolveDocumentPreviewUrl(document)
const sourceUrl = documentPreviewUrl || filePreviewState.value.objectUrl
const sourceKind = documentPreviewUrl ? 'image' : resolveSourceKind(sourceUrl, rawFile)
const documentPreviewAsset = resolveDocumentPreviewAsset(document)
const inlinePreviewAvailable = documentPreviewAsset?.url && isInlinePreviewUrl(documentPreviewAsset.url)
const sourceUrl = inlinePreviewAvailable
? documentPreviewAsset.url
: filePreviewState.value.objectUrl
const sourceKind = inlinePreviewAvailable
? documentPreviewAsset.kind
: filePreviewState.value.loading
? 'loading'
: filePreviewState.value.objectKind || resolveSourceKind(sourceUrl, rawFile)
const documentTypeLabel = normalizePreviewText(
document?.document_type_label ||
document?.scene_label ||