feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -148,104 +148,111 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Transition name="structured-card-reveal" appear>
|
||||
<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)"
|
||||
v-if="message.role === 'assistant' && message.applicationPreview"
|
||||
class="application-preview-shell"
|
||||
aria-label="申请信息核对结果"
|
||||
>
|
||||
<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"
|
||||
:class="{ 'application-preview-date-chip': row.key === 'time' && !row.missing }"
|
||||
>{{ 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>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && message.applicationPreview && ui.resolveApplicationPreviewMissingFields(message)?.length"
|
||||
class="application-preview-footer application-preview-footer-missing"
|
||||
aria-live="polite"
|
||||
>
|
||||
<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}`"
|
||||
<div
|
||||
class="application-preview-table"
|
||||
role="table"
|
||||
aria-label="申请信息核对表"
|
||||
>
|
||||
<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="message.role === 'assistant' && message.applicationPreview && 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 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"
|
||||
:class="{ 'application-preview-date-chip': row.key === 'time' && !row.missing }"
|
||||
>{{ 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>
|
||||
|
||||
<div
|
||||
v-if="ui.resolveApplicationPreviewMissingFields(message)?.length"
|
||||
class="application-preview-footer application-preview-footer-missing"
|
||||
aria-live="polite"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
|
||||
@@ -267,32 +274,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
||||
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: ui.isSuggestedActionSelected(message, action),
|
||||
locked: message.suggestedActionsLocked
|
||||
}"
|
||||
:disabled="message.suggestedActionsLocked || ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.handleSuggestedAction(message, action)"
|
||||
<Transition name="structured-card-reveal" appear>
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
||||
class="message-suggested-actions"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
|
||||
<strong>风险标签</strong>
|
||||
@@ -481,20 +490,26 @@
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="reimbursement-draft-card" role="group" aria-label="报销草稿已生成">
|
||||
<div
|
||||
class="reimbursement-draft-card"
|
||||
role="group"
|
||||
:aria-label="ui.canOpenDraftDetail(message) ? '报销草稿已生成' : '报销草稿待保存'"
|
||||
>
|
||||
<span class="reimbursement-draft-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-file-document-edit-outline"></i>
|
||||
</span>
|
||||
<div class="reimbursement-draft-main">
|
||||
<strong>报销草稿已生成</strong>
|
||||
<strong>{{ ui.canOpenDraftDetail(message) ? '报销草稿已生成' : '报销草稿待保存' }}</strong>
|
||||
<p>
|
||||
单号:<span>{{ ui.resolveReimbursementDraftClaimNo(message.draftPayload) }}</span>
|
||||
<button
|
||||
v-if="ui.canOpenDraftDetail(message)"
|
||||
type="button"
|
||||
class="reimbursement-draft-link"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.openApplicationDraftDetail(message)"
|
||||
>查看详情</button>
|
||||
<span v-else class="reimbursement-draft-pending-detail">保存后可查看详情</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -580,21 +595,53 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ui.isOperationFeedbackVisible(message)"
|
||||
class="message-feedback-bubble"
|
||||
v-if="ui.shouldShowAssistantMessageActions(message)"
|
||||
class="message-action-toolbar"
|
||||
role="toolbar"
|
||||
aria-label="系统消息操作"
|
||||
>
|
||||
<OperationFeedbackInlineCard
|
||||
:busy="Boolean(message.operationFeedback?.submitting)"
|
||||
:error-message="message.operationFeedback?.error || ''"
|
||||
:submitted="Boolean(message.operationFeedback?.submitted)"
|
||||
:submitted-rating="Number(message.operationFeedback?.rating || 0)"
|
||||
:reset-key="`${message.id}-${message.operationFeedback?.context?.runId || message.operationFeedback?.context?.run_id || ''}`"
|
||||
@dismiss="ui.dismissOperationFeedbackForMessage(message)"
|
||||
@submit="ui.submitOperationFeedbackForMessage(message, $event)"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
@@ -602,14 +649,12 @@
|
||||
<script>
|
||||
import BudgetAssistantReport from './BudgetAssistantReport.vue'
|
||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||
import OperationFeedbackInlineCard from '../shared/OperationFeedbackInlineCard.vue'
|
||||
|
||||
export default {
|
||||
name: 'TravelReimbursementMessageItem',
|
||||
components: {
|
||||
BudgetAssistantReport,
|
||||
EnterpriseSelect,
|
||||
OperationFeedbackInlineCard
|
||||
EnterpriseSelect
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
|
||||
Reference in New Issue
Block a user