refactor: update orchestrator service and travel form view
- services/orchestrator.py: update orchestrator service - views/TravelReimbursementCreateView.vue: update travel form view - views/scripts/TravelReimbursementCreateView.js: update travel form script
This commit is contained in:
@@ -120,6 +120,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && message.queryPayload?.resultType === 'expense_claim_list'"
|
||||
class="message-detail-block expense-query-block"
|
||||
>
|
||||
<strong>{{ message.queryPayload.recentWindowApplied ? '近 10 日单据' : '单据明细' }}</strong>
|
||||
|
||||
<p v-if="buildExpenseQueryWindowLabel(message.queryPayload)" class="expense-query-window-label">
|
||||
{{ buildExpenseQueryWindowLabel(message.queryPayload) }}
|
||||
</p>
|
||||
|
||||
<div v-if="message.queryPayload.statusGroups?.length" class="expense-query-summary-row">
|
||||
<span
|
||||
v-for="item in message.queryPayload.statusGroups"
|
||||
:key="`${message.id}-${item.key}`"
|
||||
class="expense-query-summary-chip"
|
||||
:class="item.key"
|
||||
>
|
||||
{{ item.label }} {{ item.count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="message.queryPayload.records?.length" class="expense-query-record-list compact">
|
||||
<button
|
||||
v-for="record in getExpenseQueryVisibleRecords(message.queryPayload)"
|
||||
:key="`${message.id}-${record.claimId}`"
|
||||
type="button"
|
||||
class="expense-query-record-card"
|
||||
@click="openExpenseQueryRecord(record)"
|
||||
>
|
||||
<div class="expense-query-record-main">
|
||||
<div class="expense-query-record-top">
|
||||
<strong>{{ record.claimNo }}</strong>
|
||||
<span class="expense-query-record-status" :class="record.statusGroup || 'other'">
|
||||
{{ record.statusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<p>{{ record.summary }}</p>
|
||||
<div class="expense-query-record-meta">
|
||||
<span>{{ record.expenseTypeLabel }}</span>
|
||||
<span>{{ record.dateDisplay }}</span>
|
||||
<span>{{ record.amountDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="getExpenseQueryTotalPages(message.queryPayload) > 1"
|
||||
class="expense-query-pager"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="expense-query-pager-btn"
|
||||
:disabled="getExpenseQueryActivePage(message.queryPayload) === 1"
|
||||
aria-label="上一页"
|
||||
@click="shiftExpenseQueryPage(message, -1)"
|
||||
>
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<div class="expense-query-pager-dots" aria-label="单据分页">
|
||||
<button
|
||||
v-for="page in getExpenseQueryTotalPages(message.queryPayload)"
|
||||
:key="`${message.id}-query-page-${page}`"
|
||||
type="button"
|
||||
class="expense-query-pager-dot"
|
||||
:class="{ active: getExpenseQueryActivePage(message.queryPayload) === page }"
|
||||
:aria-label="`第 ${page} 页`"
|
||||
@click="setExpenseQueryPage(message, page)"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="expense-query-pager-btn"
|
||||
:disabled="getExpenseQueryActivePage(message.queryPayload) === getExpenseQueryTotalPages(message.queryPayload)"
|
||||
aria-label="下一页"
|
||||
@click="shiftExpenseQueryPage(message, 1)"
|
||||
>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="expense-query-empty">
|
||||
<i class="mdi mdi-file-search-outline"></i>
|
||||
<span>当前没有可直接展开的近期待办单据。</span>
|
||||
</div>
|
||||
|
||||
<p v-if="buildExpenseQueryHint(message.queryPayload)" class="expense-query-hint">
|
||||
{{ buildExpenseQueryHint(message.queryPayload) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
|
||||
<div class="review-card-shell">
|
||||
<div class="review-card-head">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user