- 新增 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 测试
277 lines
8.5 KiB
JavaScript
277 lines
8.5 KiB
JavaScript
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()
|
|
}
|
|
|
|
function formatFileSize(size) {
|
|
const bytes = Number(size || 0)
|
|
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
return '-'
|
|
}
|
|
if (bytes < 1024 * 1024) {
|
|
return `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
}
|
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
}
|
|
|
|
function formatFileTime(timestamp) {
|
|
const value = Number(timestamp || 0)
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
return '-'
|
|
}
|
|
return new Date(value).toLocaleString('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
function normalizePreviewField(field = {}) {
|
|
const value = normalizePreviewText(field.value ?? field.text)
|
|
if (!value) {
|
|
return null
|
|
}
|
|
return {
|
|
label: normalizePreviewText(field.label || field.key || field.name) || '识别字段',
|
|
value
|
|
}
|
|
}
|
|
|
|
function resolveSourceKind(sourceUrl, rawFile = {}) {
|
|
const type = normalizePreviewText(rawFile?.type).toLowerCase()
|
|
const name = normalizePreviewText(rawFile?.name).toLowerCase()
|
|
if (!sourceUrl) {
|
|
return 'unsupported'
|
|
}
|
|
if (sourceUrl.startsWith('data:image/') || type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/.test(name)) {
|
|
return 'image'
|
|
}
|
|
if (type === 'application/pdf' || /\.pdf$/.test(name)) {
|
|
return 'pdf'
|
|
}
|
|
return 'unsupported'
|
|
}
|
|
|
|
function createObjectUrl(rawFile) {
|
|
if (!rawFile || typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') {
|
|
return ''
|
|
}
|
|
return URL.createObjectURL(rawFile)
|
|
}
|
|
|
|
export function useWorkbenchAiFilePreview({
|
|
attachmentFlow,
|
|
conversationStarted,
|
|
scrollInlineConversationToBottom,
|
|
selectedFiles
|
|
}) {
|
|
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])
|
|
})))
|
|
|
|
function clearFilePreviewObjectUrl() {
|
|
const objectUrl = filePreviewState.value.objectUrl
|
|
if (objectUrl && objectUrl.startsWith('blob:') && typeof URL !== 'undefined') {
|
|
URL.revokeObjectURL(objectUrl)
|
|
}
|
|
}
|
|
|
|
function findSelectedFile(fileKey) {
|
|
const index = selectedFileCards.value.findIndex((file) => file.key === fileKey)
|
|
if (index < 0) {
|
|
return null
|
|
}
|
|
return {
|
|
card: selectedFileCards.value[index],
|
|
index,
|
|
rawFile: selectedFiles.value[index]
|
|
}
|
|
}
|
|
|
|
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: 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: '',
|
|
objectKind: '',
|
|
objectSource: '',
|
|
loading: false
|
|
}
|
|
}
|
|
|
|
const activeAiModeFilePreview = computed(() => {
|
|
if (!filePreviewState.value.open || !filePreviewState.value.key) {
|
|
return null
|
|
}
|
|
const target = findSelectedFile(filePreviewState.value.key)
|
|
if (!target) {
|
|
return null
|
|
}
|
|
const rawFile = target.rawFile
|
|
const recognitionState = attachmentFlow.resolveAiModeReceiptRecognitionState(rawFile) || target.card.ocrState || null
|
|
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 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 ||
|
|
document?.document_type ||
|
|
target.card.typeLabel
|
|
)
|
|
|
|
return {
|
|
open: true,
|
|
key: target.card.key,
|
|
name: target.card.name,
|
|
sourceKind,
|
|
sourceUrl,
|
|
documentTypeLabel: documentTypeLabel || '待识别',
|
|
recognitionStatus: recognitionState?.status || 'idle',
|
|
recognitionStatusLabel: recognitionState?.label || '等待智能录入识别',
|
|
recognitionStatusTitle: recognitionState?.title || '',
|
|
fileInfoRows: [
|
|
{ label: '文件类型', value: target.card.typeLabel },
|
|
{ label: '文件大小', value: formatFileSize(rawFile?.size) },
|
|
{ label: '上传时间', value: formatFileTime(rawFile?.lastModified) }
|
|
],
|
|
ocrFields,
|
|
ocrSummary: normalizePreviewText(document?.summary),
|
|
rawText: normalizePreviewText(document?.text).slice(0, 600)
|
|
}
|
|
})
|
|
|
|
watch(selectedFiles, (files, previousFiles = []) => {
|
|
attachmentFlow.primeAiModeReceiptContext(files)
|
|
const fileCountChanged = files.length !== previousFiles.length
|
|
if (conversationStarted.value && fileCountChanged) {
|
|
scrollInlineConversationToBottom({ force: true })
|
|
}
|
|
if (filePreviewState.value.open && !findSelectedFile(filePreviewState.value.key)) {
|
|
closeAiModeFilePreview()
|
|
}
|
|
}, { flush: 'sync' })
|
|
|
|
onBeforeUnmount(() => {
|
|
closeAiModeFilePreview()
|
|
})
|
|
|
|
return {
|
|
activeAiModeFilePreview,
|
|
closeAiModeFilePreview,
|
|
openAiModeFilePreview,
|
|
selectedFileCards
|
|
}
|
|
}
|