2026-05-19 17:24:13 +00:00
|
|
|
|
<template>
|
|
|
|
|
|
<Teleport to="body">
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<Transition name="assistant-modal" @after-enter="handleAssistantModalAfterEnter" @after-leave="emitCloseAfterLeave">
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div v-if="workbenchVisible" class="assistant-overlay">
|
|
|
|
|
|
<section class="assistant-modal">
|
|
|
|
|
|
<div class="assistant-header-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="assistant-toggle-btn"
|
|
|
|
|
|
:class="{ disabled: !hasInsightPanelContent }"
|
|
|
|
|
|
:disabled="!hasInsightPanelContent || sessionSwitchBusy"
|
|
|
|
|
|
:title="insightPanelToggleLabel"
|
|
|
|
|
|
:aria-label="insightPanelToggleLabel"
|
|
|
|
|
|
@click="toggleInsightPanel"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="showInsightPanel ? 'mdi mdi-arrow-collapse-right' : 'mdi mdi-arrow-expand-right'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="session-trash-btn"
|
|
|
|
|
|
:disabled="!canDeleteCurrentSession || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
|
|
|
|
|
|
title="删除当前会话"
|
|
|
|
|
|
aria-label="删除当前会话"
|
|
|
|
|
|
@click="openDeleteSessionDialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-delete-outline"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="assistant-close-btn"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
title="关闭工作台"
|
|
|
|
|
|
aria-label="关闭对话工作台"
|
|
|
|
|
|
@click="requestCloseWorkbench"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="assistant-modal-stage">
|
|
|
|
|
|
<header class="assistant-header">
|
|
|
|
|
|
<div class="assistant-header-main">
|
|
|
|
|
|
<div>
|
2026-05-25 13:35:39 +08:00
|
|
|
|
<h2>{{ assistantHeaderTitle }}</h2>
|
|
|
|
|
|
<p>{{ assistantHeaderDescription }}</p>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="assistant-layout"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'can-show-insight': hasInsightPanelContent,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
'has-insight': hasInsightPanelContent && showInsightPanel
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<section class="dialog-panel">
|
|
|
|
|
|
<div v-if="shortcuts.length" class="dialog-toolbar">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="shortcut in shortcuts"
|
|
|
|
|
|
:key="shortcut.label"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="shortcut-chip"
|
2026-05-25 13:35:39 +08:00
|
|
|
|
:class="{ active: shortcut.active }"
|
|
|
|
|
|
:aria-pressed="shortcut.active ? 'true' : 'false'"
|
|
|
|
|
|
:disabled="shortcut.active || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
@click="runShortcut(shortcut)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="shortcut.icon"></i>
|
|
|
|
|
|
<span>{{ shortcut.label }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div ref="messageListRef" class="message-list" aria-live="polite">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="message in messages"
|
|
|
|
|
|
:key="message.id"
|
|
|
|
|
|
class="message-row"
|
|
|
|
|
|
:class="message.role"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="message-avatar">
|
|
|
|
|
|
<img
|
|
|
|
|
|
:src="message.role === 'assistant' ? aiAvatar : userAvatar"
|
|
|
|
|
|
:alt="message.role === 'assistant' ? '财务助手头像' : '用户头像'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
2026-05-23 19:54:42 +08:00
|
|
|
|
<div class="message-bubble" :class="buildMessageBubbleClass(message)">
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<header class="message-meta">
|
|
|
|
|
|
<strong>{{ message.role === 'assistant' ? (message.assistantName || ASSISTANT_DISPLAY_NAME) : '我' }}</strong>
|
|
|
|
|
|
<time>{{ message.time }}</time>
|
|
|
|
|
|
</header>
|
2026-05-21 23:53:03 +08:00
|
|
|
|
<div
|
2026-05-22 08:58:59 +08:00
|
|
|
|
v-if="message.text && message.role === 'assistant' && message.reviewPayload && buildReviewMainMessageText(message)"
|
2026-05-21 23:53:03 +08:00
|
|
|
|
class="review-summary message-answer-content message-answer-markdown"
|
2026-05-22 08:58:59 +08:00
|
|
|
|
v-html="renderMarkdown(buildReviewMainMessageText(message))"
|
|
|
|
|
|
@click="handleAssistantMarkdownClick($event, message)"
|
2026-05-21 23:53:03 +08:00
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="message.text && message.role !== 'assistant'"
|
|
|
|
|
|
class="message-answer-content message-answer-markdown message-rich-text"
|
|
|
|
|
|
v-html="renderMarkdown(message.text)"
|
|
|
|
|
|
></div>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="message.text && message.role === 'assistant'"
|
|
|
|
|
|
class="message-answer-content message-answer-markdown"
|
|
|
|
|
|
v-html="renderMarkdown(message.text)"
|
2026-05-22 08:58:59 +08:00
|
|
|
|
@click="handleAssistantMarkdownClick($event, message)"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
></div>
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && message.applicationPreview"
|
|
|
|
|
|
class="application-preview-table"
|
|
|
|
|
|
role="table"
|
|
|
|
|
|
aria-label="申请信息核对表"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="application-preview-row head" role="row">
|
|
|
|
|
|
<span role="columnheader">字段</span>
|
|
|
|
|
|
<span role="columnheader">内容</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="row in resolveApplicationPreviewRows(message)"
|
|
|
|
|
|
:key="`${message.id}-${row.key}`"
|
|
|
|
|
|
class="application-preview-row"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
missing: row.missing,
|
|
|
|
|
|
editable: row.editable && !submitting && !reviewActionBusy && !sessionSwitchBusy,
|
|
|
|
|
|
highlight: row.highlight
|
|
|
|
|
|
}"
|
|
|
|
|
|
role="row"
|
|
|
|
|
|
:tabindex="row.editable && !submitting && !reviewActionBusy && !sessionSwitchBusy ? 0 : -1"
|
|
|
|
|
|
:aria-label="row.editable ? `编辑${row.label}` : row.label"
|
|
|
|
|
|
@click="row.editable && !isApplicationPreviewEditing(message, row.key) && !submitting && !reviewActionBusy && !sessionSwitchBusy && openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
@keydown.enter.prevent="row.editable && !isApplicationPreviewEditing(message, row.key) && !submitting && !reviewActionBusy && !sessionSwitchBusy && openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
@keydown.space.prevent="row.editable && !isApplicationPreviewEditing(message, row.key) && !submitting && !reviewActionBusy && !sessionSwitchBusy && openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="application-preview-label" role="cell">{{ row.label }}</span>
|
|
|
|
|
|
<span class="application-preview-value" role="cell">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-if="isApplicationPreviewEditing(message, row.key) && resolveApplicationPreviewEditorControl(row.key) !== 'select'"
|
|
|
|
|
|
v-model="applicationPreviewEditor.draftValue"
|
|
|
|
|
|
class="application-preview-input"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
autofocus
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@keydown.stop="handleApplicationPreviewEditorKeydown($event, message)"
|
|
|
|
|
|
@blur="commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<select
|
|
|
|
|
|
v-else-if="isApplicationPreviewEditing(message, row.key)"
|
|
|
|
|
|
v-model="applicationPreviewEditor.draftValue"
|
|
|
|
|
|
class="application-preview-input application-preview-select"
|
|
|
|
|
|
autofocus
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@change="commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
@keydown.stop="handleApplicationPreviewEditorKeydown($event, message)"
|
|
|
|
|
|
@blur="commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">请选择</option>
|
|
|
|
|
|
<option
|
|
|
|
|
|
v-for="option in resolveApplicationPreviewEditorOptions(row.key)"
|
|
|
|
|
|
:key="option"
|
|
|
|
|
|
:value="option"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ option }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<span class="application-preview-text">{{ row.value }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="row.editable"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="application-preview-edit-btn"
|
|
|
|
|
|
title="修改内容"
|
|
|
|
|
|
aria-label="修改内容"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@click.stop="openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-pencil-outline"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && message.applicationPreview && buildApplicationPreviewFooterText(message)"
|
|
|
|
|
|
class="application-preview-footer message-answer-content message-answer-markdown"
|
|
|
|
|
|
v-html="renderMarkdown(buildApplicationPreviewFooterText(message))"
|
|
|
|
|
|
@click="handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
|
|
|
|
|
|
class="welcome-quick-actions"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p class="welcome-quick-actions-title">您可以对我进行以下操作:</p>
|
|
|
|
|
|
<div class="welcome-quick-action-grid">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="action in message.welcomeQuickActions"
|
|
|
|
|
|
:key="`${message.id}-${action.label}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="welcome-quick-action-btn"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@click="runWelcomeQuickAction(action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="action.icon"></i>
|
|
|
|
|
|
<span>{{ action.label }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<div v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.meta?.length" class="message-meta-row">
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<span
|
|
|
|
|
|
v-for="item in message.meta"
|
|
|
|
|
|
:key="item"
|
|
|
|
|
|
class="message-meta-chip"
|
|
|
|
|
|
:class="message.metaTone"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item }}
|
|
|
|
|
|
</span>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
<div
|
2026-05-22 16:00:19 +08:00
|
|
|
|
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
2026-05-21 16:09:47 +08:00
|
|
|
|
class="message-suggested-actions"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="action in message.suggestedActions"
|
|
|
|
|
|
:key="`${message.id}-${action.action_type}-${action.label}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-suggested-action-btn"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
selected: isSuggestedActionSelected(message, action),
|
|
|
|
|
|
locked: message.suggestedActionsLocked
|
|
|
|
|
|
}"
|
|
|
|
|
|
:disabled="message.suggestedActionsLocked || submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@click="handleSuggestedAction(message, action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="message-suggested-action-icon" aria-hidden="true">
|
|
|
|
|
|
<i :class="action.icon || 'mdi mdi-shape-outline'"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="message-suggested-action-copy">
|
|
|
|
|
|
<span class="message-suggested-action-title">{{ action.label }}</span>
|
|
|
|
|
|
<small v-if="action.description">{{ action.description }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<i class="message-suggested-action-arrow mdi mdi-arrow-right" aria-hidden="true"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
|
|
|
|
|
|
<strong>风险标签</strong>
|
|
|
|
|
|
<div class="message-detail-chip-row">
|
|
|
|
|
|
<span v-for="item in message.riskFlags" :key="item" class="message-risk-chip">{{ item }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<details
|
2026-05-22 16:00:19 +08:00
|
|
|
|
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.citations?.length"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
class="message-detail-block message-citation-disclosure"
|
|
|
|
|
|
>
|
|
|
|
|
|
<summary>
|
|
|
|
|
|
<strong>引用依据</strong>
|
|
|
|
|
|
<span>{{ message.citations.length }} 项</span>
|
|
|
|
|
|
<i class="mdi mdi-chevron-down"></i>
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div class="message-citation-list">
|
|
|
|
|
|
<article v-for="item in message.citations" :key="`${message.id}-${item.code}`" class="message-citation-card">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<span>{{ item.title }}</span>
|
|
|
|
|
|
<small>{{ item.version || item.source_type }}</small>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<p>{{ item.excerpt || item.code }}</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && !message.reviewPayload && message.queryPayload?.resultType === 'expense_claim_list'"
|
|
|
|
|
|
class="message-detail-block expense-query-block"
|
|
|
|
|
|
>
|
2026-05-21 16:09:47 +08:00
|
|
|
|
<strong>
|
2026-05-22 16:00:19 +08:00
|
|
|
|
{{ message.queryPayload.title || (message.queryPayload.selectionMode === 'draft_association' ? '选择关联草稿' : '最近 5 条筛选结果') }}
|
2026-05-21 16:09:47 +08:00
|
|
|
|
</strong>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
<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"
|
2026-05-21 16:09:47 +08:00
|
|
|
|
:class="{
|
|
|
|
|
|
selectable: message.queryPayload.selectionMode === 'draft_association',
|
|
|
|
|
|
selected: message.selectedQueryRecordId === record.claimId || message.queryPayload.selectedClaimId === record.claimId,
|
|
|
|
|
|
locked: message.querySelectionLocked || message.queryPayload.selectionLocked
|
|
|
|
|
|
}"
|
|
|
|
|
|
:disabled="message.queryPayload.selectionMode === 'draft_association' && (message.querySelectionLocked || message.queryPayload.selectionLocked)"
|
|
|
|
|
|
@click="handleExpenseQueryRecordClick(message, record)"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<div v-if="record.riskItems?.length" class="expense-query-risk-row">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="risk in record.riskItems"
|
|
|
|
|
|
:key="`${message.id}-${record.claimId}-${risk.key}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="expense-query-risk-chip"
|
|
|
|
|
|
:class="risk.level"
|
|
|
|
|
|
@click.stop="appendExpenseQueryRiskToConversation(record, risk)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{{ record.claimNo }}</span>
|
|
|
|
|
|
<strong>{{ risk.levelLabel }}</strong>
|
|
|
|
|
|
<em>{{ risk.title }}</em>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</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>
|
2026-05-21 16:09:47 +08:00
|
|
|
|
<span>{{ message.queryPayload.emptyText || '当前没有可直接展开的近期待办单据。' }}</span>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<p
|
|
|
|
|
|
v-if="buildExpenseQueryHint(message.queryPayload)"
|
|
|
|
|
|
class="expense-query-hint message-answer-markdown"
|
|
|
|
|
|
v-html="renderMarkdown(buildExpenseQueryHint(message.queryPayload))"
|
|
|
|
|
|
@click="handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
|
2026-05-21 23:53:03 +08:00
|
|
|
|
<div class="review-plain-followup">
|
|
|
|
|
|
<template
|
2026-05-22 16:00:19 +08:00
|
|
|
|
v-for="followup in [buildReviewPlainFollowupForMessage(message)]"
|
2026-05-21 23:53:03 +08:00
|
|
|
|
:key="`${message.id}-review-followup`"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
>
|
2026-05-22 08:58:59 +08:00
|
|
|
|
<h3
|
|
|
|
|
|
class="review-plain-lead"
|
|
|
|
|
|
:class="{ danger: followup.tone === 'danger' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ followup.lead }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p v-if="followup.summary" class="review-plain-summary">
|
|
|
|
|
|
{{ followup.summary }}
|
|
|
|
|
|
</p>
|
2026-05-21 23:53:03 +08:00
|
|
|
|
<ul v-if="followup.items.length" class="review-plain-list">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="item in followup.items"
|
|
|
|
|
|
:key="`${message.id}-${item.key}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="review-plain-label">{{ item.label }}:</span>
|
|
|
|
|
|
<span>{{ item.text }}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-for="line in followup.notes"
|
|
|
|
|
|
:key="`${message.id}-note-${line}`"
|
|
|
|
|
|
class="review-plain-note"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ line }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p v-if="canUseInlineSaveDraft(message)" class="review-inline-save-copy">
|
|
|
|
|
|
请核查上面的关键信息。您也可以暂时不处理上述的这些内容,我可以帮你先保存为
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-inline-draft-link"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@click="handleInlineSaveDraft(message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
草稿
|
|
|
|
|
|
</button>
|
|
|
|
|
|
。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-23 19:54:42 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="buildReviewNextStepRichCopyForMessage(message)"
|
|
|
|
|
|
class="review-next-step-rich-copy message-answer-markdown"
|
|
|
|
|
|
v-html="renderMarkdown(buildReviewNextStepRichCopyForMessage(message))"
|
|
|
|
|
|
@click="handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
2026-05-21 14:24:51 +08:00
|
|
|
|
<div
|
2026-05-21 23:53:03 +08:00
|
|
|
|
v-if="resolveReviewFooterActions(message.reviewPayload).length"
|
2026-05-21 14:24:51 +08:00
|
|
|
|
class="review-footer-actions"
|
|
|
|
|
|
>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div class="review-footer-btn-row">
|
|
|
|
|
|
<button
|
2026-05-21 23:53:03 +08:00
|
|
|
|
v-for="action in resolveReviewFooterActions(message.reviewPayload)"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
:key="`${message.id}-${action.action_type}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:class="['review-footer-btn', action.emphasis === 'primary' ? 'primary' : '']"
|
2026-05-21 14:24:51 +08:00
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
@click="handleReviewAction(message, action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ action.label || buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload" class="draft-preview">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<strong>{{ message.draftPayload.title }}</strong>
|
|
|
|
|
|
<span>待人工确认</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<pre>{{ message.draftPayload.body }}</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="message.attachments?.length" class="message-files">
|
|
|
|
|
|
<span v-for="file in message.attachments" :key="file" class="file-chip">
|
|
|
|
|
|
<i class="mdi mdi-paperclip"></i>
|
|
|
|
|
|
{{ file }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form class="composer" @submit.prevent="submitComposer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
class="hidden-file-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
|
|
|
|
|
@change="handleFilesChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!isKnowledgeSession && attachedFiles.length" class="composer-files-panel">
|
|
|
|
|
|
<div class="composer-files-head">
|
|
|
|
|
|
<strong>已添加 {{ attachedFiles.length }} 份附件</strong>
|
|
|
|
|
|
<div class="composer-files-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="hiddenAttachedFileCount"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-file-link"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
@click="toggleAttachedFilesExpanded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="composerFilesExpanded ? 'mdi mdi-chevron-up' : 'mdi mdi-chevron-down'"></i>
|
|
|
|
|
|
{{ composerFilesExpanded ? '收起附件' : `展开其余 ${hiddenAttachedFileCount} 份` }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-file-link danger"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
@click="clearAttachedFiles"
|
|
|
|
|
|
>
|
|
|
|
|
|
清空
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="composer-file-chip-row">
|
|
|
|
|
|
<span v-for="file in visibleAttachedFiles" :key="file.name" class="file-chip active composer-file-chip">
|
|
|
|
|
|
<i class="mdi mdi-paperclip"></i>
|
|
|
|
|
|
<span class="file-chip-label" :title="file.name">{{ file.name }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="file-chip-remove"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
:aria-label="`移除 ${file.name}`"
|
|
|
|
|
|
@click="removeAttachedFile(file)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="hiddenAttachedFileCount"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="file-chip active summary"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
@click="toggleAttachedFilesExpanded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-file-multiple-outline"></i>
|
|
|
|
|
|
另外 {{ hiddenAttachedFileCount }} 份
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="composerFilesExpanded && hiddenAttachedFileCount" class="composer-files-expanded">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="file in attachedFiles.slice(visibleAttachedFiles.length)"
|
|
|
|
|
|
:key="`${file.name}-expanded`"
|
|
|
|
|
|
class="composer-expanded-file"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="composer-expanded-file-copy">
|
|
|
|
|
|
<i class="mdi mdi-file-document-outline"></i>
|
|
|
|
|
|
<span :title="file.name">{{ file.name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-expanded-file-remove"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
:aria-label="`移除 ${file.name}`"
|
|
|
|
|
|
@click="removeAttachedFile(file)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="composer-row" :class="{ 'knowledge-mode': isKnowledgeSession }">
|
|
|
|
|
|
<div v-if="!isKnowledgeSession" class="composer-leading-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="tool-btn composer-side-btn"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
aria-label="上传附件"
|
|
|
|
|
|
@click="triggerFileUpload"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-paperclip"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="composer-date-anchor">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="tool-btn composer-side-btn"
|
|
|
|
|
|
:class="{ active: composerDatePickerOpen }"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
aria-label="选择业务发生时间"
|
|
|
|
|
|
:aria-expanded="composerDatePickerOpen"
|
|
|
|
|
|
@click.stop="toggleComposerDatePicker"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-calendar-range"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="composerDatePickerOpen"
|
|
|
|
|
|
class="composer-date-popover"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-label="业务发生时间"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="composer-date-mode-tabs">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-mode-btn"
|
|
|
|
|
|
:class="{ active: composerDateMode === 'single' }"
|
|
|
|
|
|
@click="setComposerDateMode('single')"
|
|
|
|
|
|
>
|
|
|
|
|
|
当天
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-mode-btn"
|
|
|
|
|
|
:class="{ active: composerDateMode === 'range' }"
|
|
|
|
|
|
@click="setComposerDateMode('range')"
|
|
|
|
|
|
>
|
|
|
|
|
|
时间段
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="composerDateMode === 'single'" class="composer-date-fields">
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>日期</span>
|
2026-05-20 14:21:56 +08:00
|
|
|
|
<input v-model="composerSingleDate" type="date" @change="handleComposerDateInputChange" />
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="composer-date-fields composer-date-fields-range">
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>开始</span>
|
2026-05-20 14:21:56 +08:00
|
|
|
|
<input v-model="composerRangeStartDate" type="date" @change="handleComposerDateInputChange" />
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</label>
|
|
|
|
|
|
<span class="composer-date-range-sep">至</span>
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>结束</span>
|
2026-05-20 14:21:56 +08:00
|
|
|
|
<input
|
|
|
|
|
|
v-model="composerRangeEndDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
:min="composerRangeStartDate"
|
|
|
|
|
|
@change="handleComposerDateInputChange"
|
|
|
|
|
|
/>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p v-if="composerDateMode === 'range' && !composerCanApplyDateSelection" class="composer-date-hint">
|
|
|
|
|
|
请确认结束日期不早于开始日期。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="composer-date-popover-actions">
|
|
|
|
|
|
<button type="button" class="composer-date-cancel-btn" @click="closeComposerDatePicker">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-apply-btn"
|
|
|
|
|
|
:disabled="!composerCanApplyDateSelection"
|
|
|
|
|
|
@click="applyComposerDateSelection"
|
|
|
|
|
|
>
|
|
|
|
|
|
插入标签
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-26 09:15:14 +08:00
|
|
|
|
<div v-if="canShowTravelCalculator" class="travel-calculator-anchor">
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="tool-btn composer-side-btn travel-calculator-trigger"
|
|
|
|
|
|
:class="{ active: travelCalculatorOpen }"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
aria-label="差旅计算器"
|
|
|
|
|
|
title="差旅计算器"
|
|
|
|
|
|
:aria-expanded="travelCalculatorOpen"
|
|
|
|
|
|
@click.stop="toggleTravelCalculator"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-calculator"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="travelCalculatorOpen"
|
|
|
|
|
|
class="travel-calculator-popover"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-label="差旅计算器"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="travel-calculator-mini-head">
|
|
|
|
|
|
<strong>差旅计算器</strong>
|
|
|
|
|
|
<span>按规则中心差旅表测算</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="travel-calculator-form">
|
|
|
|
|
|
<label class="travel-calculator-field">
|
|
|
|
|
|
<span>实际天数</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="travelCalculatorForm.days"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
step="1"
|
|
|
|
|
|
inputmode="numeric"
|
|
|
|
|
|
:disabled="travelCalculatorBusy"
|
|
|
|
|
|
@keydown.enter.prevent="submitTravelCalculator"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="travel-calculator-field">
|
|
|
|
|
|
<span>出差地点</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="travelCalculatorForm.location"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="例如:北京、成都"
|
|
|
|
|
|
:disabled="travelCalculatorBusy"
|
|
|
|
|
|
@keydown.enter.prevent="submitTravelCalculator"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p v-if="travelCalculatorError" class="travel-calculator-error">
|
|
|
|
|
|
{{ travelCalculatorError }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="composer-date-popover-actions">
|
|
|
|
|
|
<button type="button" class="composer-date-cancel-btn" :disabled="travelCalculatorBusy" @click="closeTravelCalculator">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-apply-btn"
|
|
|
|
|
|
:disabled="!travelCalculatorCanSubmit"
|
|
|
|
|
|
@click="submitTravelCalculator"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ travelCalculatorBusy ? '计算中...' : 'AI计算' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="composer-shell">
|
|
|
|
|
|
<div class="composer-shell-body">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="tag in composerBusinessTimeTags"
|
|
|
|
|
|
:key="tag.id"
|
|
|
|
|
|
class="composer-biz-time-tag"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-calendar-check"></i>
|
|
|
|
|
|
<span class="composer-biz-time-tag-label">{{ tag.label }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-biz-time-tag-remove"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
aria-label="移除业务发生时间"
|
|
|
|
|
|
@click="removeComposerBusinessTimeTag(tag.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref="composerTextareaRef"
|
|
|
|
|
|
v-model="composerDraft"
|
|
|
|
|
|
rows="1"
|
|
|
|
|
|
:placeholder="composerPlaceholder"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@input="handleComposerInput"
|
|
|
|
|
|
@keydown.enter.exact.prevent="handleComposerEnter"
|
|
|
|
|
|
@keydown.ctrl.enter.prevent="submitComposer"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="send-btn composer-side-btn" type="submit" :disabled="!canSubmit || reviewActionBusy || sessionSwitchBusy" aria-label="发送">
|
|
|
|
|
|
<i :class="submitting ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="hasInsightPanelContent"
|
|
|
|
|
|
class="insight-panel-shell"
|
|
|
|
|
|
:class="{ collapsed: !showInsightPanel }"
|
|
|
|
|
|
:aria-hidden="(!showInsightPanel).toString()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<aside class="insight-panel">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!isKnowledgeSession"
|
|
|
|
|
|
class="insight-head"
|
|
|
|
|
|
:class="{ 'review-mode': activeReviewPayload || isReviewFlowDrawer }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div v-if="!activeReviewPayload && !isReviewFlowDrawer" class="insight-head-eyebrow">
|
|
|
|
|
|
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="review-insight-title-row">
|
|
|
|
|
|
<div class="review-insight-title-copy">
|
|
|
|
|
|
<h3>{{ reviewDrawerTitle }}</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.title }}</h3>
|
|
|
|
|
|
<p v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.summary }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="activeReviewPayload || isReviewFlowDrawer" class="review-insight-tools">
|
|
|
|
|
|
<button
|
2026-05-22 16:00:19 +08:00
|
|
|
|
v-if="activeReviewPayload && reviewOverviewDrawerAvailable"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-insight-switch-icon-btn"
|
2026-05-20 21:00:47 +08:00
|
|
|
|
:class="{
|
|
|
|
|
|
available: true,
|
|
|
|
|
|
active: isReviewOverviewDrawer
|
|
|
|
|
|
}"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
title="报销识别核对"
|
|
|
|
|
|
aria-label="报销识别核对"
|
|
|
|
|
|
@click="switchToReviewOverviewDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="isReviewOverviewDrawer ? 'mdi mdi-clipboard-check' : 'mdi mdi-clipboard-check-outline'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="activeReviewPayload && reviewDocumentDrawerAvailable"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-insight-switch-icon-btn"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
:class="{
|
|
|
|
|
|
available: reviewDocumentDrawerAvailable,
|
|
|
|
|
|
active: reviewDocumentDrawerAvailable && isReviewDocumentDrawer
|
|
|
|
|
|
}"
|
2026-05-20 21:00:47 +08:00
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
title="单据识别"
|
|
|
|
|
|
aria-label="单据识别"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
@click="toggleReviewDocumentDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="reviewDocumentDrawerIcon"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
2026-05-20 21:00:47 +08:00
|
|
|
|
v-if="activeReviewPayload && reviewRiskDrawerAvailable"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-insight-switch-icon-btn risk"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
available: reviewRiskDrawerAvailable,
|
|
|
|
|
|
active: reviewRiskDrawerAvailable && isReviewRiskDrawer
|
|
|
|
|
|
}"
|
2026-05-20 21:00:47 +08:00
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
title="显示风险"
|
|
|
|
|
|
aria-label="显示风险"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
@click="toggleReviewRiskDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="reviewRiskDrawerIcon"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-insight-switch-icon-btn flow"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
available: reviewFlowDrawerAvailable,
|
|
|
|
|
|
active: reviewFlowDrawerAvailable && isReviewFlowDrawer,
|
|
|
|
|
|
running: flowOverallStatusTone === 'running'
|
|
|
|
|
|
}"
|
|
|
|
|
|
:disabled="!reviewFlowDrawerAvailable || submitting || reviewActionBusy"
|
2026-05-20 21:00:47 +08:00
|
|
|
|
title="调用流程"
|
|
|
|
|
|
aria-label="调用流程"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
@click="toggleReviewFlowDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="reviewFlowDrawerIcon"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="confidence-card" v-if="!activeReviewPayload && !isReviewFlowDrawer">
|
|
|
|
|
|
<span>{{ currentInsight.metricLabel }}</span>
|
|
|
|
|
|
<strong>{{ currentInsight.metricValue }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Transition name="insight-switch" mode="out-in">
|
2026-05-23 19:54:42 +08:00
|
|
|
|
<div
|
|
|
|
|
|
:key="`${activeSessionType}-${currentInsight.intent}-${currentInsight.title}-${reviewDrawerMode}`"
|
|
|
|
|
|
class="insight-body"
|
|
|
|
|
|
:class="{ 'document-review-body': isReviewDocumentDrawer }"
|
|
|
|
|
|
>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<template v-if="isKnowledgeSession">
|
|
|
|
|
|
<section class="insight-card knowledge-hot-card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h4>热门问题 Top 10</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="knowledge-question-list">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(item, index) in hotKnowledgeQuestions"
|
|
|
|
|
|
:key="item"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="knowledge-question-btn"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
|
|
|
|
|
|
@click="askHotKnowledgeQuestion(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="knowledge-question-index"
|
|
|
|
|
|
:class="resolveKnowledgeRankTone(index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ resolveKnowledgeRankLabel(index) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="knowledge-question-copy">{{ item }}</span>
|
|
|
|
|
|
<i class="mdi mdi-arrow-top-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="isReviewFlowDrawer">
|
|
|
|
|
|
<section class="review-flow-panel">
|
|
|
|
|
|
<div class="review-flow-summary">
|
|
|
|
|
|
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
|
|
|
|
|
|
<span>总耗时 {{ flowTotalDurationText }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="flow-icon-btn"
|
|
|
|
|
|
:disabled="!flowRunId || flowRefreshBusy"
|
|
|
|
|
|
title="刷新流程"
|
|
|
|
|
|
aria-label="刷新流程"
|
|
|
|
|
|
@click="refreshFlowRunDetail"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="flowSteps.length" class="review-flow-list">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="(step, index) in flowSteps"
|
|
|
|
|
|
:key="step.key"
|
|
|
|
|
|
class="flow-step-item"
|
|
|
|
|
|
:class="step.status"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flow-step-rail">
|
|
|
|
|
|
<span>{{ index + 1 }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flow-step-card">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<strong>{{ step.title }}</strong>
|
|
|
|
|
|
<div class="flow-step-side">
|
|
|
|
|
|
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
|
|
|
|
|
|
<time>{{ formatFlowStepDuration(step) }}</time>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
|
|
|
|
|
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
|
|
|
|
|
|
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="flow-empty-state compact">
|
|
|
|
|
|
<i class="mdi mdi-timeline-question-outline"></i>
|
|
|
|
|
|
<strong>暂无识别流程</strong>
|
|
|
|
|
|
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent">
|
|
|
|
|
|
<template v-if="activeReviewPayload">
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<template v-if="reviewOverviewDrawerAvailable && !isReviewDocumentDrawer && !isReviewRiskDrawer && !isReviewFlowDrawer">
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<section class="review-side-card review-side-overview-card">
|
|
|
|
|
|
<div class="review-side-intent-row">
|
|
|
|
|
|
<i class="mdi mdi-account-outline"></i>
|
|
|
|
|
|
<span>用户意图:</span>
|
|
|
|
|
|
<strong>{{ reviewIntentText }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<section class="review-side-grid compact">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="item in reviewFactCards"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
class="review-side-metric-card"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
editable: item.editor,
|
|
|
|
|
|
editing: reviewInlineEditorKey === item.key,
|
2026-05-21 09:28:33 +08:00
|
|
|
|
invalid: Boolean(reviewInlineErrors[item.key]),
|
|
|
|
|
|
wide: item.wide
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}"
|
|
|
|
|
|
@click="openInlineReviewEditor(item.key)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="review-side-metric-icon">
|
|
|
|
|
|
<i :class="item.icon"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="review-side-metric-copy">
|
|
|
|
|
|
<small>{{ item.label }}</small>
|
|
|
|
|
|
<template v-if="reviewInlineEditorKey === item.key && item.editor === 'date'">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="reviewInlineForm[item.modelKey]"
|
|
|
|
|
|
class="review-inline-input"
|
|
|
|
|
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:placeholder="`仅支持 ${DATE_INPUT_FORMAT}`"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@input="clearInlineReviewFieldError(item.key)"
|
|
|
|
|
|
@blur="commitInlineReviewEditor"
|
|
|
|
|
|
@keydown.enter.prevent="commitInlineReviewEditor"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="reviewInlineEditorKey === item.key && item.editor === 'amount'">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="reviewInlineForm[item.modelKey]"
|
|
|
|
|
|
class="review-inline-input"
|
|
|
|
|
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:placeholder="item.placeholder"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@input="clearInlineReviewFieldError(item.key)"
|
|
|
|
|
|
@blur="commitInlineReviewEditor"
|
|
|
|
|
|
@keydown.enter.prevent="commitInlineReviewEditor"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="reviewInlineEditorKey === item.key && item.editor === 'text'">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="reviewInlineForm[item.modelKey]"
|
|
|
|
|
|
class="review-inline-input"
|
|
|
|
|
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:placeholder="item.placeholder"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@input="clearInlineReviewFieldError(item.key)"
|
|
|
|
|
|
@blur="commitInlineReviewEditor"
|
|
|
|
|
|
@keydown.enter.prevent="commitInlineReviewEditor"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<template v-else-if="reviewInlineEditorKey === item.key && item.editor === 'textarea'">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="reviewInlineForm[item.modelKey]"
|
|
|
|
|
|
class="review-inline-input review-inline-textarea"
|
|
|
|
|
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
|
|
|
|
|
:placeholder="item.placeholder"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@input="clearInlineReviewFieldError(item.key)"
|
|
|
|
|
|
@blur="commitInlineReviewEditor"
|
|
|
|
|
|
@keydown.enter.stop
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
</template>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<template v-else-if="reviewInlineEditorKey === item.key && item.editor === 'select'">
|
|
|
|
|
|
<div class="review-inline-select-list" @click.stop>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="scene in REVIEW_SCENE_OPTIONS"
|
|
|
|
|
|
:key="scene"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-inline-select-option"
|
|
|
|
|
|
:class="{ active: reviewInlineForm.scene_label === scene }"
|
|
|
|
|
|
@click.stop="selectInlineScene(scene)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ scene }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-if="reviewInlineForm.scene_label === REVIEW_SCENE_OTHER_OPTION"
|
|
|
|
|
|
v-model="reviewInlineForm.reason_value"
|
|
|
|
|
|
class="review-inline-input review-inline-select-custom"
|
|
|
|
|
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="请输入具体事由"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@input="clearInlineReviewFieldError(item.key)"
|
|
|
|
|
|
@blur="commitInlineReviewEditor"
|
|
|
|
|
|
@keydown.enter.prevent="commitInlineReviewEditor"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<strong v-else :title="item.value">{{ item.value }}</strong>
|
|
|
|
|
|
<span v-if="reviewInlineErrors[item.key]" class="review-inline-error">
|
|
|
|
|
|
{{ reviewInlineErrors[item.key] }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-if="item.key !== 'attachments'" class="review-side-edit-hint">修改</span>
|
|
|
|
|
|
<span v-else class="review-side-edit-hint upload">{{ reviewInlinePendingFiles.length ? '已选择' : '上传' }}</span>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="review-side-card">
|
|
|
|
|
|
<div class="review-side-head">
|
|
|
|
|
|
<strong>报销分类</strong>
|
|
|
|
|
|
<span class="review-side-confidence">置信度 {{ reviewPanelConfidence }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="review-side-category-grid">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="item in reviewCategoryOptions"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-side-category-card"
|
|
|
|
|
|
:class="{ active: item.active }"
|
|
|
|
|
|
@click="selectReviewCategory(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="review-side-category-copy">
|
|
|
|
|
|
<strong>{{ item.label }}</strong>
|
|
|
|
|
|
<p>{{ item.is_other && reviewSelectedOtherCategory ? reviewSelectedOtherCategory : item.caption }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<i v-if="item.active" class="mdi mdi-check-circle review-side-group-check"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="reviewOtherCategoryOpen" class="review-other-category-popover">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="item in reviewOtherCategoryOptions"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-other-category-option"
|
|
|
|
|
|
:class="{ active: reviewSelectedOtherCategory === item.label }"
|
|
|
|
|
|
@click="selectReviewOtherCategory(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.label }} · {{ item.confidenceLabel }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="isReviewFlowDrawer">
|
|
|
|
|
|
<section class="review-flow-panel">
|
|
|
|
|
|
<div class="review-flow-summary">
|
|
|
|
|
|
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
|
|
|
|
|
|
<span>总耗时 {{ flowTotalDurationText }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="flow-icon-btn"
|
|
|
|
|
|
:disabled="!flowRunId || flowRefreshBusy"
|
|
|
|
|
|
title="刷新流程"
|
|
|
|
|
|
aria-label="刷新流程"
|
|
|
|
|
|
@click="refreshFlowRunDetail"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="flowSteps.length" class="review-flow-list">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="(step, index) in flowSteps"
|
|
|
|
|
|
:key="step.key"
|
|
|
|
|
|
class="flow-step-item"
|
|
|
|
|
|
:class="step.status"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flow-step-rail">
|
|
|
|
|
|
<span>{{ index + 1 }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flow-step-card">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<strong>{{ step.title }}</strong>
|
|
|
|
|
|
<div class="flow-step-side">
|
|
|
|
|
|
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
|
|
|
|
|
|
<time>{{ formatFlowStepDuration(step) }}</time>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
|
|
|
|
|
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
|
|
|
|
|
|
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="flow-empty-state compact">
|
|
|
|
|
|
<i class="mdi mdi-timeline-question-outline"></i>
|
|
|
|
|
|
<strong>暂无识别流程</strong>
|
|
|
|
|
|
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="isReviewDocumentDrawer">
|
|
|
|
|
|
<section class="review-side-card review-document-switch-card review-ticket-drawer">
|
|
|
|
|
|
<div class="review-side-head review-document-switch-head">
|
|
|
|
|
|
<div class="review-side-head-copy">
|
|
|
|
|
|
<strong>票据识别结果卡片</strong>
|
|
|
|
|
|
<p>逐张查看 OCR 结果,可直接修正后再切回核对滑窗。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="review-document-nav">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-document-nav-btn"
|
|
|
|
|
|
:disabled="activeReviewDocumentIndex === 0"
|
|
|
|
|
|
aria-label="上一张票据"
|
|
|
|
|
|
@click="goReviewDocument(-1)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-chevron-left"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span>{{ activeReviewDocumentIndex + 1 }} / {{ reviewDocumentCount }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-document-nav-btn"
|
|
|
|
|
|
:disabled="activeReviewDocumentIndex >= reviewDocumentCount - 1"
|
|
|
|
|
|
aria-label="下一张票据"
|
|
|
|
|
|
@click="goReviewDocument(1)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="activeReviewDocument" class="review-document-stage">
|
|
|
|
|
|
<div class="review-document-stage-head">
|
|
|
|
|
|
<div class="review-document-stage-copy">
|
|
|
|
|
|
<strong :title="activeReviewDocument.filename">{{ activeReviewDocument.filename }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="review-document-meta-chip-row">
|
|
|
|
|
|
<span class="review-document-meta-chip">{{ activeReviewDocument.documentTypeLabel }}</span>
|
|
|
|
|
|
<span class="review-document-meta-chip">{{ activeReviewDocument.expenseTypeLabel }}</span>
|
|
|
|
|
|
<span class="review-document-meta-chip confidence">{{ activeReviewDocument.confidenceLabel }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="review-document-scroll">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="review-document-preview-card"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
activeReviewDocumentPreview?.kind || 'file',
|
|
|
|
|
|
{ clickable: canPreviewActiveReviewDocument }
|
|
|
|
|
|
]"
|
|
|
|
|
|
:role="canPreviewActiveReviewDocument ? 'button' : null"
|
|
|
|
|
|
:tabindex="canPreviewActiveReviewDocument ? 0 : null"
|
|
|
|
|
|
@click="canPreviewActiveReviewDocument ? openActiveReviewDocumentPreview() : null"
|
|
|
|
|
|
@keydown.enter.prevent="canPreviewActiveReviewDocument ? openActiveReviewDocumentPreview() : null"
|
|
|
|
|
|
@keydown.space.prevent="canPreviewActiveReviewDocument ? openActiveReviewDocumentPreview() : null"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="activeReviewDocumentPreview?.kind === 'image' && activeReviewDocumentPreview?.url"
|
|
|
|
|
|
:src="activeReviewDocumentPreview.url"
|
|
|
|
|
|
:alt="activeReviewDocument.filename"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div v-else-if="activeReviewDocumentPreview?.kind === 'pdf'" class="review-document-preview-placeholder">
|
|
|
|
|
|
<i class="mdi mdi-file-pdf-box"></i>
|
|
|
|
|
|
<strong>PDF 票据文件</strong>
|
|
|
|
|
|
<p>当前文件还没有生成图片预览,可先核对下方识别字段。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="review-document-preview-placeholder">
|
|
|
|
|
|
<i class="mdi mdi-file-search-outline"></i>
|
|
|
|
|
|
<strong>当前无可预览票据</strong>
|
|
|
|
|
|
<p>这张票据还没有可用预览,可先核对下方识别字段。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="review-document-edit-field summary">
|
|
|
|
|
|
<span>票据摘要</span>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="activeReviewDocument.summary"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
placeholder="可根据票据图片修正 OCR 摘要"
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="review-document-edit-field">
|
|
|
|
|
|
<span>票据场景</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="activeReviewDocument.scene_label"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
2026-05-23 19:54:42 +08:00
|
|
|
|
placeholder="例如:出租车/网约车票据 / 火车/高铁票"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="activeReviewDocument.fields.length" class="review-document-edit-grid">
|
|
|
|
|
|
<label
|
|
|
|
|
|
v-for="field in activeReviewDocument.fields"
|
|
|
|
|
|
:key="`${activeReviewDocument.filename}-${field.label}`"
|
|
|
|
|
|
class="review-document-edit-field"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{{ field.label }}</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="field.value"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:disabled="submitting || reviewActionBusy"
|
|
|
|
|
|
:placeholder="`修正 ${field.label}`"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="review-side-empty compact">
|
|
|
|
|
|
<span class="review-side-empty-icon">
|
|
|
|
|
|
<i class="mdi mdi-text-recognition"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<strong>暂无结构化字段</strong>
|
|
|
|
|
|
<p>当前只返回了摘要信息,你仍然可以直接修改上面的票据摘要。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="activeReviewDocument.warnings?.length" class="review-document-warning-list">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="warning in activeReviewDocument.warnings"
|
|
|
|
|
|
:key="`${activeReviewDocument.filename}-${warning}`"
|
|
|
|
|
|
class="review-document-warning-item"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-alert-circle-outline"></i>
|
|
|
|
|
|
<span>{{ warning }}</span>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="isReviewRiskDrawer">
|
|
|
|
|
|
<section class="review-side-card review-side-risk-card">
|
|
|
|
|
|
<div class="review-side-head">
|
|
|
|
|
|
<div class="review-side-head-copy">
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<strong>差旅合规提示</strong>
|
|
|
|
|
|
<p>结合票据识别结果与差旅规则,逐项查看需要处理的风险点。</p>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="review-side-risk-summary">{{ reviewRiskSummary }}</p>
|
2026-05-20 14:21:56 +08:00
|
|
|
|
<div v-if="reviewRiskItems.length" class="review-side-risk-list">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="item in reviewRiskItems"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-side-risk-item"
|
|
|
|
|
|
:class="item.level"
|
2026-05-21 09:28:33 +08:00
|
|
|
|
@click="appendReviewRiskBriefToConversation(item)"
|
2026-05-20 14:21:56 +08:00
|
|
|
|
>
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<span class="review-side-risk-icon" :title="item.levelLabel">
|
2026-05-20 14:21:56 +08:00
|
|
|
|
<i :class="item.icon"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="review-side-risk-copy">
|
|
|
|
|
|
<strong>{{ item.title }}</strong>
|
|
|
|
|
|
<p>{{ item.summary }}</p>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="review-side-risk-meta">
|
|
|
|
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div v-else-if="reviewRiskEmpty" class="review-side-empty">
|
|
|
|
|
|
<span class="review-side-empty-icon">
|
|
|
|
|
|
<i class="mdi mdi-shield-check-outline"></i>
|
|
|
|
|
|
</span>
|
2026-05-21 09:28:33 +08:00
|
|
|
|
<strong>暂无风险提示</strong>
|
|
|
|
|
|
<p>当前没有需要额外处理的结构化风险点。</p>
|
2026-05-19 17:24:13 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="reviewHasUnsavedChanges"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-side-save-pill"
|
|
|
|
|
|
:disabled="reviewActionBusy || submitting"
|
|
|
|
|
|
@click="saveInlineReviewChanges"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-content-save-outline"></i>
|
|
|
|
|
|
保存右侧修改
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
<section v-if="currentInsight.agent.citations?.length && !currentInsight.agent.queryPayload && !activeReviewPayload" class="insight-card">
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h4>制度依据</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="citation-stack">
|
|
|
|
|
|
<article v-for="item in currentInsight.agent.citations" :key="item.code" class="citation-card">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<strong>{{ item.title }}</strong>
|
|
|
|
|
|
<span>{{ item.version || item.source_type }}</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<p>{{ item.excerpt || item.code }}</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="!activeReviewPayload">
|
|
|
|
|
|
<section class="insight-card primary">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h4>识别结果</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="note-block">
|
|
|
|
|
|
<strong>{{ currentInsight.title }}</strong>
|
|
|
|
|
|
<p>{{ currentInsight.summary }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-if="currentInsight.agent.riskFlags?.length" class="insight-card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<h4>风险标签</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="capability-chip-row">
|
|
|
|
|
|
<span v-for="item in currentInsight.agent.riskFlags" :key="item" class="risk-chip">{{ item }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
|
:open="deleteSessionDialogOpen"
|
|
|
|
|
|
badge="删除会话"
|
|
|
|
|
|
badge-tone="danger"
|
|
|
|
|
|
title="确认删除当前会话内容?"
|
|
|
|
|
|
description="删除后将清空当前类型会话下的全部对话内容,且无法恢复。"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
confirm-text="确认删除"
|
|
|
|
|
|
busy-text="删除中..."
|
|
|
|
|
|
confirm-tone="danger"
|
|
|
|
|
|
confirm-icon="mdi mdi-delete-outline"
|
|
|
|
|
|
:busy="deleteSessionBusy"
|
|
|
|
|
|
@close="closeDeleteSessionDialog"
|
|
|
|
|
|
@confirm="confirmDeleteCurrentSession"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
<ConfirmDialog
|
|
|
|
|
|
:open="applicationSubmitConfirmDialog.open"
|
|
|
|
|
|
badge="提交确认"
|
|
|
|
|
|
badge-tone="primary"
|
|
|
|
|
|
title="确认提交当前费用申请?"
|
|
|
|
|
|
description="提交后申请将进入领导审核流程,并同步纳入预算管理口径,请确认关键申请信息和预计费用已经核对无误。"
|
|
|
|
|
|
cancel-text="再检查一下"
|
|
|
|
|
|
confirm-text="确认提交"
|
|
|
|
|
|
busy-text="提交中..."
|
|
|
|
|
|
confirm-tone="primary"
|
|
|
|
|
|
confirm-icon="mdi mdi-send-check-outline"
|
|
|
|
|
|
:busy="reviewActionBusy"
|
|
|
|
|
|
@close="closeApplicationSubmitConfirm"
|
|
|
|
|
|
@confirm="confirmApplicationSubmit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-23 19:54:42 +08:00
|
|
|
|
<ConfirmDialog
|
|
|
|
|
|
:open="nextStepConfirmDialog.open"
|
|
|
|
|
|
badge="提交确认"
|
|
|
|
|
|
badge-tone="primary"
|
|
|
|
|
|
title="确认提交当前单据?"
|
|
|
|
|
|
description="提交后单据将进入审批流程,请确认关键信息、票据和风险提示已经核对无误。"
|
|
|
|
|
|
cancel-text="再检查一下"
|
|
|
|
|
|
confirm-text="确认提交"
|
|
|
|
|
|
busy-text="提交中..."
|
|
|
|
|
|
confirm-tone="primary"
|
|
|
|
|
|
confirm-icon="mdi mdi-send-check-outline"
|
|
|
|
|
|
:busy="reviewActionBusy"
|
|
|
|
|
|
@close="closeReviewNextStepConfirm"
|
|
|
|
|
|
@confirm="confirmReviewNextStepSubmit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
<Transition name="assistant-modal">
|
|
|
|
|
|
<div v-if="documentPreviewDialog.open" class="assistant-overlay review-overlay">
|
|
|
|
|
|
<section class="review-preview-modal">
|
|
|
|
|
|
<header class="review-preview-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="assistant-badge">票据原图</span>
|
|
|
|
|
|
<h3>{{ documentPreviewDialog.filename }}</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="close-btn" type="button" aria-label="关闭原图预览" @click="closeDocumentPreview">
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="review-preview-body" :class="documentPreviewDialog.kind">
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="documentPreviewDialog.kind === 'image'"
|
2026-05-23 19:54:42 +08:00
|
|
|
|
:key="documentPreviewDialog.renderKey"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
:src="documentPreviewDialog.url"
|
|
|
|
|
|
:alt="documentPreviewDialog.filename"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<iframe
|
|
|
|
|
|
v-else-if="documentPreviewDialog.kind === 'pdf'"
|
2026-05-23 19:54:42 +08:00
|
|
|
|
:key="documentPreviewDialog.renderKey"
|
2026-05-19 17:24:13 +00:00
|
|
|
|
:src="documentPreviewDialog.url"
|
|
|
|
|
|
title="票据 PDF 原图预览"
|
|
|
|
|
|
></iframe>
|
|
|
|
|
|
<div v-else class="review-side-empty">
|
|
|
|
|
|
<span class="review-side-empty-icon">
|
|
|
|
|
|
<i class="mdi mdi-file-outline"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<strong>当前文件暂不支持内置预览</strong>
|
|
|
|
|
|
<p>请重新上传图片或 PDF 票据,以便在这里查看原图。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script src="./scripts/TravelReimbursementCreateView.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped src="../assets/styles/views/travel-reimbursement-create-view.css"></style>
|
2026-05-21 23:53:03 +08:00
|
|
|
|
<style scoped src="../assets/styles/views/travel-reimbursement-create-view-part2.css"></style>
|
|
|
|
|
|
<style scoped src="../assets/styles/views/travel-reimbursement-create-view-part3.css"></style>
|
|
|
|
|
|
<style scoped src="../assets/styles/views/travel-reimbursement-create-view-part4.css"></style>
|