diff --git a/web/src/views/scripts/TravelReimbursementCreateView.js b/web/src/views/scripts/TravelReimbursementCreateView.js
index b84df8f..0142d21 100644
--- a/web/src/views/scripts/TravelReimbursementCreateView.js
+++ b/web/src/views/scripts/TravelReimbursementCreateView.js
@@ -1,4 +1,5 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { useRouter } from 'vue-router'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import { useSystemState } from '../../composables/useSystemState.js'
@@ -151,6 +152,7 @@ const MAX_ATTACHMENTS = 10
const MAX_OCR_DOCUMENTS = 10
const VISIBLE_ATTACHMENT_CHIPS = 2
const COMPOSER_MAX_ROWS = 5
+const EXPENSE_QUERY_PAGE_SIZE = 5
const SESSION_TYPE_EXPENSE = 'expense'
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const HOT_KNOWLEDGE_QUESTIONS = [
@@ -199,6 +201,7 @@ function createMessage(role, text, attachments = [], extras = {}) {
meta: [],
citations: [],
suggestedActions: [],
+ queryPayload: null,
draftPayload: null,
reviewPayload: null,
riskFlags: [],
@@ -625,8 +628,50 @@ function resolveKnowledgeRankTone(index) {
return 'default'
}
+function parseConversationMessageSequence(message) {
+ const messageJson = message?.message_json || message?.messageJson || {}
+ const sequence = Number.parseInt(messageJson?.sequence, 10)
+ return Number.isFinite(sequence) && sequence > 0 ? sequence : null
+}
+
+function parseConversationMessageTime(message) {
+ const rawValue = message?.created_at || message?.createdAt || ''
+ const timestamp = new Date(rawValue).getTime()
+ return Number.isFinite(timestamp) ? timestamp : Number.MAX_SAFE_INTEGER
+}
+
+function resolveConversationMessageRolePriority(message) {
+ return String(message?.role || '').trim() === 'user' ? 0 : 1
+}
+
+function sortConversationMessages(messages) {
+ return [...(Array.isArray(messages) ? messages : [])].sort((left, right) => {
+ const leftSequence = parseConversationMessageSequence(left)
+ const rightSequence = parseConversationMessageSequence(right)
+ if (leftSequence !== null && rightSequence !== null && leftSequence !== rightSequence) {
+ return leftSequence - rightSequence
+ }
+
+ const timeDiff = parseConversationMessageTime(left) - parseConversationMessageTime(right)
+ if (timeDiff !== 0) {
+ return timeDiff
+ }
+
+ const leftRunId = String(left?.run_id || left?.runId || '').trim()
+ const rightRunId = String(right?.run_id || right?.runId || '').trim()
+ if (leftRunId && rightRunId && leftRunId === rightRunId) {
+ const roleDiff = resolveConversationMessageRolePriority(left) - resolveConversationMessageRolePriority(right)
+ if (roleDiff !== 0) {
+ return roleDiff
+ }
+ }
+
+ return String(left?.id || '').localeCompare(String(right?.id || ''))
+ })
+}
+
function normalizeInitialConversationMessages(conversation) {
- const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : []
+ const rawMessages = sortConversationMessages(conversation?.messages)
return rawMessages.map((item) => {
const messageJson = item?.message_json || item?.messageJson || {}
@@ -645,6 +690,7 @@ function normalizeInitialConversationMessages(conversation) {
item.role === 'assistant' && Array.isArray(result?.suggested_actions)
? result.suggested_actions
: [],
+ queryPayload: item.role === 'assistant' ? normalizeExpenseQueryPayload(result?.query_payload) : null,
draftPayload: item.role === 'assistant' ? result?.draft_payload || messageJson?.draft_payload || null : null,
reviewPayload: item.role === 'assistant' ? result?.review_payload || null : null,
riskFlags: item.role === 'assistant' && Array.isArray(result?.risk_flags) ? result.risk_flags : []
@@ -909,6 +955,159 @@ function formatAmountDisplay(value) {
}).format(amount)
}
+function normalizeExpenseQueryStatusGroup(item) {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+
+ const rawCount = Number(item.count || 0)
+ return {
+ key: String(item.key || 'other').trim() || 'other',
+ label: String(item.label || '其他状态').trim() || '其他状态',
+ count: Number.isFinite(rawCount) ? Math.max(0, rawCount) : 0
+ }
+}
+
+function normalizeExpenseQueryRecord(item) {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+
+ const amount = Number(item.amount || 0)
+ const amountValue = Number.isFinite(amount) ? amount : 0
+ const expenseTypeLabel = String(item.expense_type_label || item.expense_type || '报销').trim() || '报销'
+ const reason = String(item.reason || '').trim()
+ const documentDate = String(item.document_date || '').trim()
+ const occurredAt = String(item.occurred_at || '').trim()
+
+ return {
+ claimId: String(item.claim_id || '').trim(),
+ claimNo: String(item.claim_no || '').trim() || '未编号',
+ employeeName: String(item.employee_name || '').trim(),
+ expenseType: String(item.expense_type || '').trim(),
+ expenseTypeLabel,
+ amount: amountValue,
+ amountDisplay: formatAmountDisplay(amountValue),
+ status: String(item.status || '').trim(),
+ statusLabel: String(item.status_label || '处理中').trim() || '处理中',
+ statusGroup: String(item.status_group || 'other').trim() || 'other',
+ statusGroupLabel: String(item.status_group_label || '其他状态').trim() || '其他状态',
+ approvalStage: String(item.approval_stage || '').trim(),
+ documentDate,
+ occurredAt,
+ reason,
+ location: String(item.location || '').trim(),
+ summary: reason || `${expenseTypeLabel}报销`,
+ dateDisplay: documentDate || occurredAt || '待补充日期'
+ }
+}
+
+function normalizeExpenseQueryPayload(payload) {
+ if (!payload || typeof payload !== 'object') {
+ return null
+ }
+
+ const resultType = String(payload.result_type || '').trim()
+ if (resultType && resultType !== 'expense_claim_list') {
+ return null
+ }
+
+ const records = (Array.isArray(payload.records) ? payload.records : [])
+ .map(normalizeExpenseQueryRecord)
+ .filter(Boolean)
+ const statusGroups = (Array.isArray(payload.status_groups) ? payload.status_groups : [])
+ .map(normalizeExpenseQueryStatusGroup)
+ .filter(Boolean)
+
+ const rawRecordCount = Number(payload.record_count || 0)
+ const rawPreviewCount = Number(payload.preview_count || records.length)
+ const rawOlderRecordCount = Number(payload.older_record_count || 0)
+ const totalAmount = Number(payload.total_amount || 0)
+ const rawWindowDays = Number(payload.window_days || 0)
+ const windowStartDate = String(payload.window_start_date || '').trim()
+ const windowEndDate = String(payload.window_end_date || '').trim()
+
+ return {
+ resultType: 'expense_claim_list',
+ scopeLabel: String(payload.scope_label || '报销单').trim() || '报销单',
+ recentWindowApplied: Boolean(payload.recent_window_applied),
+ windowDays:
+ payload.window_days === null || payload.window_days === undefined || payload.window_days === ''
+ ? null
+ : (Number.isFinite(rawWindowDays) ? Math.max(1, rawWindowDays) : null),
+ windowStartDate: windowStartDate || '',
+ windowEndDate: windowEndDate || '',
+ recordCount: Number.isFinite(rawRecordCount) ? Math.max(0, rawRecordCount) : 0,
+ previewCount: Number.isFinite(rawPreviewCount) ? Math.max(0, rawPreviewCount) : records.length,
+ olderRecordCount: Number.isFinite(rawOlderRecordCount) ? Math.max(0, rawOlderRecordCount) : 0,
+ hasMoreInWindow: Boolean(payload.has_more_in_window || payload.has_more),
+ totalAmount: Number.isFinite(totalAmount) ? totalAmount : 0,
+ statusGroups,
+ records,
+ currentPage: 1
+ }
+}
+
+function buildExpenseQueryWindowLabel(queryPayload) {
+ if (!queryPayload) {
+ return ''
+ }
+
+ if (queryPayload.windowStartDate && queryPayload.windowEndDate) {
+ return `${queryPayload.windowStartDate} 至 ${queryPayload.windowEndDate}`
+ }
+
+ if (queryPayload.recentWindowApplied && queryPayload.windowDays) {
+ return `近 ${queryPayload.windowDays} 日内`
+ }
+
+ return '当前条件下'
+}
+
+function getExpenseQueryTotalPages(queryPayload) {
+ const recordCount = Array.isArray(queryPayload?.records) ? queryPayload.records.length : 0
+ return Math.max(1, Math.ceil(recordCount / EXPENSE_QUERY_PAGE_SIZE))
+}
+
+function getExpenseQueryActivePage(queryPayload) {
+ const totalPages = getExpenseQueryTotalPages(queryPayload)
+ const rawPage = Number(queryPayload?.currentPage || 1)
+ if (!Number.isFinite(rawPage)) {
+ return 1
+ }
+ return Math.min(Math.max(1, Math.round(rawPage)), totalPages)
+}
+
+function getExpenseQueryVisibleRecords(queryPayload) {
+ const records = Array.isArray(queryPayload?.records) ? queryPayload.records : []
+ const activePage = getExpenseQueryActivePage(queryPayload)
+ const start = (activePage - 1) * EXPENSE_QUERY_PAGE_SIZE
+ return records.slice(start, start + EXPENSE_QUERY_PAGE_SIZE)
+}
+
+function buildExpenseQueryHint(queryPayload) {
+ if (!queryPayload) {
+ return ''
+ }
+
+ const parts = []
+ const windowText = buildExpenseQueryWindowLabel(queryPayload)
+
+ if (Array.isArray(queryPayload.records) && queryPayload.records.length > EXPENSE_QUERY_PAGE_SIZE) {
+ parts.push(`当前共整理 ${queryPayload.records.length} 笔单据,可左右切换查看`)
+ }
+
+ if (queryPayload.hasMoreInWindow && queryPayload.previewCount < queryPayload.recordCount) {
+ parts.push(`${windowText}共 ${queryPayload.recordCount} 笔,当前先整理最近 ${queryPayload.previewCount} 笔`)
+ }
+
+ if (queryPayload.olderRecordCount > 0 && queryPayload.windowDays) {
+ parts.push(`另有 ${queryPayload.olderRecordCount} 笔超过 ${queryPayload.windowDays} 日的单据,请前往个人报销中心查看`)
+ }
+
+ return parts.join('。')
+}
+
function countReviewPendingItems(reviewPayload) {
return resolveReviewMissingSlotCards(reviewPayload).length
}
@@ -1661,6 +1860,7 @@ function buildErrorInsight(error, fileNames = []) {
fileNames,
citations: [],
suggestedActions: [],
+ queryPayload: null,
draftPayload: null,
reviewPayload: null,
riskFlags: [],
@@ -1699,6 +1899,7 @@ function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
fileNames,
citations: Array.isArray(result?.citations) ? result.citations : [],
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
+ queryPayload: normalizeExpenseQueryPayload(result?.query_payload),
draftPayload: result?.draft_payload || null,
reviewPayload: result?.review_payload || null,
riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [],
@@ -1743,6 +1944,7 @@ export default {
},
emits: ['close', 'draft-saved'],
setup(props, { emit }) {
+ const router = useRouter()
const { currentUser } = useSystemState()
const { toast } = useToast()
@@ -1816,7 +2018,7 @@ export default {
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
}
- return '例如:查一下本周报销金额、解释酒店超标风险,或根据附件生成报销草稿。'
+ return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件生成报销草稿。'
})
const currentIntentLabel = computed(() => {
if (isKnowledgeSession.value && currentInsight.value.intent === 'welcome') {
@@ -2395,6 +2597,37 @@ export default {
emit('close')
}
+ function openExpenseQueryRecord(record) {
+ const claimId = String(record?.claimId || '').trim()
+ if (!claimId) {
+ return
+ }
+
+ router.push({
+ name: 'app-request-detail',
+ params: { requestId: claimId }
+ })
+ emit('close')
+ }
+
+ function setExpenseQueryPage(message, page) {
+ if (!message?.queryPayload) {
+ return
+ }
+
+ const totalPages = getExpenseQueryTotalPages(message.queryPayload)
+ const nextPage = Math.min(Math.max(1, Number(page || 1)), totalPages)
+ message.queryPayload.currentPage = nextPage
+ }
+
+ function shiftExpenseQueryPage(message, delta) {
+ if (!message?.queryPayload) {
+ return
+ }
+
+ setExpenseQueryPage(message, getExpenseQueryActivePage(message.queryPayload) + Number(delta || 0))
+ }
+
function openDeleteSessionDialog() {
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || sessionSwitchBusy.value) {
return
@@ -2637,6 +2870,7 @@ export default {
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
? payload.result.suggested_actions
: [],
+ queryPayload: normalizeExpenseQueryPayload(payload?.result?.query_payload),
draftPayload: payload?.result?.draft_payload || null,
reviewPayload: payload?.result?.review_payload || null,
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
@@ -2886,6 +3120,11 @@ export default {
buildReviewRiskHint,
buildReviewActionHint,
buildReviewStatusTag,
+ buildExpenseQueryWindowLabel,
+ buildExpenseQueryHint,
+ getExpenseQueryActivePage,
+ getExpenseQueryTotalPages,
+ getExpenseQueryVisibleRecords,
resolveDocumentPreview,
triggerFileUpload,
handleFilesChange,
@@ -2899,6 +3138,9 @@ export default {
removeAttachedFile,
clearAttachedFiles,
requestCloseWorkbench,
+ openExpenseQueryRecord,
+ setExpenseQueryPage,
+ shiftExpenseQueryPage,
openDeleteSessionDialog,
closeDeleteSessionDialog,
confirmDeleteCurrentSession,