From 1455e8a5dcea2209b4d1baf4d513aa8fbfb2b39b Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期五, 08 五月 2026 17:44:45 +0800
Subject: [PATCH] 隐藏新财务模块
---
src/components/AIChatSidebar/index.vue | 1313 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 1,210 insertions(+), 103 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index 3bc2b8f..a5dcf0b 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -187,6 +187,37 @@
<!-- 鏂囨湰鍐呭 -->
<div class="text-box" v-html="message.htmlContent"></div>
+ <div v-if="message.localUploadFiles?.length" class="message-local-file-list">
+ <div
+ v-for="(file, fileIndex) in message.localUploadFiles"
+ :key="`${file.previewId || file.name}-${fileIndex}`"
+ :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
+ @click="handleMessageFileClick(file)"
+ >
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(message.localUploadFiles)"
+ :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="message-local-file-thumb"
+ />
+ <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
+ <div class="message-local-file-meta">
+ <span
+ :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
+ :title="file.name"
+ @click.stop="openMessageAttachment(file)"
+ >
+ {{ file.name }}
+ </span>
+ <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ </div>
+ </div>
+
<!-- 鍥捐〃鍐呭 -->
<div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
<div
@@ -254,14 +285,141 @@
</ul>
</div>
<div class="purchase-section-title">琛ュ厖鎴栫‘璁ゆ暟鎹�</div>
- <el-input
- v-model="message.payloadText"
- type="textarea"
- :rows="8"
- resize="vertical"
- spellcheck="false"
- class="payload-editor"
- />
+ <div class="payload-toolbar">
+ <el-button
+ size="small"
+ plain
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseRootField(message)"
+ >
+ <el-icon><Plus /></el-icon>
+ 鏂板椤跺眰瀛楁
+ </el-button>
+ </div>
+ <div class="payload-tree-table-wrapper">
+ <el-table
+ :data="message.payloadTreeData || []"
+ row-key="id"
+ border
+ stripe
+ size="small"
+ default-expand-all
+ :tree-props="{ children: 'children' }"
+ empty-text="鏆傛棤寰呯‘璁ゆ暟鎹�"
+ >
+ <el-table-column label="瀛楁" min-width="240">
+ <template #default="{ row }">
+ <div class="payload-key-cell">
+ <template v-if="row.parentType === 'object'">
+ <el-input
+ v-if="row.keyEditable"
+ v-model="row.key"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ placeholder="瀛楁鍚�"
+ />
+ <div v-else class="payload-fixed-key" :title="row.key">
+ <span>{{ getPurchaseFieldLabel(row.key) }}</span>
+ <small v-if="getPurchaseFieldLabel(row.key) !== row.key">{{ row.key }}</small>
+ </div>
+ </template>
+ <span v-else class="payload-array-index">{{ getPurchaseArrayItemLabel(row, message) }}</span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="绫诲瀷" width="130" align="center">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.valueType"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ @change="handlePurchaseNodeTypeChange(message, row)"
+ >
+ <el-option
+ v-for="option in purchaseValueTypeOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍊�" min-width="250">
+ <template #default="{ row }">
+ <div v-if="row.valueType === 'object'" class="payload-container-cell">
+ 瀵硅薄锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <div v-else-if="row.valueType === 'array'" class="payload-container-cell">
+ 鏁扮粍锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <el-switch
+ v-else-if="row.valueType === 'boolean'"
+ v-model="row.value"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ />
+ <span v-else-if="row.valueType === 'null'" class="payload-null-value">null</span>
+ <el-input
+ v-else
+ v-model="row.value"
+ size="small"
+ :placeholder="row.valueType === 'number' ? '璇疯緭鍏ユ暟瀛�' : '璇疯緭鍏ュ唴瀹�'"
+ :disabled="message.confirming || message.confirmed"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center">
+ <template #default="{ row }">
+ <div class="payload-row-actions">
+ <el-tooltip v-if="row.valueType === 'object'" content="鏂板瀛楁" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-else-if="row.valueType === 'array'" content="鏂板鏁扮粍椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-if="row.parentType === 'array'" content="鏂板鍚岀骇椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseSiblingNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎褰撳墠椤�" placement="top">
+ <el-button
+ :icon="Delete"
+ circle
+ size="small"
+ text
+ type="danger"
+ :disabled="message.confirming || message.confirmed"
+ @click="removePurchaseNode(message, row)"
+ />
+ </el-tooltip>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
<div class="payload-editor-tip">
鏃ユ湡璇峰~鍐� yyyy-MM-dd锛屼緥濡� 2026-04-30銆備骇鍝佹槑缁嗗缓璁斁鍦ㄦ瘡鏉¢噰璐彴璐︾殑 productData 涓紝纭鏃朵細鑷姩鍏煎鏃ф牸寮忓苟娓呯悊瀹℃壒瀛楁銆�
</div>
@@ -274,7 +432,7 @@
size="small"
:loading="message.confirming"
:disabled="message.confirmed || isSending"
- @click="confirmPurchaseAnalysis(message)"
+ @click="confirmPurchaseAnalysisFromTable(message)"
>
纭骞舵墽琛�
</el-button>
@@ -316,9 +474,22 @@
</div>
<div class="input-box">
<div v-if="selectedFiles.length" class="selected-file-list">
- <div v-for="(file, fileIndex) in selectedFiles" :key="`${file.name}-${fileIndex}`" class="selected-file-tag">
- <el-icon><Document /></el-icon>
- <span class="file-name">{{ file.name }}</span>
+ <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag">
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(selectedFileSnapshots)"
+ :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="selected-file-thumb"
+ />
+ <el-icon v-else><Document /></el-icon>
+ <div class="selected-file-meta">
+ <span class="file-name">{{ file.name }}</span>
+ <small class="file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
<el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
</div>
</div>
@@ -351,8 +522,9 @@
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
import request from '@/utils/request'
import * as echarts from 'echarts'
-import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
+import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
+import { builtInAssistants, generalAssistant } from './assistants'
const props = defineProps({
assistants: {
@@ -365,69 +537,10 @@
}
})
-const builtInAssistants = [
- {
- key: 'general',
- label: '寰呭姙鍔╃悊',
- title: '寰呭姙鏅鸿兘鍔╃悊',
- tooltip: '寰呭姙鍔╂墜',
- icon: Cpu,
- apiBase: '/xiaozhi',
- storageKey: 'ai_chat_uuid',
- placeholder: '璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
- welcomeMessage: '浣犲ソ',
- description: '鎴戝彲浠ュ洖绛斾綘鐨勯棶棰橈紝涓轰綘鎻愪緵涓氬姟鏁版嵁瑙h淇℃伅銆佸鐞嗗缓璁拰杈呭姪鍐崇瓥鏀寔銆�',
- allowFileUpload: true,
- emptySessionText: '鏆傛棤鍘嗗彶浼氳瘽'
- },
- {
- key: 'purchase',
- label: '閲囪喘鍔╃悊',
- title: '閲囪喘鏅鸿兘鍔╃悊',
- tooltip: '閲囪喘鏅鸿兘鍔╃悊',
- icon: ShoppingCart,
- apiBase: '/purchase-ai',
- storageKey: 'purchase_ai_chat_uuid',
- placeholder: '璇疯緭鍏ラ噰璐棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
- welcomeMessage: '浣犲ソ',
- description: '鎴戝彲浠ュ崗鍔╀綘鍒嗘瀽閲囪喘璁㈠崟銆佸埌璐ц繘搴︺�佷緵搴斿晢琛ㄧ幇鍜屼粯娆炬儏鍐碉紝甯姪浣犲揩閫熷畾浣嶉噰璐紓甯搞��',
- allowFileUpload: true,
- allowMultipleFileUpload: true,
- fileAnalyzeUrl: '/purchase-ai/analyze-files',
- emptySessionText: '鏆傛棤閲囪喘浼氳瘽'
- }
-]
-
const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
const showAssistantSwitch = computed(() => assistants.value.length > 1)
-const assistantQuickPromptMap = {
- general: [
- '鎴戝綋鍓嶆湁鍝簺瀹℃壒寰呭姙闇�瑕佸鐞嗭紵',
- '甯垜鍒楀嚭浠婂ぉ鏂板鐨勫鎵瑰緟鍔炪��',
- '褰撳墠寰呮垜瀹℃壒鐨勫崟鎹紝鎸夋椂闂村�掑簭鍒楀嚭鏉ャ��',
- '鎴戝彂璧风殑瀹℃壒閲岋紝鍝簺杩樺湪澶勭悊涓紵',
- '鏌ヨ娴佺▼缂栧彿 XXX 鐨勫鎵硅鎯呫��',
- '娴佺▼缂栧彿 XXX 鐜板湪鍗″湪鍝釜瀹℃壒鑺傜偣锛熷綋鍓嶅鎵逛汉鏄皝锛�',
- '甯垜鏌ョ湅娴佺▼缂栧彿 XXX 鐨勫鎵规祦杞褰曘��',
- '杩�7澶╂垜鐨勫鎵瑰緟鍔炵粺璁℃儏鍐垫�庝箞鏍凤紵',
- '鏈湀鎴戠殑瀹℃壒涓紝閫氳繃銆侀┏鍥炪�佸鐞嗕腑鍚勬湁澶氬皯锛�',
- '杩�30澶╁悇绫诲瀷瀹℃壒鏁伴噺鍒嗗竷鏄粈涔堬紵',
- '甯垜瀹℃壒閫氳繃娴佺▼缂栧彿 XXX锛屽娉ㄢ�滃悓鎰忊�濄��',
- '甯垜椹冲洖娴佺▼缂栧彿 XXX锛屽娉ㄢ�滆琛ュ厖璇存槑鈥濄��',
- '鎾ら攢鎴戝垰鍒氬娴佺▼缂栧彿 XXX 鐨勫鎵规搷浣溿��',
- '甯垜淇敼娴佺▼缂栧彿 XXX 鐨勫娉ㄤ负鈥滃凡琛ュ厖闄勪欢鈥濄��',
- '鍒犻櫎鎴戝彂璧风殑娴佺▼缂栧彿 XXX銆�'
- ],
- purchase: [
- '鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺锛�',
- '鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱锛�',
- '鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜涳紵',
- '甯垜缁熻寰呬粯娆鹃噰璐崟',
- '鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�'
- ]
-}
const quickPromptLimit = 3
const quickPromptStart = ref(0)
const quickPrompts = computed(() => {
@@ -435,7 +548,7 @@
if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
return assistant.quickPrompts
}
- return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
+ return generalAssistant.quickPrompts || []
})
const displayedQuickPrompts = computed(() => {
const prompts = quickPrompts.value || []
@@ -453,8 +566,8 @@
const windowWidth = ref(window.innerWidth)
const drawerSize = computed(() => {
if (windowWidth.value < 768) return '100%'
- if (windowWidth.value < 1200) return '500px'
- return '600px'
+ if (windowWidth.value < 1200) return '50%'
+ return '50%'
})
const messageListRef = ref(null)
const isSending = ref(false)
@@ -462,6 +575,7 @@
const inputMessage = ref('')
const selectedFiles = ref([])
const uploadFileList = ref([])
+const selectedFileSnapshots = ref([])
const messages = ref([])
const uuid = ref('')
const chartInstances = ref({})
@@ -535,7 +649,8 @@
totalPriceWithTax: '鍚◣鎬讳环',
invoiceType: '鍙戠エ绫诲瀷',
inventoryWarningQuantity: '搴撳瓨棰勮鏁伴噺',
- isInspected: '鏄惁璐ㄦ'
+ isInspected: '鏄惁璐ㄦ',
+ isChecked: '鏄惁璐ㄦ'
}
const purchasePayloadFieldKeyMap = {
閲囪喘鍙拌处: 'purchaseLedgers',
@@ -621,13 +736,184 @@
taxInclusiveTotalPrice: 'taxInclusiveTotalPrice',
invoiceType: 'invoiceType',
inventoryWarningQuantity: 'inventoryWarningQuantity',
- isInspected: 'isInspected'
+ isInspected: 'isInspected',
+ isChecked: 'isInspected'
}
// 鍘嗗彶浼氳瘽鐩稿叧
+const purchaseValueTypeOptions = [
+ { label: '鏂囨湰', value: 'string' },
+ { label: '鏁板瓧', value: 'number' },
+ { label: '甯冨皵', value: 'boolean' },
+ { label: '绌哄��', value: 'null' },
+ { label: '瀵硅薄', value: 'object' },
+ { label: '鏁扮粍', value: 'array' }
+]
+const purchaseContainerValueTypes = new Set(['object', 'array'])
+const purchaseHiddenFieldKeySet = new Set(['templatename', 'approvalstatus', 'phonenumber', 'type'])
+const purchaseHiddenKeyWordList = [
+ 'attachment',
+ 'file',
+ 'invoice',
+ 'ticketregistration',
+ 'receiptpayment',
+ 'payment'
+]
+const purchaseHiddenChineseKeywordList = ['闄勪欢', '寮�绁�', '鏉ョエ', '鍥炴', '浠樻']
+let purchasePayloadTreeNodeSeed = 0
+
+const shouldHidePurchaseField = (fieldKey = '') => {
+ const rawKey = String(fieldKey || '')
+ if (!rawKey) return false
+ const normalizedFieldKey = purchasePayloadFieldKeyMap[rawKey] || rawKey
+ const lowerKey = String(normalizedFieldKey).toLowerCase()
+
+ if (lowerKey.endsWith('id') || lowerKey.endsWith('ids')) return true
+ if (purchaseHiddenFieldKeySet.has(lowerKey)) return true
+ if (purchaseHiddenKeyWordList.some(keyword => lowerKey.includes(keyword))) return true
+ if (purchaseHiddenChineseKeywordList.some(keyword => rawKey.includes(keyword))) return true
+ return false
+}
+
const showHistory = ref(false)
const sessions = ref([])
const loadingSessions = ref(false)
+
+const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
+const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
+
+const getPathnameFromFilePath = (filePath = '') => {
+ const rawPath = String(filePath || '').trim()
+ if (!rawPath) return ''
+ try {
+ const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
+ return new URL(rawPath, baseOrigin).pathname || ''
+ } catch (err) {
+ return rawPath.split('?')[0]
+ }
+}
+
+const isImageFilePath = (filePath = '') => {
+ const pathname = getPathnameFromFilePath(filePath).toLowerCase()
+ return imageFilePathPattern.test(pathname)
+}
+
+const getHistoryFileName = (filePath = '', index = 0) => {
+ const pathname = getPathnameFromFilePath(filePath)
+ const fileName = pathname.split('/').filter(Boolean).pop()
+ if (!fileName) return `file-${index + 1}`
+ try {
+ return decodeURIComponent(fileName)
+ } catch (err) {
+ return fileName
+ }
+}
+
+const getImagePreviewList = (files = []) => {
+ if (!Array.isArray(files)) return []
+ return files
+ .filter(item => item?.isImage && item?.previewUrl)
+ .map(item => item.previewUrl)
+}
+
+const getImagePreviewInitialIndex = (files = [], previewUrl = '') => {
+ const list = getImagePreviewList(files)
+ const index = list.indexOf(previewUrl)
+ return index >= 0 ? index : 0
+}
+
+const formatFileSize = (size) => {
+ const bytes = Number(size)
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB`
+}
+
+const createLocalFileSnapshot = (file, index = 0) => {
+ const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null
+ const fileType = rawFile?.type || ''
+ const isImage = isImageFileType(fileType)
+ const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
+ const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
+ return {
+ previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
+ name: rawFile?.name || `file-${index + 1}`,
+ size: rawFile?.size || 0,
+ type: fileType,
+ isImage,
+ previewUrl,
+ accessUrl: '',
+ rawFile,
+ isObjectUrl: !!previewUrl
+ }
+}
+
+const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
+ const normalizedPath = String(filePath || '').trim()
+ if (!normalizedPath) return null
+ const isImage = isImageFilePath(normalizedPath)
+ return {
+ previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
+ name: getHistoryFileName(normalizedPath, fileIndex),
+ size: 0,
+ type: '',
+ isImage,
+ previewUrl: isImage ? normalizedPath : '',
+ accessUrl: normalizedPath,
+ rawFile: null,
+ isObjectUrl: false
+ }
+}
+
+const revokeLocalFileSnapshots = (snapshots = []) => {
+ const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function'
+ if (!canRevokeObjectURL) return
+ if (!Array.isArray(snapshots)) return
+ snapshots.forEach((snapshot) => {
+ if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
+ URL.revokeObjectURL(snapshot.previewUrl)
+ }
+ })
+}
+
+const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
+ if (!Array.isArray(filePaths)) return []
+ return filePaths
+ .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
+ .filter(Boolean)
+}
+
+const openMessageAttachment = (file) => {
+ const accessUrl = String(file?.accessUrl || '').trim()
+ if (!accessUrl) return
+ if (typeof window === 'undefined' || typeof window.open !== 'function') return
+ window.open(accessUrl, '_blank', 'noopener,noreferrer')
+}
+
+const handleMessageFileClick = (file) => {
+ if (!file?.accessUrl || file?.isImage) return
+ openMessageAttachment(file)
+}
+
+const revokeMessageLocalFileSnapshots = (messageList = []) => {
+ if (!Array.isArray(messageList)) return
+ messageList.forEach((msg) => {
+ if (Array.isArray(msg?.localUploadFiles)) {
+ revokeLocalFileSnapshots(msg.localUploadFiles)
+ msg.localUploadFiles = []
+ }
+ })
+}
+
+const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => {
+ if (releaseSnapshots) {
+ revokeLocalFileSnapshots(selectedFileSnapshots.value)
+ }
+ selectedFiles.value = []
+ uploadFileList.value = []
+ selectedFileSnapshots.value = []
+}
const abortCurrentRequest = () => {
if (!currentAbortController.value) return
@@ -672,6 +958,7 @@
const selectSession = async (session) => {
showHistory.value = false
+ clearSelectedFiles()
uuid.value = session.memoryId
localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
@@ -679,6 +966,7 @@
try {
const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
if (res.code === 200) {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
const historyMsgs = res.data || []
@@ -690,13 +978,16 @@
const messageObj = {
isUser,
- content: msg.content,
+ content: msg.content || '',
htmlContent: '',
isTyping: false,
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null,
+ localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
}
messages.value.push(messageObj)
@@ -711,15 +1002,15 @@
}
// 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
- const extracted = extractEmbeddedSuccessJson(msg.content)
+ const extracted = extractEmbeddedSuccessJson(msg.content || '')
if (extracted) {
applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
}
- updateOutputState(msg.content, botMsgIndex)
- messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
+ updateOutputState(msg.content || '', botMsgIndex)
+ messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
} else {
- messageObj.htmlContent = convertTextToHtml(msg.content)
+ messageObj.htmlContent = convertTextToHtml(msg.content || '')
}
})
scrollToBottom()
@@ -763,6 +1054,8 @@
})
onUnmounted(() => {
+ revokeMessageLocalFileSnapshots(messages.value)
+ clearSelectedFiles()
disposeCharts()
window.removeEventListener('resize', handleWindowResize)
})
@@ -771,13 +1064,13 @@
if (!prevKey || nextKey === prevKey) return
abortCurrentRequest()
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
outputState.value = {}
sessions.value = []
showHistory.value = false
- selectedFiles.value = []
- uploadFileList.value = []
+ clearSelectedFiles()
inputMessage.value = ''
quickPromptStart.value = 0
initUUID()
@@ -820,13 +1113,13 @@
}
const newChat = () => {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
outputState.value = {}
sessions.value = []
showHistory.value = false
- selectedFiles.value = []
- uploadFileList.value = []
+ clearSelectedFiles()
quickPromptStart.value = 0
localStorage.removeItem(currentAssistant.value.storageKey)
initUUID()
@@ -931,8 +1224,13 @@
if (parsedData.action === 'confirm_required' && parsedData.businessType) {
messageObj.type = 'purchase_analysis_confirm'
messageObj.purchaseAnalysisData = parsedData
+ if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
+ initializePurchasePayloadTree(messageObj, parsedData.payload || {})
+ }
if (!messageObj.payloadText) {
- messageObj.payloadText = JSON.stringify(localizePurchasePayload(parsedData.payload || {}), null, 2)
+ const payloadFromTree = buildPurchasePayloadFromNodes(messageObj.payloadTreeData, 'object')
+ const payloadWithHidden = mergePurchasePayloadWithHidden(payloadFromTree, messageObj.payloadHiddenData)
+ messageObj.payloadText = JSON.stringify(localizePurchasePayload(payloadWithHidden), null, 2)
}
messageObj.confirmResult = ''
messageObj.confirmed = false
@@ -1079,22 +1377,361 @@
}
}
+const hasMeaningfulPayloadValue = (value) => {
+ if (value === null || value === undefined) return false
+ if (typeof value === 'string') return value.trim() !== ''
+ if (Array.isArray(value)) return value.some(item => hasMeaningfulPayloadValue(item))
+ if (typeof value === 'object') return Object.values(value).some(item => hasMeaningfulPayloadValue(item))
+ return true
+}
+
+const mergeMappedPayloadValue = (existingValue, incomingValue) => {
+ if (existingValue === undefined) return incomingValue
+
+ const existingHasValue = hasMeaningfulPayloadValue(existingValue)
+ const incomingHasValue = hasMeaningfulPayloadValue(incomingValue)
+
+ if (existingHasValue && !incomingHasValue) return existingValue
+ if (!existingHasValue && incomingHasValue) return incomingValue
+
+ if (
+ existingValue &&
+ incomingValue &&
+ typeof existingValue === 'object' &&
+ typeof incomingValue === 'object' &&
+ !Array.isArray(existingValue) &&
+ !Array.isArray(incomingValue)
+ ) {
+ return { ...existingValue, ...incomingValue }
+ }
+
+ return incomingValue
+}
+
const mapPayloadKeys = (value, keyMap) => {
if (Array.isArray(value)) {
return value.map(item => mapPayloadKeys(item, keyMap))
}
if (value && typeof value === 'object') {
return Object.entries(value).reduce((result, [key, item]) => {
- result[keyMap[key] || key] = mapPayloadKeys(item, keyMap)
+ const mappedKey = keyMap[key] || key
+ const mappedValue = mapPayloadKeys(item, keyMap)
+ result[mappedKey] = mergeMappedPayloadValue(result[mappedKey], mappedValue)
return result
}, {})
}
return value
}
+const clonePurchasePayloadValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => clonePurchasePayloadValue(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = clonePurchasePayloadValue(item)
+ return result
+ }, {})
+ }
+ return value
+}
+
+const splitPurchasePayloadByVisibility = (value) => {
+ if (Array.isArray(value)) {
+ const splitItems = value.map(item => splitPurchasePayloadByVisibility(item))
+ const visible = splitItems.map(item => item.visible)
+ const hidden = splitItems.map(item => item.hidden)
+ return { visible, hidden }
+ }
+
+ if (value && typeof value === 'object') {
+ const visible = {}
+ const hidden = {}
+
+ Object.entries(value).forEach(([key, item]) => {
+ if (shouldHidePurchaseField(key)) {
+ hidden[key] = clonePurchasePayloadValue(item)
+ return
+ }
+ const child = splitPurchasePayloadByVisibility(item)
+ visible[key] = child.visible
+ if (hasMeaningfulPayloadValue(child.hidden)) {
+ hidden[key] = child.hidden
+ }
+ })
+
+ return { visible, hidden }
+ }
+
+ return { visible: value, hidden: undefined }
+}
+
+const mergePurchasePayloadWithHidden = (visibleValue, hiddenValue) => {
+ if (hiddenValue === undefined || hiddenValue === null) return visibleValue
+ if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
+
+ if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
+ const maxLength = Math.max(visibleValue.length, hiddenValue.length)
+ const merged = []
+ for (let i = 0; i < maxLength; i++) {
+ merged[i] = mergePurchasePayloadWithHidden(visibleValue[i], hiddenValue[i])
+ }
+ return merged
+ }
+
+ if (
+ visibleValue &&
+ hiddenValue &&
+ typeof visibleValue === 'object' &&
+ typeof hiddenValue === 'object' &&
+ !Array.isArray(visibleValue) &&
+ !Array.isArray(hiddenValue)
+ ) {
+ const merged = { ...clonePurchasePayloadValue(hiddenValue) }
+ Object.entries(visibleValue).forEach(([key, item]) => {
+ merged[key] = mergePurchasePayloadWithHidden(item, merged[key])
+ })
+ return merged
+ }
+
+ return visibleValue
+}
+
const localizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldLabelMap)
const normalizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldKeyMap)
+
+const createPurchasePayloadNodeId = () => `purchase-node-${Date.now()}-${purchasePayloadTreeNodeSeed++}`
+
+const detectPurchaseValueType = (value) => {
+ if (Array.isArray(value)) return 'array'
+ if (value === null) return 'null'
+ const valueType = typeof value
+ if (valueType === 'number') return 'number'
+ if (valueType === 'boolean') return 'boolean'
+ if (valueType === 'object') return 'object'
+ return 'string'
+}
+
+const normalizePurchaseNodeValueForEdit = (value, valueType) => {
+ if (valueType === 'number') return value === null || value === undefined ? '' : String(value)
+ if (valueType === 'boolean') return Boolean(value)
+ if (valueType === 'null') return ''
+ return value === null || value === undefined ? '' : String(value)
+}
+
+const createPurchaseTreeNode = ({
+ key = '',
+ parentType = 'object',
+ keyEditable = false,
+ valueType = 'string',
+ value = '',
+ children = []
+} = {}) => ({
+ id: createPurchasePayloadNodeId(),
+ key,
+ parentType,
+ keyEditable,
+ valueType,
+ value,
+ children
+})
+
+const reorderPurchaseObjectEntries = (value) => {
+ const entries = Object.entries(value || {})
+ const productDataIndex = entries.findIndex(([key]) => key === 'productData')
+ if (productDataIndex <= -1 || productDataIndex === entries.length - 1) {
+ return entries
+ }
+ const [productDataEntry] = entries.splice(productDataIndex, 1)
+ entries.push(productDataEntry)
+ return entries
+}
+
+const buildPurchasePayloadTreeNodes = (value, parentType = 'object') => {
+ if (Array.isArray(value)) {
+ return value.map(item => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key: '',
+ parentType: 'array',
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ if (value && typeof value === 'object') {
+ return reorderPurchaseObjectEntries(value).map(([key, item]) => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key,
+ parentType,
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ return []
+}
+
+const initializePurchasePayloadTree = (messageObj, payload = {}) => {
+ const sourcePayload = payload && typeof payload === 'object' && !Array.isArray(payload)
+ ? payload
+ : {}
+ const { visible, hidden } = splitPurchasePayloadByVisibility(sourcePayload)
+ const visiblePayload = visible && typeof visible === 'object' && !Array.isArray(visible) ? visible : {}
+ messageObj.payloadTreeData = buildPurchasePayloadTreeNodes(visiblePayload, 'object')
+ messageObj.payloadHiddenData = hidden && typeof hidden === 'object' ? hidden : {}
+}
+
+const getPurchaseFieldLabel = (fieldKey) => purchasePayloadFieldLabelMap[fieldKey] || fieldKey || '瀛楁'
+
+const createPurchaseDefaultNode = (parentType = 'object') => createPurchaseTreeNode({
+ key: parentType === 'object' ? 'newField' : '',
+ parentType,
+ keyEditable: parentType === 'object',
+ valueType: 'string',
+ value: ''
+})
+
+const getPurchaseScalarNodeValue = (node) => {
+ if (node.valueType === 'null') return null
+ if (node.valueType === 'boolean') return Boolean(node.value)
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (!text) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : text
+ }
+ return node.value === null || node.value === undefined ? '' : String(node.value)
+}
+
+const buildPurchasePayloadFromNodes = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) {
+ return parentType === 'array' ? [] : {}
+ }
+
+ if (parentType === 'array') {
+ return nodes.map(node => {
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return buildPurchasePayloadFromNodes(node.children, node.valueType)
+ }
+ return getPurchaseScalarNodeValue(node)
+ })
+ }
+
+ return nodes.reduce((result, node, index) => {
+ const rawKey = String(node.key ?? '').trim()
+ const key = rawKey || `field_${index + 1}`
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ result[key] = buildPurchasePayloadFromNodes(node.children, node.valueType)
+ } else {
+ result[key] = getPurchaseScalarNodeValue(node)
+ }
+ return result
+ }, {})
+}
+
+const findPurchaseNodeLocation = (nodes, targetId, parentNode = null) => {
+ if (!Array.isArray(nodes)) return null
+ for (let index = 0; index < nodes.length; index++) {
+ const node = nodes[index]
+ if (node.id === targetId) {
+ return {
+ siblings: nodes,
+ index,
+ node,
+ parentNode
+ }
+ }
+ const next = findPurchaseNodeLocation(node.children, targetId, node)
+ if (next) return next
+ }
+ return null
+}
+
+const getPurchaseArrayItemLabel = (row, message) => {
+ const location = findPurchaseNodeLocation(message?.payloadTreeData, row.id)
+ return `[${(location?.index ?? 0) + 1}]`
+}
+
+const handlePurchaseNodeTypeChange = (message, row) => {
+ if (!message || !row) return
+ if (purchaseContainerValueTypes.has(row.valueType)) {
+ row.children = []
+ row.value = ''
+ return
+ }
+ row.children = []
+ if (row.valueType === 'boolean') {
+ row.value = false
+ } else if (row.valueType === 'null') {
+ row.value = ''
+ } else {
+ row.value = ''
+ }
+}
+
+const addPurchaseRootField = (message) => {
+ if (!message) return
+ if (!Array.isArray(message.payloadTreeData)) {
+ message.payloadTreeData = []
+ }
+ message.payloadTreeData.push(createPurchaseDefaultNode('object'))
+}
+
+const addPurchaseChildNode = (message, row) => {
+ if (!message || !row || !purchaseContainerValueTypes.has(row.valueType)) return
+ if (!Array.isArray(row.children)) {
+ row.children = []
+ }
+ row.children.push(createPurchaseDefaultNode(row.valueType))
+}
+
+const addPurchaseSiblingNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location || location.node.parentType !== 'array') return
+ location.siblings.splice(location.index + 1, 0, createPurchaseDefaultNode('array'))
+}
+
+const removePurchaseNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location) return
+ location.siblings.splice(location.index, 1)
+}
+
+const hasPurchaseNodeValidationError = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) return false
+ return nodes.some((node) => {
+ if (parentType === 'object' && !String(node.key ?? '').trim()) {
+ return true
+ }
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (text && !Number.isFinite(Number(text))) {
+ return true
+ }
+ }
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return hasPurchaseNodeValidationError(node.children, node.valueType)
+ }
+ return false
+ })
+}
const purchaseDateFieldKeys = new Set([
'entryDateStart',
@@ -1157,6 +1794,76 @@
'瀹℃壒浜篒D',
'瀹℃壒鐢ㄦ埛ID鍒楄〃'
])
+
+const purchaseIntegerFieldKeys = new Set([
+ 'id',
+ 'supplierId',
+ 'recorderId',
+ 'salesContractNoId',
+ 'salesLedgerId',
+ 'Type',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'ticketRegistrationId',
+ 'type',
+ 'approvalStatus',
+ 'inventoryWarningQuantity'
+])
+
+const purchaseDecimalFieldKeys = new Set([
+ 'invoiceAmount',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'quantity',
+ 'taxRate',
+ 'taxInclusiveUnitPrice',
+ 'taxInclusiveTotalPrice',
+ 'taxExclusiveTotalPrice',
+ 'priceWithTax',
+ 'totalPriceWithTax'
+])
+
+const purchaseBooleanFieldKeys = new Set([
+ 'hasChildren',
+ 'isWhite',
+ 'isInspected',
+ 'isChecked'
+])
+
+const purchaseStringFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'purchaseContractNumber',
+ 'supplierName',
+ 'recorderName',
+ 'salesContractNo',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'phoneNumber',
+ 'invoiceNumber',
+ 'paymentMethod',
+ 'templateName',
+ 'productCategory',
+ 'specificationModel',
+ 'unit',
+ 'invoiceType'
+])
+
+const purchaseGenericArrayFieldKeys = new Set([
+ 'purchaseLedgers',
+ 'productData'
+])
+
+const purchaseStringArrayFieldKeys = new Set(['tempFileIds'])
+
+const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles'])
const normalizePurchaseProductRecord = (record) => {
if (!record || typeof record !== 'object' || Array.isArray(record)) return record
@@ -1237,10 +1944,191 @@
}, {})
}
+const normalizeAttachmentMaterialsValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseAttachmentMaterialsField = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchaseAttachmentMaterialsField(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ if (key === 'attachmentMaterials') {
+ result[key] = normalizeAttachmentMaterialsValue(item)
+ } else {
+ result[key] = normalizePurchaseAttachmentMaterialsField(item)
+ }
+ return result
+ }, {})
+ }
+ return value
+}
+
+const normalizePurchaseNumericText = (text = '') => {
+ return String(text)
+ .replace(/[,\s锛宂/g, '')
+ .replace(/[楼锟ュ厓%]/g, '')
+}
+
+const parsePurchaseNumberValue = (value) => {
+ if (typeof value === 'number') {
+ return Number.isFinite(value) ? value : null
+ }
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0
+ }
+ if (typeof value !== 'string') {
+ return null
+ }
+ const text = normalizePurchaseNumericText(value.trim())
+ if (!text) return null
+ if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : null
+}
+
+const normalizePurchaseIntegerFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ if (numberValue === null) return null
+ return Math.trunc(numberValue)
+}
+
+const normalizePurchaseDecimalFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ return numberValue === null ? null : numberValue
+}
+
+const normalizePurchaseBooleanFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ if (typeof value === 'boolean') return value
+ if (typeof value === 'number') return value !== 0
+ if (typeof value !== 'string') return null
+
+ const text = value.trim().toLowerCase()
+ if (!text) return null
+ if (['true', '1', 'yes', 'y', '鏄�', '宸�', 'checked'].includes(text)) return true
+ if (['false', '0', 'no', 'n', '鍚�', '鏈�', 'unchecked'].includes(text)) return false
+ return null
+}
+
+const normalizePurchaseStringFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseStringArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ if (value === null || value === undefined || value === '') return []
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) return []
+ if (/^\[.*\]$/.test(text)) {
+ try {
+ const parsedValue = JSON.parse(text)
+ if (Array.isArray(parsedValue)) {
+ return parsedValue
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ } catch (err) {
+ // Keep as plain text when not valid JSON array.
+ }
+ }
+ const splitValues = text
+ .split(/[,\n锛宂/)
+ .map(item => item.trim())
+ .filter(Boolean)
+ return splitValues.length > 1 ? splitValues : [text]
+ }
+ const normalizedValue = normalizePurchaseStringFieldValue(value)
+ return normalizedValue === null || normalizedValue === undefined || normalizedValue === ''
+ ? []
+ : [normalizedValue]
+}
+
+const normalizePurchaseObjectArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.filter(item => item && typeof item === 'object')
+ }
+ if (value && typeof value === 'object') {
+ return [value]
+ }
+ return []
+}
+
+const normalizePurchaseValueByFieldKey = (fieldKey, value) => {
+ if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value)
+ if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value)
+ if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value)
+ if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value)
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value)
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value)
+ if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value)
+ return value
+}
+
+const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => {
+ if (purchaseGenericArrayFieldKeys.has(fieldKey)) {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+ if (value && typeof value === 'object') {
+ return [normalizePurchasePayloadFieldTypes(value)]
+ }
+ return []
+ }
+
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseStringArrayFieldValue(value)
+ }
+
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseObjectArrayFieldValue(value)
+ }
+
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = normalizePurchasePayloadFieldTypes(item, key)
+ return result
+ }, {})
+ }
+
+ return normalizePurchaseValueByFieldKey(fieldKey, value)
+}
+
const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
- const sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ sanitized = normalizePurchaseAttachmentMaterialsField(sanitized)
+ sanitized = normalizePurchasePayloadFieldTypes(sanitized)
if (Array.isArray(sanitized.purchaseLedgers)) {
sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
}
@@ -1257,7 +2145,11 @@
const getVisiblePurchaseMissingFields = (analysisData) => {
const fields = Array.isArray(analysisData?.missingFields) ? analysisData.missingFields : []
const visibleFields = analysisData?.businessType === 'purchase_ledger'
- ? fields.filter(field => !purchaseApprovalFieldKeys.has(field))
+ ? fields.filter(field => {
+ if (purchaseApprovalFieldKeys.has(field)) return false
+ const normalizedField = purchasePayloadFieldKeyMap[field] || field
+ return !shouldHidePurchaseField(normalizedField) && !shouldHidePurchaseField(field)
+ })
: fields
return visibleFields.map(field => purchasePayloadFieldLabelMap[field] || field)
}
@@ -1372,6 +2264,53 @@
}
}
+const confirmPurchaseAnalysisFromTable = async (message) => {
+ if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+ if (!Array.isArray(message.payloadTreeData)) {
+ initializePurchasePayloadTree(message, message.purchaseAnalysisData.payload || {})
+ }
+ if (hasPurchaseNodeValidationError(message.payloadTreeData, 'object')) {
+ message.confirmResult = '璇峰厛琛ュ叏瀛楁鍚嶏紝骞剁‘淇濇暟瀛楀瓧娈靛~鍐欏悎娉曟暟瀛�'
+ message.confirmed = false
+ return
+ }
+
+ let payload
+ try {
+ const draftPayload = buildPurchasePayloadFromNodes(message.payloadTreeData, 'object')
+ const mergedPayload = mergePurchasePayloadWithHidden(draftPayload, message.payloadHiddenData)
+ const normalizedPayload = normalizePurchasePayload(mergedPayload)
+ payload = sanitizePurchasePayloadForSubmit(
+ normalizePurchasePayloadDates(normalizedPayload),
+ message.purchaseAnalysisData.businessType
+ )
+ message.payloadText = JSON.stringify(localizePurchasePayload(normalizedPayload), null, 2)
+ } catch (err) {
+ message.confirmResult = '寰呮彁浜ゆ暟鎹牸寮忔湁璇紝璇锋鏌ュ悗鍐嶇‘璁�'
+ message.confirmed = false
+ return
+ }
+
+ message.confirming = true
+ message.confirmResult = ''
+
+ try {
+ const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+ businessType: message.purchaseAnalysisData.businessType,
+ payload
+ })
+ message.confirmed = true
+ message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+ ElMessage.success(message.confirmResult)
+ } catch (err) {
+ message.confirmed = false
+ message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+ } finally {
+ message.confirming = false
+ }
+}
+
const scrollToBottom = () => {
nextTick(() => {
if (messageListRef.value) {
@@ -1394,16 +2333,20 @@
return isLt10M
})
+ clearSelectedFiles()
selectedFiles.value = validFiles
uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
+ selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index))
}
const removeSelectedFile = (index) => {
+ const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1)
+ revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : [])
selectedFiles.value.splice(index, 1)
uploadFileList.value.splice(index, 1)
}
-const analyzeFiles = async (files, message = '') => {
+const analyzeFiles = async (files, message = '', localFileSnapshots = []) => {
const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
if (!uploadFiles.length) return
if (isSending.value) return
@@ -1416,7 +2359,8 @@
isUser: true,
content: userMsg,
htmlContent: convertTextToHtml(userMsg),
- isTyping: false
+ isTyping: false,
+ localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : []
})
const botMsgIndex = messages.value.length
@@ -1428,7 +2372,9 @@
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null
})
outputState.value[botMsgIndex] = {
@@ -1513,9 +2459,9 @@
const msg = inputMessage.value?.trim() || ''
if ((msg || selectedFiles.value.length) && !isSending.value) {
if (selectedFiles.value.length) {
- analyzeFiles([...selectedFiles.value], msg)
- selectedFiles.value = []
- uploadFileList.value = []
+ const localFileSnapshots = selectedFileSnapshots.value
+ analyzeFiles([...selectedFiles.value], msg, localFileSnapshots)
+ clearSelectedFiles({ releaseSnapshots: false })
} else {
sendRequest(msg)
}
@@ -1549,7 +2495,9 @@
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null
}
messages.value.push(botMsg)
@@ -2561,6 +3509,87 @@
border-radius: 2px;
}
}
+
+ .message-local-file-list {
+ margin-top: 8px;
+ display: grid;
+ gap: 8px;
+ max-width: 100%;
+ }
+
+ .message-local-file-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(88, 117, 255, 0.2);
+ background: rgba(255, 255, 255, 0.9);
+ max-width: 100%;
+
+ &.clickable {
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: rgba(44, 109, 255, 0.38);
+ background: rgba(243, 247, 255, 0.96);
+ }
+ }
+ }
+
+ .message-local-file-thumb {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ overflow: hidden;
+ flex-shrink: 0;
+ border: 1px solid rgba(124, 148, 255, 0.26);
+ background: #f4f7ff;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .message-local-file-icon {
+ font-size: 20px;
+ color: $primary-blue;
+ flex-shrink: 0;
+ }
+
+ .message-local-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .message-local-file-name {
+ font-size: 12px;
+ color: #1f2a44;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.clickable {
+ color: $primary-blue;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .message-local-file-size {
+ font-size: 11px;
+ color: #7f8ba1;
+ line-height: 1.2;
+ }
}
&.bot-message {
@@ -2774,12 +3803,62 @@
color: $deep-blue;
}
-.payload-editor {
- :deep(.el-textarea__inner) {
- font-family: Consolas, Monaco, monospace;
- font-size: 12px;
- line-height: 1.55;
+.payload-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 8px;
+}
+
+.payload-tree-table-wrapper {
+ border: 1px solid rgba(0, 85, 212, 0.1);
+ border-radius: 10px;
+ overflow: auto;
+
+ :deep(.el-table) {
+ --el-table-header-bg-color: #f5f8ff;
+ --el-table-border-color: rgba(0, 85, 212, 0.08);
}
+}
+
+.payload-key-cell {
+ display: flex;
+ align-items: center;
+ min-height: 28px;
+}
+
+.payload-fixed-key {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ line-height: 1.3;
+ color: #1f2937;
+
+ small {
+ font-size: 11px;
+ color: #6b7280;
+ }
+}
+
+.payload-array-index {
+ font-size: 12px;
+ color: #475467;
+}
+
+.payload-container-cell {
+ color: #344054;
+ font-size: 12px;
+}
+
+.payload-null-value {
+ color: #6b7280;
+ font-size: 12px;
+}
+
+.payload-row-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
}
.payload-editor-tip {
@@ -2923,6 +4002,28 @@
font-size: 18px;
}
+ .selected-file-thumb {
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ flex-shrink: 0;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .selected-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
.file-name {
font-size: 13px;
color: $deep-blue;
@@ -2932,6 +4033,12 @@
font-weight: 600;
}
+ .file-size {
+ font-size: 11px;
+ color: #5f86b4;
+ line-height: 1.1;
+ }
+
.remove-file {
cursor: pointer;
color: $secondary-blue;
--
Gitblit v1.9.3