Files
X-Financial/web/src/views/TravelReimbursementCreateView.vue

1035 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Teleport to="body">
<Transition name="assistant-modal">
<div 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="关闭对话工作台"
@pointerdown.stop.prevent="requestCloseWorkbench"
@click.stop.prevent="requestCloseWorkbench"
>
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="assistant-modal-stage">
<header class="assistant-header">
<div class="assistant-header-main">
<div>
<h2>财务AI工作台</h2>
<p>个人工作台发起报销智能录入统一走这里右侧会根据你的意图实时切换状态视图</p>
</div>
</div>
</header>
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
<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"
:disabled="submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
@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' ? 'AI 助手头像' : '用户头像'"
/>
</span>
<div class="message-bubble">
<header class="message-meta">
<strong>{{ message.role === 'assistant' ? 'AI 助手' : '我' }}</strong>
<time>{{ message.time }}</time>
</header>
<p v-if="message.text" :class="{ 'review-summary': message.role === 'assistant' && message.reviewPayload }">{{ message.text }}</p>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row">
<span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span>
</div>
<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>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.citations?.length" class="message-detail-block">
<strong>引用依据</strong>
<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>
</div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.suggestedActions?.length" class="message-detail-block">
<strong>建议动作</strong>
<div class="message-detail-chip-row">
<span
v-for="item in message.suggestedActions"
:key="`${message.id}-${item.action_type}-${item.label}`"
class="message-action-chip"
>
{{ item.label }}
</span>
</div>
</div>
<div
v-if="message.role === 'assistant' && !message.reviewPayload && message.queryPayload?.resultType === 'expense_claim_list'"
class="message-detail-block expense-query-block"
>
<strong>{{ message.queryPayload.recentWindowApplied ? '近 10 日单据' : '单据明细' }}</strong>
<p v-if="buildExpenseQueryWindowLabel(message.queryPayload)" class="expense-query-window-label">
{{ buildExpenseQueryWindowLabel(message.queryPayload) }}
</p>
<div v-if="message.queryPayload.statusGroups?.length" class="expense-query-summary-row">
<span
v-for="item in message.queryPayload.statusGroups"
:key="`${message.id}-${item.key}`"
class="expense-query-summary-chip"
:class="item.key"
>
{{ item.label }} {{ item.count }}
</span>
</div>
<div v-if="message.queryPayload.records?.length" class="expense-query-record-list compact">
<button
v-for="record in getExpenseQueryVisibleRecords(message.queryPayload)"
:key="`${message.id}-${record.claimId}`"
type="button"
class="expense-query-record-card"
@click="openExpenseQueryRecord(record)"
>
<div class="expense-query-record-main">
<div class="expense-query-record-top">
<strong>{{ record.claimNo }}</strong>
<span class="expense-query-record-status" :class="record.statusGroup || 'other'">
{{ record.statusLabel }}
</span>
</div>
<p>{{ record.summary }}</p>
<div class="expense-query-record-meta">
<span>{{ record.expenseTypeLabel }}</span>
<span>{{ record.dateDisplay }}</span>
<span>{{ record.amountDisplay }}</span>
</div>
</div>
<i class="mdi mdi-chevron-right"></i>
</button>
<div
v-if="getExpenseQueryTotalPages(message.queryPayload) > 1"
class="expense-query-pager"
>
<button
type="button"
class="expense-query-pager-btn"
:disabled="getExpenseQueryActivePage(message.queryPayload) === 1"
aria-label="上一页"
@click="shiftExpenseQueryPage(message, -1)"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<div class="expense-query-pager-dots" aria-label="单据分页">
<button
v-for="page in getExpenseQueryTotalPages(message.queryPayload)"
:key="`${message.id}-query-page-${page}`"
type="button"
class="expense-query-pager-dot"
:class="{ active: getExpenseQueryActivePage(message.queryPayload) === page }"
:aria-label="` ${page} `"
@click="setExpenseQueryPage(message, page)"
></button>
</div>
<button
type="button"
class="expense-query-pager-btn"
:disabled="getExpenseQueryActivePage(message.queryPayload) === getExpenseQueryTotalPages(message.queryPayload)"
aria-label="下一页"
@click="shiftExpenseQueryPage(message, 1)"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</div>
<div v-else class="expense-query-empty">
<i class="mdi mdi-file-search-outline"></i>
<span>当前没有可直接展开的近期待办单据</span>
</div>
<p v-if="buildExpenseQueryHint(message.queryPayload)" class="expense-query-hint">
{{ buildExpenseQueryHint(message.queryPayload) }}
</p>
</div>
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
<div class="review-card-shell">
<div class="review-card-head">
<div class="review-card-head-main">
<span class="review-card-icon">
<i class="mdi mdi-shield-alert-outline"></i>
</span>
<div class="review-card-head-copy">
<strong>{{ buildReviewHeadline(message.reviewPayload, message.draftPayload) }}</strong>
<p>{{ buildReviewSubline(message.reviewPayload, message.draftPayload) }}</p>
</div>
</div>
<span class="review-card-state" :class="buildReviewStateTone(message.reviewPayload, message.draftPayload)">
{{ buildReviewStateLabel(message.reviewPayload, message.draftPayload) }}
</span>
</div>
<details class="review-disclosure-card" :open="shouldOpenReviewDisclosure(message.reviewPayload)">
<summary class="review-disclosure-summary">
<div class="review-disclosure-copy">
<strong>{{ buildReviewDisclosureTitle(message.reviewPayload) }}</strong>
<p>{{ buildReviewDisclosureHint(message.reviewPayload) }}</p>
</div>
<span class="review-disclosure-toggle">
<i class="mdi mdi-chevron-down"></i>
</span>
</summary>
<div class="review-disclosure-body">
<div class="review-pending-list plain">
<!-- 待补充信息项 -->
<article
v-for="item in buildReviewTodoItems(message.reviewPayload)"
:key="`${message.id}-${item.key}`"
class="review-pending-item"
>
<span class="review-pending-icon">
<i :class="item.icon"></i>
</span>
<div class="review-pending-copy">
<strong>{{ item.title }}</strong>
<p>{{ item.hint }}</p>
</div>
<span class="review-pending-status" :class="item.tone">{{ item.status }}</span>
</article>
</div>
</div>
</details>
<div v-if="resolveReviewSubmitActions(message.reviewPayload).length || message.draftPayload?.claim_no" class="review-footer-actions">
<div class="review-footer-btn-row">
<button
v-for="action in resolveReviewSubmitActions(message.reviewPayload)"
:key="`${message.id}-${action.action_type}`"
type="button"
:class="['review-footer-btn', action.emphasis === 'primary' ? 'primary' : '']"
:disabled="reviewActionBusy"
@click="handleReviewAction(message, action)"
>
{{ action.label || buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
</button>
<button
v-if="resolveReviewEditAction(message.reviewPayload)"
type="button"
class="review-footer-btn"
:disabled="reviewActionBusy"
@click="handleReviewAction(message, resolveReviewEditAction(message.reviewPayload))"
>
修改识别信息
</button>
<button
type="button"
class="review-footer-btn"
:disabled="submitting || reviewActionBusy"
@click="triggerFileUpload(message.reviewPayload.document_cards?.length ? 'composer-continue' : 'composer')"
>
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
</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 }">
<button
v-if="!isKnowledgeSession"
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-shell">
<textarea
ref="composerTextareaRef"
v-model="composerDraft"
rows="1"
:placeholder="composerPlaceholder"
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
@input="handleComposerInput"
@keydown.enter.exact.stop
@keydown.ctrl.enter.prevent="submitComposer"
/>
</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 }">
<div>
<div v-if="!activeReviewPayload" 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">{{ currentInsight.title }}</h3>
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p>
</div>
<div v-if="activeReviewPayload" class="review-insight-tools">
<button
type="button"
class="review-insight-switch-icon-btn"
:class="{
available: reviewDocumentDrawerAvailable,
active: reviewDocumentDrawerAvailable && isReviewDocumentDrawer
}"
:disabled="!reviewDocumentDrawerAvailable || submitting || reviewActionBusy"
:title="reviewDocumentDrawerLabel"
:aria-label="reviewDocumentDrawerLabel"
@click="toggleReviewDocumentDrawer"
>
<i :class="reviewDocumentDrawerIcon"></i>
</button>
<button
type="button"
class="review-insight-switch-icon-btn risk"
:class="{
available: reviewRiskDrawerAvailable,
active: reviewRiskDrawerAvailable && isReviewRiskDrawer
}"
:disabled="!reviewRiskDrawerAvailable || submitting || reviewActionBusy"
:title="reviewRiskDrawerLabel"
:aria-label="reviewRiskDrawerLabel"
@click="toggleReviewRiskDrawer"
>
<i :class="reviewRiskDrawerIcon"></i>
</button>
</div>
<div class="confidence-card" v-if="!activeReviewPayload">
<span>{{ currentInsight.metricLabel }}</span>
<strong>{{ currentInsight.metricValue }}</strong>
</div>
</div>
<Transition name="insight-switch" mode="out-in">
<div :key="`${activeSessionType}-${currentInsight.intent}-${currentInsight.title}-${reviewDrawerMode}`" class="insight-body">
<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="currentInsight.intent === 'agent' && currentInsight.agent">
<template v-if="activeReviewPayload">
<template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer">
<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,
invalid: Boolean(reviewInlineErrors[item.key])
}"
@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>
<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="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"
placeholder="例如:业务招待费 / 差旅费"
/>
</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">
<strong>合规提醒 / 风险评分</strong>
<p>结合本体附件要求和识别结果集中查看当前票据风险</p>
</div>
<span class="review-side-risk-score" :class="{ empty: reviewRiskScore === null }">
{{ reviewRiskScore === null ? '无' : `${reviewRiskScore}/100` }}
</span>
</div>
<p class="review-side-risk-summary">{{ reviewRiskSummary }}</p>
<ul v-if="reviewRiskItems.length" class="review-side-risk-list">
<li v-for="item in reviewRiskItems" :key="item">{{ item }}</li>
</ul>
<div v-else-if="reviewRiskEmpty" class="review-side-empty">
<span class="review-side-empty-icon">
<i class="mdi mdi-shield-check-outline"></i>
</span>
<strong>暂无风险评分</strong>
<p>当前版本还没有返回结构化风险评分结果这里先不展示虚拟分数</p>
</div>
<button
v-if="reviewRiskActionAvailable"
type="button"
class="review-side-link"
:disabled="submitting || reviewActionBusy"
@click="explainCurrentReviewRisk"
>
查看全部风险项
<i class="mdi mdi-chevron-right"></i>
</button>
</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>
<section v-if="currentInsight.agent.citations?.length && !activeReviewPayload" class="insight-card">
<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"
/>
<ConfirmDialog
:open="reviewCancelDialogOpen"
badge="取消核对"
badge-tone="warning"
title="确认放弃本次识别结果?"
description="关闭后将退出当前核对窗口,本次尚未确认的修改不会继续保留。"
cancel-text="返回继续核对"
confirm-text="确认取消"
busy-text="处理中..."
confirm-tone="danger"
confirm-icon="mdi mdi-close-circle-outline"
:busy="reviewActionBusy"
@close="closeCancelReviewDialog"
@confirm="confirmCancelReview"
/>
<Transition name="assistant-modal">
<div v-if="uploadDecisionDialogOpen" class="assistant-overlay review-overlay">
<section class="review-confirm-modal review-upload-decision-modal">
<div class="review-upload-decision-copy">
<span class="assistant-badge">上传票据</span>
<h3>检测到你已有单据事件</h3>
<p>这次新上传的附件需要先确认处理方式你可以继续归集到上一笔单据也可以重新开启一张新单据</p>
</div>
<div class="review-confirm-actions review-upload-decision-actions">
<button type="button" class="primary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="continueExistingUpload">
继续
</button>
<button type="button" class="secondary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="createNewUploadDocument">
新单据
</button>
<button type="button" class="secondary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="closeUploadDecisionDialog">
取消
</button>
</div>
</section>
</div>
</Transition>
<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'"
:src="documentPreviewDialog.url"
:alt="documentPreviewDialog.filename"
/>
<iframe
v-else-if="documentPreviewDialog.kind === 'pdf'"
: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>
<Transition name="assistant-modal">
<div v-if="reviewEditDialogOpen" class="assistant-overlay review-overlay">
<section class="review-edit-modal">
<header class="review-edit-head">
<div>
<span class="assistant-badge">修改识别信息</span>
<h3>请按当前识别结果逐项修改</h3>
<p>修改后会重新发送到智能体右侧识别结果会按新内容刷新</p>
</div>
<button class="close-btn" type="button" aria-label="关闭修改面板" :disabled="reviewActionBusy" @click="closeEditReviewDialog">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="review-edit-form">
<label
v-for="item in reviewEditFields"
:key="item.key"
class="review-edit-field"
:class="item.group"
>
<span>{{ item.label }}<em v-if="item.required">*</em></span>
<textarea
v-if="item.field_type === 'textarea'"
v-model="item.value"
rows="3"
:placeholder="item.placeholder"
:disabled="reviewActionBusy"
></textarea>
<input
v-else
v-model="item.value"
type="text"
:placeholder="item.placeholder"
:disabled="reviewActionBusy"
/>
</label>
</div>
<div class="review-edit-actions">
<button type="button" class="secondary-dialog-btn" :disabled="reviewActionBusy" @click="closeEditReviewDialog">取消</button>
<button type="button" class="primary-dialog-btn" :disabled="reviewActionBusy" @click="applyEditedReview">
{{ reviewActionBusy ? '提交中...' : '确认修改' }}
</button>
</div>
</section>
</div>
</Transition>
</Teleport>
</template>
<script src="./scripts/TravelReimbursementCreateView.js"></script>
<style scoped src="../assets/styles/views/travel-reimbursement-create-view.css"></style>