2026-05-27 09:17:57 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<article
|
|
|
|
|
|
class="message-row"
|
2026-06-04 14:25:14 +08:00
|
|
|
|
:class="[message.role, { 'has-steward-plan': message.stewardPlan }]"
|
2026-05-27 09:17:57 +08:00
|
|
|
|
>
|
|
|
|
|
|
<span class="message-avatar">
|
|
|
|
|
|
<img
|
|
|
|
|
|
:src="message.role === 'assistant' ? ui.aiAvatar : ui.userAvatar"
|
|
|
|
|
|
:alt="message.role === 'assistant' ? '财务助手头像' : '用户头像'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
2026-06-04 11:03:29 +08:00
|
|
|
|
<div class="message-stack">
|
|
|
|
|
|
<details
|
|
|
|
|
|
v-if="message.role === 'assistant' && message.stewardPlan && (message.stewardPlan.streamStatus === 'streaming' || message.stewardPlan.thinkingEvents?.length)"
|
|
|
|
|
|
class="steward-intent-bubble"
|
|
|
|
|
|
:open="message.stewardPlan.streamStatus === 'streaming'"
|
|
|
|
|
|
aria-label="小财管家意图识别智能体"
|
|
|
|
|
|
>
|
|
|
|
|
|
<summary>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<i class="mdi mdi-brain"></i>
|
|
|
|
|
|
意图识别智能体
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<small>{{ message.stewardPlan.streamStatus === 'streaming' ? '识别中' : `${message.stewardPlan.thinkingEvents?.length || 0} 步` }}</small>
|
|
|
|
|
|
<i class="mdi mdi-chevron-down"></i>
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<ol v-if="message.stewardPlan.thinkingEvents?.length" class="steward-intent-event-list">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="event in (message.stewardPlan.thinkingEvents || []).slice(0, message.stewardPlan.visibleThinkingEventCount || message.stewardPlan.thinkingEvents?.length || 0)"
|
|
|
|
|
|
:key="`${message.id}-${event.eventId}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>{{ event.title }}</strong>
|
2026-06-04 14:25:14 +08:00
|
|
|
|
<span :class="{ 'typing': event.status === 'running' }">{{ event.content }}</span>
|
2026-06-04 11:03:29 +08:00
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<p v-else class="steward-intent-empty">正在建立任务上下文...</p>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!message.stewardPlan || message.stewardPlan.streamStatus !== 'streaming' || message.text"
|
|
|
|
|
|
class="message-bubble"
|
|
|
|
|
|
:class="ui.buildMessageBubbleClass(message)"
|
|
|
|
|
|
>
|
2026-05-27 09:17:57 +08:00
|
|
|
|
<header class="message-meta">
|
|
|
|
|
|
<strong>{{ message.role === 'assistant' ? (message.assistantName || ui.ASSISTANT_DISPLAY_NAME) : '我' }}</strong>
|
|
|
|
|
|
<time>{{ message.time }}</time>
|
|
|
|
|
|
</header>
|
2026-06-04 11:03:29 +08:00
|
|
|
|
|
2026-05-27 09:17:57 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.text && message.role === 'assistant' && message.reviewPayload && ui.buildReviewMainMessageText(message)"
|
|
|
|
|
|
class="review-summary message-answer-content message-answer-markdown"
|
|
|
|
|
|
v-html="ui.renderMarkdown(ui.buildReviewMainMessageText(message))"
|
|
|
|
|
|
@click="ui.handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="message.text && message.role !== 'assistant'"
|
|
|
|
|
|
class="message-answer-content message-answer-markdown message-rich-text"
|
|
|
|
|
|
v-html="ui.renderMarkdown(message.text)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="message.text && message.role === 'assistant'"
|
2026-06-04 14:25:14 +08:00
|
|
|
|
:class="[
|
|
|
|
|
|
'message-answer-content',
|
|
|
|
|
|
'message-answer-markdown',
|
|
|
|
|
|
{
|
|
|
|
|
|
'steward-plan-markdown': message.stewardPlan,
|
|
|
|
|
|
'steward-plan-typing': message.stewardPlan?.streamStatus === 'typing'
|
|
|
|
|
|
}
|
|
|
|
|
|
]"
|
2026-05-27 09:17:57 +08:00
|
|
|
|
v-html="ui.renderMarkdown(message.text)"
|
|
|
|
|
|
@click="ui.handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
2026-05-27 12:27:17 +08:00
|
|
|
|
<BudgetAssistantReport
|
|
|
|
|
|
v-if="message.role === 'assistant' && message.budgetReport"
|
|
|
|
|
|
:report="message.budgetReport"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-04 11:03:29 +08:00
|
|
|
|
<div
|
2026-06-04 14:25:14 +08:00
|
|
|
|
v-if="message.role === 'assistant' && message.stewardPlan && message.stewardPlan.streamStatus !== 'streaming' && !message.stewardPlan.initialSummaryOnly"
|
2026-06-04 11:03:29 +08:00
|
|
|
|
class="steward-plan-block"
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
aria-label="小财管家任务计划"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="message.stewardPlan.tasks?.length" class="steward-task-list">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="task in message.stewardPlan.tasks"
|
|
|
|
|
|
:key="`${message.id}-${task.taskId}`"
|
|
|
|
|
|
class="steward-task-card"
|
|
|
|
|
|
>
|
2026-06-04 14:25:14 +08:00
|
|
|
|
<header class="steward-task-header">
|
|
|
|
|
|
<span class="steward-task-type">{{ task.taskTypeLabel }}</span>
|
|
|
|
|
|
<span class="steward-task-agent">{{ task.assignedAgentLabel }}</span>
|
2026-06-04 11:03:29 +08:00
|
|
|
|
</header>
|
2026-06-04 14:25:14 +08:00
|
|
|
|
<div class="steward-task-body">
|
|
|
|
|
|
<strong class="steward-task-title">{{ task.title }}</strong>
|
|
|
|
|
|
<p class="steward-task-summary">{{ task.summary }}</p>
|
|
|
|
|
|
</div>
|
2026-06-04 11:03:29 +08:00
|
|
|
|
<div class="steward-task-meta">
|
|
|
|
|
|
<span>置信度 {{ Math.round((task.confidence || 0) * 100) }}%</span>
|
2026-06-04 14:25:14 +08:00
|
|
|
|
<span v-if="!ui.resolveStewardMissingFieldItems(task).length">信息已基本齐备</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="ui.resolveStewardMissingFieldItems(task).length"
|
|
|
|
|
|
class="steward-task-missing"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="steward-task-missing-label">还需要补充</span>
|
|
|
|
|
|
<ul class="steward-task-missing-list">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="field in ui.resolveStewardMissingFieldItems(task)"
|
|
|
|
|
|
:key="`${task.taskId}-missing-${field.key}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>{{ field.label }}</strong>
|
|
|
|
|
|
<small v-if="field.hint">{{ field.hint }}</small>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
2026-06-04 11:03:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="message.stewardPlan.attachmentGroups?.length" class="steward-attachment-list">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="group in message.stewardPlan.attachmentGroups"
|
|
|
|
|
|
:key="`${message.id}-${group.groupId}`"
|
|
|
|
|
|
class="steward-attachment-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<span>{{ group.sceneLabel }}</span>
|
|
|
|
|
|
<small>{{ Math.round((group.confidence || 0) * 100) }}%</small>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<p>{{ group.rationale }}</p>
|
|
|
|
|
|
<div class="steward-attachment-chip-row">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="name in group.attachmentNames"
|
|
|
|
|
|
:key="`${group.groupId}-in-${name}`"
|
|
|
|
|
|
class="steward-attachment-chip include"
|
|
|
|
|
|
>{{ name }}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="name in group.excludedAttachmentNames"
|
|
|
|
|
|
:key="`${group.groupId}-out-${name}`"
|
|
|
|
|
|
class="steward-attachment-chip exclude"
|
|
|
|
|
|
>排除:{{ name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<Transition name="structured-card-reveal" appear>
|
2026-05-27 09:17:57 +08:00
|
|
|
|
<div
|
2026-06-06 17:19:07 +08:00
|
|
|
|
v-if="message.role === 'assistant' && message.applicationPreview"
|
|
|
|
|
|
class="application-preview-shell"
|
|
|
|
|
|
aria-label="申请信息核对结果"
|
2026-05-27 09:17:57 +08:00
|
|
|
|
>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
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 ui.resolveApplicationPreviewRows(message)"
|
|
|
|
|
|
:key="`${message.id}-${row.key}`"
|
|
|
|
|
|
class="application-preview-row"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
missing: row.missing,
|
|
|
|
|
|
editable: row.editable && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy,
|
|
|
|
|
|
highlight: row.highlight
|
|
|
|
|
|
}"
|
|
|
|
|
|
role="row"
|
|
|
|
|
|
:tabindex="row.editable && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy ? 0 : -1"
|
|
|
|
|
|
:aria-label="row.editable ? `编辑${row.label}` : row.label"
|
|
|
|
|
|
@click.stop="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
@keydown.enter.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
@keydown.space.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.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="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) === 'text'"
|
|
|
|
|
|
v-model="ui.applicationPreviewEditor.draftValue"
|
|
|
|
|
|
class="application-preview-input"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
autofocus
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@keydown.stop="ui.handleApplicationPreviewEditorKeydown($event, message)"
|
|
|
|
|
|
@blur="ui.commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<EnterpriseSelect
|
|
|
|
|
|
v-else-if="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) === 'select'"
|
|
|
|
|
|
v-model="ui.applicationPreviewEditor.draftValue"
|
|
|
|
|
|
class="application-preview-select"
|
|
|
|
|
|
:options="ui.resolveApplicationPreviewEditorOptions(row.key)"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
:teleported="false"
|
|
|
|
|
|
autofocus
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@change="ui.commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
@keydown.stop="ui.handleApplicationPreviewEditorKeydown($event, message)"
|
|
|
|
|
|
@blur="ui.commitApplicationPreviewEditor(message)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="application-preview-text"
|
2026-06-15 22:55:18 +08:00
|
|
|
|
:class="{ 'application-preview-date-chip': row.key === 'time' && !row.missing }"
|
2026-06-06 17:19:07 +08:00
|
|
|
|
>{{ row.value }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="row.editable"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="application-preview-edit-btn"
|
|
|
|
|
|
title="修改内容"
|
|
|
|
|
|
aria-label="修改内容"
|
|
|
|
|
|
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click.stop="ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-pencil-outline"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-27 09:17:57 +08:00
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="ui.resolveApplicationPreviewMissingFields(message)?.length"
|
|
|
|
|
|
class="application-preview-footer application-preview-footer-missing"
|
|
|
|
|
|
aria-live="polite"
|
2026-05-27 10:32:08 +08:00
|
|
|
|
>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<span class="application-preview-missing-prefix">当前还需要补充:</span>
|
|
|
|
|
|
<span class="application-preview-missing-list">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-for="(field, index) in ui.resolveApplicationPreviewMissingFields(message)"
|
|
|
|
|
|
:key="`${message.id}-missing-${field}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="application-preview-missing-chip">{{ field }}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="index < ui.resolveApplicationPreviewMissingFields(message).length - 1"
|
|
|
|
|
|
class="application-preview-missing-separator"
|
|
|
|
|
|
>、</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="application-preview-missing-suffix">。补齐后我再帮您提交申请。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="ui.buildApplicationPreviewFooterText(message)"
|
|
|
|
|
|
class="application-preview-footer message-answer-content message-answer-markdown"
|
|
|
|
|
|
v-html="ui.renderMarkdown(ui.buildApplicationPreviewFooterText(message))"
|
|
|
|
|
|
@click="ui.handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
2026-05-27 09:17:57 +08: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="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.runWelcomeQuickAction(action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="action.icon"></i>
|
|
|
|
|
|
<span>{{ action.label }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<Transition name="structured-card-reveal" appear>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
|
|
|
|
|
class="message-suggested-actions"
|
2026-05-27 09:17:57 +08:00
|
|
|
|
>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<button
|
|
|
|
|
|
v-for="action in message.suggestedActions"
|
|
|
|
|
|
:key="`${message.id}-${action.action_type}-${action.label}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-suggested-action-btn"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
selected: ui.isSuggestedActionSelected(message, action),
|
|
|
|
|
|
locked: message.suggestedActionsLocked
|
|
|
|
|
|
}"
|
|
|
|
|
|
:disabled="message.suggestedActionsLocked || ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.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>
|
|
|
|
|
|
</Transition>
|
2026-05-27 09:17:57 +08: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
|
|
|
|
|
|
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.citations?.length"
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
{{ message.queryPayload.title || (message.queryPayload.selectionMode === 'draft_association' ? '选择关联草稿' : '最近 5 条筛选结果') }}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="ui.buildExpenseQueryWindowLabel(message.queryPayload)" class="expense-query-window-label">
|
|
|
|
|
|
{{ ui.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 ui.getExpenseQueryVisibleRecords(message.queryPayload)"
|
|
|
|
|
|
:key="`${message.id}-${record.claimId}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="expense-query-record-card"
|
|
|
|
|
|
: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="ui.handleExpenseQueryRecordClick(message, 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 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="ui.appendExpenseQueryRiskToConversation(record, risk)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{{ record.claimNo }}</span>
|
|
|
|
|
|
<strong>{{ risk.levelLabel }}</strong>
|
|
|
|
|
|
<em>{{ risk.title }}</em>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="ui.getExpenseQueryTotalPages(message.queryPayload) > 1"
|
|
|
|
|
|
class="expense-query-pager"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="expense-query-pager-btn"
|
|
|
|
|
|
:disabled="ui.getExpenseQueryActivePage(message.queryPayload) === 1"
|
|
|
|
|
|
aria-label="上一页"
|
|
|
|
|
|
@click="ui.shiftExpenseQueryPage(message, -1)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-chevron-left"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="expense-query-pager-dots" aria-label="单据分页">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="page in ui.getExpenseQueryTotalPages(message.queryPayload)"
|
|
|
|
|
|
:key="`${message.id}-query-page-${page}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="expense-query-pager-dot"
|
|
|
|
|
|
:class="{ active: ui.getExpenseQueryActivePage(message.queryPayload) === page }"
|
|
|
|
|
|
:aria-label="`第 ${page} 页`"
|
|
|
|
|
|
@click="ui.setExpenseQueryPage(message, page)"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="expense-query-pager-btn"
|
|
|
|
|
|
:disabled="ui.getExpenseQueryActivePage(message.queryPayload) === ui.getExpenseQueryTotalPages(message.queryPayload)"
|
|
|
|
|
|
aria-label="下一页"
|
|
|
|
|
|
@click="ui.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>{{ message.queryPayload.emptyText || '当前没有可直接展开的近期待办单据。' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="ui.buildExpenseQueryHint(message.queryPayload)"
|
|
|
|
|
|
class="expense-query-hint message-answer-markdown"
|
|
|
|
|
|
v-html="ui.renderMarkdown(ui.buildExpenseQueryHint(message.queryPayload))"
|
|
|
|
|
|
@click="ui.handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="message.role === 'assistant' && ui.shouldShowDraftSavedCard(message)"
|
|
|
|
|
|
class="draft-preview application-draft-preview"
|
|
|
|
|
|
:class="{ 'reimbursement-draft-preview': !ui.isApplicationDraftPayload(message.draftPayload) }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template v-if="ui.isApplicationDraftPayload(message.draftPayload)">
|
|
|
|
|
|
<header class="application-draft-head">
|
|
|
|
|
|
<span class="application-draft-icon" aria-hidden="true">
|
|
|
|
|
|
<i class="mdi mdi-file-document-check-outline"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="application-draft-title">
|
|
|
|
|
|
<strong>申请单据已生成</strong>
|
|
|
|
|
|
<small>已为本次业务生成申请单,请按需查看完整详情。</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="application-draft-status">{{ ui.resolveApplicationDraftStatusLabel(message.draftPayload) }}</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<div class="application-draft-brief" role="group" aria-label="申请单据简要信息">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in ui.buildApplicationDraftSummaryItems(message.draftPayload)"
|
|
|
|
|
|
:key="`${message.id}-application-draft-${item.label}`"
|
|
|
|
|
|
class="application-draft-brief-item"
|
|
|
|
|
|
:class="{ 'is-primary': item.label === '单号' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{{ item.label }}</span>
|
|
|
|
|
|
<strong>{{ item.value }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<footer class="application-draft-footer">
|
|
|
|
|
|
<p>
|
|
|
|
|
|
完整审批链、附件和明细可在单据详情中
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="application-draft-detail-link"
|
|
|
|
|
|
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.openApplicationDraftDetail(message)"
|
|
|
|
|
|
>查看</button>。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="reimbursement-draft-card"
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
:aria-label="ui.canOpenDraftDetail(message) ? '报销草稿已生成' : '报销草稿待保存'"
|
|
|
|
|
|
>
|
2026-06-02 14:01:51 +08:00
|
|
|
|
<span class="reimbursement-draft-icon" aria-hidden="true">
|
|
|
|
|
|
<i class="mdi mdi-file-document-edit-outline"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="reimbursement-draft-main">
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<strong>{{ ui.canOpenDraftDetail(message) ? '报销草稿已生成' : '报销草稿待保存' }}</strong>
|
2026-06-02 14:01:51 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
单号:<span>{{ ui.resolveReimbursementDraftClaimNo(message.draftPayload) }}</span>
|
|
|
|
|
|
<button
|
2026-06-06 17:19:07 +08:00
|
|
|
|
v-if="ui.canOpenDraftDetail(message)"
|
2026-06-02 14:01:51 +08:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="reimbursement-draft-link"
|
|
|
|
|
|
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.openApplicationDraftDetail(message)"
|
|
|
|
|
|
>查看详情</button>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<span v-else class="reimbursement-draft-pending-detail">保存后可查看详情</span>
|
2026-06-02 14:01:51 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-27 09:17:57 +08:00
|
|
|
|
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
|
|
|
|
|
|
<div class="review-plain-followup">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-for="followup in [ui.buildReviewPlainFollowupForMessage(message)]"
|
|
|
|
|
|
:key="`${message.id}-review-followup`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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="ui.canUseInlineSaveDraft(message)" class="review-inline-save-copy">
|
|
|
|
|
|
请核查上面的关键信息。您也可以暂时不处理上述的这些内容,我可以帮你先保存为
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="review-inline-draft-link"
|
|
|
|
|
|
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.handleInlineSaveDraft(message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
草稿
|
|
|
|
|
|
</button>
|
|
|
|
|
|
。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="ui.buildReviewNextStepRichCopyForMessage(message)"
|
|
|
|
|
|
class="review-next-step-rich-copy message-answer-markdown"
|
|
|
|
|
|
v-html="ui.renderMarkdown(ui.buildReviewNextStepRichCopyForMessage(message))"
|
|
|
|
|
|
@click="ui.handleAssistantMarkdownClick($event, message)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="ui.resolveReviewFooterActions(message.reviewPayload).length"
|
|
|
|
|
|
class="review-footer-actions"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="review-footer-btn-row">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="action in ui.resolveReviewFooterActions(message.reviewPayload)"
|
|
|
|
|
|
:key="`${message.id}-${action.action_type}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:class="['review-footer-btn', action.emphasis === 'primary' ? 'primary' : '']"
|
|
|
|
|
|
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
|
|
|
|
|
@click="ui.handleReviewAction(message, action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ action.label || ui.buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</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>
|
2026-05-30 15:46:51 +08:00
|
|
|
|
<div
|
2026-06-06 17:19:07 +08:00
|
|
|
|
v-if="ui.shouldShowAssistantMessageActions(message)"
|
|
|
|
|
|
class="message-action-toolbar"
|
|
|
|
|
|
role="toolbar"
|
|
|
|
|
|
aria-label="系统消息操作"
|
2026-05-30 15:46:51 +08:00
|
|
|
|
>
|
2026-06-06 17:19:07 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-action-btn"
|
|
|
|
|
|
title="复制"
|
|
|
|
|
|
aria-label="复制"
|
|
|
|
|
|
@click="ui.copyAssistantMessage(message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-content-copy"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-action-btn"
|
|
|
|
|
|
title="语音播报"
|
|
|
|
|
|
aria-label="语音播报"
|
|
|
|
|
|
@click="ui.speakAssistantMessage(message)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-volume-high"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-action-btn"
|
|
|
|
|
|
:class="{ active: ui.isMessageFeedbackSelected(message, 5) }"
|
|
|
|
|
|
:disabled="Boolean(message.operationFeedback?.submitting)"
|
|
|
|
|
|
title="点赞"
|
|
|
|
|
|
aria-label="点赞"
|
|
|
|
|
|
@click="ui.submitOperationFeedbackForMessage(message, { rating: 5, reason: 'thumbs_up' })"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="ui.isMessageFeedbackSelected(message, 5) ? 'mdi mdi-thumb-up' : 'mdi mdi-thumb-up-outline'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="message-action-btn"
|
|
|
|
|
|
:class="{ active: ui.isMessageFeedbackSelected(message, 1) }"
|
|
|
|
|
|
:disabled="Boolean(message.operationFeedback?.submitting)"
|
|
|
|
|
|
title="点踩"
|
|
|
|
|
|
aria-label="点踩"
|
|
|
|
|
|
@click="ui.submitOperationFeedbackForMessage(message, { rating: 1, reason: 'thumbs_down' })"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="ui.isMessageFeedbackSelected(message, 1) ? 'mdi mdi-thumb-down' : 'mdi mdi-thumb-down-outline'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-30 15:46:51 +08:00
|
|
|
|
</div>
|
2026-05-27 09:17:57 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-05-27 12:27:17 +08:00
|
|
|
|
import BudgetAssistantReport from './BudgetAssistantReport.vue'
|
2026-05-27 09:17:57 +08:00
|
|
|
|
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TravelReimbursementMessageItem',
|
|
|
|
|
|
components: {
|
2026-05-27 12:27:17 +08:00
|
|
|
|
BudgetAssistantReport,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
EnterpriseSelect
|
2026-05-27 09:17:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
props: {
|
|
|
|
|
|
message: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
},
|
|
|
|
|
|
ui: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped src="../../assets/styles/components/travel-reimbursement-message-item.css"></style>
|