feat(web): 更新请求列表、差旅报销创建、差旅请求详情页面及对应的业务脚本逻辑
This commit is contained in:
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击任意行可查看单据详情</p>
|
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击任意行可查看单据详情</p>
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap" :class="{ 'is-empty': showEmpty }">
|
||||||
<div v-if="loading" class="table-state">
|
<div v-if="loading" class="table-state">
|
||||||
<i class="mdi mdi-loading mdi-spin"></i>
|
<i class="mdi mdi-loading mdi-spin"></i>
|
||||||
<strong>正在加载真实报销数据</strong>
|
<strong>正在加载真实报销数据</strong>
|
||||||
|
|||||||
@@ -244,35 +244,8 @@
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="review-disclosure-body">
|
<div class="review-disclosure-body">
|
||||||
<div v-if="buildReviewAlertChips(message.reviewPayload).length" class="review-alert-chip-row subtle">
|
|
||||||
<span
|
|
||||||
v-for="item in buildReviewAlertChips(message.reviewPayload)"
|
|
||||||
:key="`${message.id}-${item.key}`"
|
|
||||||
class="review-alert-chip"
|
|
||||||
:class="item.tone"
|
|
||||||
>
|
|
||||||
<i :class="item.tone === 'danger' ? 'mdi mdi-alert-circle' : item.tone === 'success' ? 'mdi mdi-check-circle' : 'mdi mdi-alert'"></i>
|
|
||||||
{{ item.label }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="resolveReviewRiskBriefs(message.reviewPayload).length" class="review-risk-brief-list">
|
|
||||||
<article
|
|
||||||
v-for="item in resolveReviewRiskBriefs(message.reviewPayload)"
|
|
||||||
:key="`${message.id}-${item.title}`"
|
|
||||||
class="review-risk-brief"
|
|
||||||
:class="item.level || 'info'"
|
|
||||||
>
|
|
||||||
<strong>{{ item.title }}</strong>
|
|
||||||
<p>{{ item.content }}</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="review-section-head review-flow-head">
|
|
||||||
<strong>{{ buildReviewTodoSectionTitle(message.reviewPayload) }}</strong>
|
|
||||||
<span>{{ buildReviewTodoSectionMeta(message.reviewPayload) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="review-pending-list plain">
|
<div class="review-pending-list plain">
|
||||||
|
<!-- 待补充信息项 -->
|
||||||
<article
|
<article
|
||||||
v-for="item in buildReviewTodoItems(message.reviewPayload)"
|
v-for="item in buildReviewTodoItems(message.reviewPayload)"
|
||||||
:key="`${message.id}-${item.key}`"
|
:key="`${message.id}-${item.key}`"
|
||||||
@@ -321,16 +294,6 @@
|
|||||||
>
|
>
|
||||||
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
|
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="message.draftPayload?.claim_no"
|
|
||||||
type="button"
|
|
||||||
class="review-footer-btn"
|
|
||||||
:disabled="submitting || reviewActionBusy"
|
|
||||||
@click="queryDraftByClaimNo(message.draftPayload.claim_no)"
|
|
||||||
>
|
|
||||||
查看当前报销信息
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -484,13 +447,46 @@
|
|||||||
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
|
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="review-insight-title-row">
|
<div v-else class="review-insight-title-row">
|
||||||
<h3>报销识别核对</h3>
|
<div class="review-insight-title-copy">
|
||||||
<span class="insight-head-badge">实时意图分析</span>
|
<h3>{{ reviewDrawerTitle }}</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 v-if="!activeReviewPayload">{{ currentInsight.title }}</h3>
|
<h3 v-if="!activeReviewPayload">{{ currentInsight.title }}</h3>
|
||||||
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p>
|
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p>
|
||||||
</div>
|
</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">
|
<div class="confidence-card" v-if="!activeReviewPayload">
|
||||||
<span>{{ currentInsight.metricLabel }}</span>
|
<span>{{ currentInsight.metricLabel }}</span>
|
||||||
<strong>{{ currentInsight.metricValue }}</strong>
|
<strong>{{ currentInsight.metricValue }}</strong>
|
||||||
@@ -498,7 +494,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition name="insight-switch" mode="out-in">
|
<Transition name="insight-switch" mode="out-in">
|
||||||
<div :key="`${activeSessionType}-${currentInsight.intent}-${currentInsight.title}`" class="insight-body">
|
<div :key="`${activeSessionType}-${currentInsight.intent}-${currentInsight.title}-${reviewDrawerMode}`" class="insight-body">
|
||||||
<template v-if="isKnowledgeSession">
|
<template v-if="isKnowledgeSession">
|
||||||
<section class="insight-card knowledge-hot-card">
|
<section class="insight-card knowledge-hot-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
@@ -528,6 +524,7 @@
|
|||||||
|
|
||||||
<template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent">
|
<template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent">
|
||||||
<template v-if="activeReviewPayload">
|
<template v-if="activeReviewPayload">
|
||||||
|
<template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer">
|
||||||
<section class="review-side-card review-side-overview-card">
|
<section class="review-side-card review-side-overview-card">
|
||||||
<div class="review-side-intent-row">
|
<div class="review-side-intent-row">
|
||||||
<i class="mdi mdi-account-outline"></i>
|
<i class="mdi mdi-account-outline"></i>
|
||||||
@@ -650,41 +647,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="review-side-card review-side-risk-card">
|
</template>
|
||||||
<div class="review-side-head">
|
|
||||||
<strong>合规提醒 / 风险评分</strong>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<section v-if="reviewDocumentCount" class="review-side-card review-document-switch-card">
|
<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 review-document-switch-head">
|
||||||
<div class="review-side-head-copy">
|
<div class="review-side-head-copy">
|
||||||
<strong>票据识别结果卡片</strong>
|
<strong>票据识别结果卡片</strong>
|
||||||
<p>逐张查看 OCR 结果,可直接修正后再继续提交。</p>
|
<p>逐张查看 OCR 结果,可直接修正后再切回核对滑窗。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="review-document-nav">
|
<div class="review-document-nav">
|
||||||
<button
|
<button
|
||||||
@@ -712,19 +682,8 @@
|
|||||||
<div v-if="activeReviewDocument" class="review-document-stage">
|
<div v-if="activeReviewDocument" class="review-document-stage">
|
||||||
<div class="review-document-stage-head">
|
<div class="review-document-stage-head">
|
||||||
<div class="review-document-stage-copy">
|
<div class="review-document-stage-copy">
|
||||||
<span class="review-document-index-chip">票据 {{ activeReviewDocument.index }}</span>
|
|
||||||
<strong :title="activeReviewDocument.filename">{{ activeReviewDocument.filename }}</strong>
|
<strong :title="activeReviewDocument.filename">{{ activeReviewDocument.filename }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
v-if="canPreviewActiveReviewDocument"
|
|
||||||
type="button"
|
|
||||||
class="review-side-link"
|
|
||||||
:disabled="submitting || reviewActionBusy"
|
|
||||||
@click="openActiveReviewDocumentPreview"
|
|
||||||
>
|
|
||||||
查看原图
|
|
||||||
<i class="mdi mdi-arrow-top-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="review-document-meta-chip-row">
|
<div class="review-document-meta-chip-row">
|
||||||
@@ -734,7 +693,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="review-document-scroll">
|
<div class="review-document-scroll">
|
||||||
<div class="review-document-preview-card" :class="activeReviewDocumentPreview?.kind || 'file'">
|
<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
|
<img
|
||||||
v-if="activeReviewDocumentPreview?.kind === 'image' && activeReviewDocumentPreview?.url"
|
v-if="activeReviewDocumentPreview?.kind === 'image' && activeReviewDocumentPreview?.url"
|
||||||
:src="activeReviewDocumentPreview.url"
|
:src="activeReviewDocumentPreview.url"
|
||||||
@@ -742,13 +712,13 @@
|
|||||||
/>
|
/>
|
||||||
<div v-else-if="activeReviewDocumentPreview?.kind === 'pdf'" class="review-document-preview-placeholder">
|
<div v-else-if="activeReviewDocumentPreview?.kind === 'pdf'" class="review-document-preview-placeholder">
|
||||||
<i class="mdi mdi-file-pdf-box"></i>
|
<i class="mdi mdi-file-pdf-box"></i>
|
||||||
<strong>PDF 原件可预览</strong>
|
<strong>PDF 票据文件</strong>
|
||||||
<p>点击右上角“查看原图”可查看完整 PDF。</p>
|
<p>当前文件还没有生成图片预览,可先核对下方识别字段。</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="review-document-preview-placeholder">
|
<div v-else class="review-document-preview-placeholder">
|
||||||
<i class="mdi mdi-file-search-outline"></i>
|
<i class="mdi mdi-file-search-outline"></i>
|
||||||
<strong>当前无可预览原图</strong>
|
<strong>当前无可预览票据</strong>
|
||||||
<p>这张票据仍可继续修改识别结果。</p>
|
<p>这张票据还没有可用预览,可先核对下方识别字段。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -758,7 +728,7 @@
|
|||||||
v-model="activeReviewDocument.summary"
|
v-model="activeReviewDocument.summary"
|
||||||
rows="3"
|
rows="3"
|
||||||
:disabled="submitting || reviewActionBusy"
|
:disabled="submitting || reviewActionBusy"
|
||||||
placeholder="可根据原图修正 OCR 摘要"
|
placeholder="可根据票据图片修正 OCR 摘要"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -808,6 +778,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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
|
<button
|
||||||
v-if="reviewHasUnsavedChanges"
|
v-if="reviewHasUnsavedChanges"
|
||||||
|
|||||||
@@ -208,6 +208,24 @@
|
|||||||
<span class="attachment-hint compact">
|
<span class="attachment-hint compact">
|
||||||
{{ resolveAttachmentDisplayName(item) || '支持上传 JPG、PNG、PDF,未上传也可先保存草稿。' }}
|
{{ resolveAttachmentDisplayName(item) || '支持上传 JPG、PNG、PDF,未上传也可先保存草稿。' }}
|
||||||
</span>
|
</span>
|
||||||
|
<div v-if="resolveAttachmentRecognition(item)" class="attachment-recognition">
|
||||||
|
<div class="attachment-recognition-pills">
|
||||||
|
<span class="attachment-recognition-pill type">
|
||||||
|
{{ resolveAttachmentRecognition(item).documentTypeLabel }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:class="['attachment-recognition-pill', resolveAttachmentRecognition(item).requirementTone]"
|
||||||
|
>
|
||||||
|
{{ resolveAttachmentRecognition(item).requirementLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="resolveAttachmentRecognition(item).message" class="attachment-recognition-message">
|
||||||
|
{{ resolveAttachmentRecognition(item).message }}
|
||||||
|
</p>
|
||||||
|
<ul v-if="resolveAttachmentRecognition(item).fields.length" class="attachment-recognition-fields">
|
||||||
|
<li v-for="field in resolveAttachmentRecognition(item).fields" :key="field">{{ field }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -244,6 +262,24 @@
|
|||||||
<span class="attachment-hint compact">
|
<span class="attachment-hint compact">
|
||||||
{{ resolveAttachmentDisplayName(item) || '未上传附件' }}
|
{{ resolveAttachmentDisplayName(item) || '未上传附件' }}
|
||||||
</span>
|
</span>
|
||||||
|
<div v-if="resolveAttachmentRecognition(item)" class="attachment-recognition">
|
||||||
|
<div class="attachment-recognition-pills">
|
||||||
|
<span class="attachment-recognition-pill type">
|
||||||
|
{{ resolveAttachmentRecognition(item).documentTypeLabel }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:class="['attachment-recognition-pill', resolveAttachmentRecognition(item).requirementTone]"
|
||||||
|
>
|
||||||
|
{{ resolveAttachmentRecognition(item).requirementLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="resolveAttachmentRecognition(item).message" class="attachment-recognition-message">
|
||||||
|
{{ resolveAttachmentRecognition(item).message }}
|
||||||
|
</p>
|
||||||
|
<ul v-if="resolveAttachmentRecognition(item).fields.length" class="attachment-recognition-fields">
|
||||||
|
<li v-for="field in resolveAttachmentRecognition(item).fields" :key="field">{{ field }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="hasExpenseRiskColumn" class="expense-risk col-risk">
|
<td v-if="hasExpenseRiskColumn" class="expense-risk col-risk">
|
||||||
|
|||||||
@@ -37,9 +37,18 @@ const INTENT_LABELS = {
|
|||||||
|
|
||||||
const DOCUMENT_TYPE_LABELS = {
|
const DOCUMENT_TYPE_LABELS = {
|
||||||
travel_ticket: '行程单/机票/车票',
|
travel_ticket: '行程单/机票/车票',
|
||||||
|
flight_itinerary: '机票/航班行程单',
|
||||||
|
train_ticket: '火车/高铁票',
|
||||||
hotel_invoice: '酒店住宿票据',
|
hotel_invoice: '酒店住宿票据',
|
||||||
|
taxi_receipt: '出租车/网约车票据',
|
||||||
|
parking_toll_receipt: '停车/通行费票据',
|
||||||
transport_receipt: '交通出行票据',
|
transport_receipt: '交通出行票据',
|
||||||
meal_receipt: '餐饮票据',
|
meal_receipt: '餐饮票据',
|
||||||
|
office_invoice: '办公用品票据',
|
||||||
|
meeting_invoice: '会议/会务票据',
|
||||||
|
training_invoice: '培训票据',
|
||||||
|
vat_invoice: '增值税发票',
|
||||||
|
receipt: '一般收据/凭证',
|
||||||
other: '其他单据'
|
other: '其他单据'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +164,9 @@ const COMPOSER_MAX_ROWS = 5
|
|||||||
const EXPENSE_QUERY_PAGE_SIZE = 5
|
const EXPENSE_QUERY_PAGE_SIZE = 5
|
||||||
const SESSION_TYPE_EXPENSE = 'expense'
|
const SESSION_TYPE_EXPENSE = 'expense'
|
||||||
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||||
|
const REVIEW_DRAWER_MODE_REVIEW = 'review'
|
||||||
|
const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents'
|
||||||
|
const REVIEW_DRAWER_MODE_RISK = 'risk'
|
||||||
const HOT_KNOWLEDGE_QUESTIONS = [
|
const HOT_KNOWLEDGE_QUESTIONS = [
|
||||||
'差旅住宿标准按什么规则执行?',
|
'差旅住宿标准按什么规则执行?',
|
||||||
'酒店超标后如何申请例外报销?',
|
'酒店超标后如何申请例外报销?',
|
||||||
@@ -316,6 +328,19 @@ function normalizeOcrDocuments(payload) {
|
|||||||
text: String(item.text || '').slice(0, 240),
|
text: String(item.text || '').slice(0, 240),
|
||||||
avg_score: Number(item.avg_score || 0),
|
avg_score: Number(item.avg_score || 0),
|
||||||
line_count: Number(item.line_count || 0),
|
line_count: Number(item.line_count || 0),
|
||||||
|
document_type: String(item.document_type || 'other').trim() || 'other',
|
||||||
|
document_type_label: String(item.document_type_label || '').trim(),
|
||||||
|
scene_code: String(item.scene_code || 'other').trim() || 'other',
|
||||||
|
scene_label: String(item.scene_label || '').trim(),
|
||||||
|
document_fields: Array.isArray(item.document_fields)
|
||||||
|
? item.document_fields
|
||||||
|
.map((field) => ({
|
||||||
|
key: String(field?.key || '').trim(),
|
||||||
|
label: String(field?.label || '').trim(),
|
||||||
|
value: String(field?.value || '').trim()
|
||||||
|
}))
|
||||||
|
.filter((field) => field.key && field.label && field.value)
|
||||||
|
: [],
|
||||||
warnings: Array.isArray(item.warnings) ? item.warnings : []
|
warnings: Array.isArray(item.warnings) ? item.warnings : []
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -362,7 +387,15 @@ function buildFilePreviews(files, previewRegistry) {
|
|||||||
|
|
||||||
function resolveDocumentPreview(filePreviews, filename) {
|
function resolveDocumentPreview(filePreviews, filename) {
|
||||||
if (!Array.isArray(filePreviews)) return null
|
if (!Array.isArray(filePreviews)) return null
|
||||||
return filePreviews.find((item) => item.filename === filename) ?? null
|
const matches = filePreviews.filter((item) => item.filename === filename)
|
||||||
|
if (!matches.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
matches.find((item) => item.kind === 'image' && item.url) ||
|
||||||
|
matches.find((item) => item.url) ||
|
||||||
|
matches[0]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFileIdentity(file) {
|
function buildFileIdentity(file) {
|
||||||
@@ -418,6 +451,17 @@ function mergeFilePreviews(existingPreviews, incomingPreviews) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOcrFilePreviews(payload) {
|
||||||
|
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||||||
|
return documents
|
||||||
|
.map((item) => ({
|
||||||
|
filename: String(item?.filename || '').trim(),
|
||||||
|
kind: String(item?.preview_kind || '').trim(),
|
||||||
|
url: String(item?.preview_data_url || '').trim()
|
||||||
|
}))
|
||||||
|
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
||||||
|
}
|
||||||
|
|
||||||
function extractReviewAttachmentNames(reviewPayload) {
|
function extractReviewAttachmentNames(reviewPayload) {
|
||||||
const documentNames = Array.isArray(reviewPayload?.document_cards)
|
const documentNames = Array.isArray(reviewPayload?.document_cards)
|
||||||
? reviewPayload.document_cards.map((item) => String(item?.filename || '').trim()).filter(Boolean)
|
? reviewPayload.document_cards.map((item) => String(item?.filename || '').trim()).filter(Boolean)
|
||||||
@@ -1117,8 +1161,8 @@ function countReviewRiskItems(reviewPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildReviewHeadline(reviewPayload) {
|
function buildReviewHeadline(reviewPayload) {
|
||||||
if (countReviewPendingItems(reviewPayload) || countReviewRiskItems(reviewPayload)) {
|
if (countReviewPendingItems(reviewPayload)) {
|
||||||
return '风险提示与待补充信息'
|
return '待补充信息'
|
||||||
}
|
}
|
||||||
if (reviewPayload?.can_proceed) {
|
if (reviewPayload?.can_proceed) {
|
||||||
return '识别结果已整理完成'
|
return '识别结果已整理完成'
|
||||||
@@ -1128,13 +1172,9 @@ function buildReviewHeadline(reviewPayload) {
|
|||||||
|
|
||||||
function buildReviewSubline(reviewPayload) {
|
function buildReviewSubline(reviewPayload) {
|
||||||
const pendingCount = countReviewPendingItems(reviewPayload)
|
const pendingCount = countReviewPendingItems(reviewPayload)
|
||||||
const riskCount = countReviewRiskItems(reviewPayload)
|
|
||||||
|
|
||||||
if (pendingCount || riskCount) {
|
if (pendingCount) {
|
||||||
const parts = []
|
return `请先展开查看 ${pendingCount} 项待补充内容,再决定继续处理、修改信息或保存草稿。`
|
||||||
if (pendingCount) parts.push(`${pendingCount} 项待补充`)
|
|
||||||
if (riskCount) parts.push(`${riskCount} 条提醒`)
|
|
||||||
return `请先展开查看${parts.join('、')},再决定继续处理、修改信息或保存草稿。`
|
|
||||||
}
|
}
|
||||||
if (reviewPayload?.can_proceed) {
|
if (reviewPayload?.can_proceed) {
|
||||||
return '当前关键信息已基本齐全,展开确认无误后可以继续下一步。'
|
return '当前关键信息已基本齐全,展开确认无误后可以继续下一步。'
|
||||||
@@ -1144,42 +1184,35 @@ function buildReviewSubline(reviewPayload) {
|
|||||||
|
|
||||||
function buildReviewStateLabel(reviewPayload) {
|
function buildReviewStateLabel(reviewPayload) {
|
||||||
const pendingCount = countReviewPendingItems(reviewPayload)
|
const pendingCount = countReviewPendingItems(reviewPayload)
|
||||||
const riskCount = countReviewRiskItems(reviewPayload)
|
|
||||||
if (pendingCount) return `待补充 ${pendingCount} 项`
|
if (pendingCount) return `待补充 ${pendingCount} 项`
|
||||||
if (riskCount) return `提醒 ${riskCount} 条`
|
|
||||||
if (reviewPayload?.can_proceed) return '可继续处理'
|
if (reviewPayload?.can_proceed) return '可继续处理'
|
||||||
return '已识别'
|
return '已识别'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReviewStateTone(reviewPayload) {
|
function buildReviewStateTone(reviewPayload) {
|
||||||
return reviewPayload?.can_proceed && !countReviewPendingItems(reviewPayload) && !countReviewRiskItems(reviewPayload)
|
return reviewPayload?.can_proceed && !countReviewPendingItems(reviewPayload)
|
||||||
? 'ready'
|
? 'ready'
|
||||||
: 'pending'
|
: 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReviewDisclosureTitle(reviewPayload) {
|
function buildReviewDisclosureTitle(reviewPayload) {
|
||||||
const pendingCount = countReviewPendingItems(reviewPayload)
|
const pendingCount = countReviewPendingItems(reviewPayload)
|
||||||
const riskCount = countReviewRiskItems(reviewPayload)
|
if (pendingCount) {
|
||||||
if (pendingCount || riskCount) {
|
return `当前有 ${pendingCount} 项待补充,点击展开查看`
|
||||||
const parts = []
|
|
||||||
if (riskCount) parts.push(`${riskCount} 条提醒`)
|
|
||||||
if (pendingCount) parts.push(`${pendingCount} 项待补充`)
|
|
||||||
return `当前有 ${parts.join(',')},点击展开查看`
|
|
||||||
}
|
}
|
||||||
return '当前无明显风险或缺失项,可展开查看识别摘要'
|
return '当前信息已齐全,可展开查看识别摘要'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReviewDisclosureHint(reviewPayload) {
|
function buildReviewDisclosureHint(reviewPayload) {
|
||||||
const pendingCount = countReviewPendingItems(reviewPayload)
|
const pendingCount = countReviewPendingItems(reviewPayload)
|
||||||
const riskCount = countReviewRiskItems(reviewPayload)
|
if (pendingCount) {
|
||||||
if (pendingCount || riskCount) {
|
return '展开后可查看待补充字段和处理建议'
|
||||||
return '展开后可查看风险说明、待补充字段和处理建议'
|
|
||||||
}
|
}
|
||||||
return '展开后可查看本轮已识别的关键信息'
|
return '展开后可查看本轮已识别的关键信息'
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldOpenReviewDisclosure(reviewPayload) {
|
function shouldOpenReviewDisclosure(reviewPayload) {
|
||||||
return !countReviewPendingItems(reviewPayload) && !countReviewRiskItems(reviewPayload)
|
return !countReviewPendingItems(reviewPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReviewTodoSectionTitle(reviewPayload) {
|
function buildReviewTodoSectionTitle(reviewPayload) {
|
||||||
@@ -1990,6 +2023,7 @@ export default {
|
|||||||
const reviewDocumentDrafts = ref([])
|
const reviewDocumentDrafts = ref([])
|
||||||
const reviewDocumentBaseDrafts = ref([])
|
const reviewDocumentBaseDrafts = ref([])
|
||||||
const activeReviewDocumentIndex = ref(0)
|
const activeReviewDocumentIndex = ref(0)
|
||||||
|
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
|
||||||
const insightPanelCollapsed = ref(false)
|
const insightPanelCollapsed = ref(false)
|
||||||
const documentPreviewDialog = ref({
|
const documentPreviewDialog = ref({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -2076,11 +2110,38 @@ export default {
|
|||||||
const reviewRiskSummary = computed(() => buildReviewRiskSummary(activeReviewPayload.value))
|
const reviewRiskSummary = computed(() => buildReviewRiskSummary(activeReviewPayload.value))
|
||||||
const reviewRiskItems = computed(() => buildReviewRiskItems(activeReviewPayload.value))
|
const reviewRiskItems = computed(() => buildReviewRiskItems(activeReviewPayload.value))
|
||||||
const reviewRiskEmpty = computed(() => reviewRiskScore.value === null && !reviewRiskItems.value.length)
|
const reviewRiskEmpty = computed(() => reviewRiskScore.value === null && !reviewRiskItems.value.length)
|
||||||
|
const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0)
|
||||||
|
const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value)
|
||||||
const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0)
|
const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0)
|
||||||
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
|
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
|
||||||
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
|
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
|
||||||
const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value))
|
const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value))
|
||||||
const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length)
|
const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length)
|
||||||
|
const isReviewDocumentDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS)
|
||||||
|
const isReviewRiskDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK)
|
||||||
|
const reviewDrawerTitle = computed(() => (
|
||||||
|
isReviewDocumentDrawer.value
|
||||||
|
? '票据识别结果'
|
||||||
|
: isReviewRiskDrawer.value
|
||||||
|
? '风险提示'
|
||||||
|
: '报销识别核对'
|
||||||
|
))
|
||||||
|
const reviewDocumentDrawerLabel = computed(() => (
|
||||||
|
isReviewDocumentDrawer.value ? '显示核对' : '显示票据'
|
||||||
|
))
|
||||||
|
const reviewDocumentDrawerIcon = computed(() => (
|
||||||
|
isReviewDocumentDrawer.value
|
||||||
|
? 'mdi mdi-file-document-multiple'
|
||||||
|
: 'mdi mdi-file-document-multiple-outline'
|
||||||
|
))
|
||||||
|
const reviewRiskDrawerLabel = computed(() => (
|
||||||
|
isReviewRiskDrawer.value ? '显示核对' : '显示风险'
|
||||||
|
))
|
||||||
|
const reviewRiskDrawerIcon = computed(() => (
|
||||||
|
isReviewRiskDrawer.value
|
||||||
|
? 'mdi mdi-shield-alert'
|
||||||
|
: 'mdi mdi-shield-alert-outline'
|
||||||
|
))
|
||||||
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
||||||
const activeReviewDocumentPreview = computed(() =>
|
const activeReviewDocumentPreview = computed(() =>
|
||||||
activeReviewDocument.value
|
activeReviewDocument.value
|
||||||
@@ -2252,6 +2313,7 @@ export default {
|
|||||||
activeReviewDocumentIndex.value = nextDocumentDrafts.length
|
activeReviewDocumentIndex.value = nextDocumentDrafts.length
|
||||||
? Math.min(activeReviewDocumentIndex.value, nextDocumentDrafts.length - 1)
|
? Math.min(activeReviewDocumentIndex.value, nextDocumentDrafts.length - 1)
|
||||||
: 0
|
: 0
|
||||||
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||||
reviewInlinePendingFiles.value = []
|
reviewInlinePendingFiles.value = []
|
||||||
reviewInlineEditorKey.value = ''
|
reviewInlineEditorKey.value = ''
|
||||||
reviewInlineErrors.value = {}
|
reviewInlineErrors.value = {}
|
||||||
@@ -2269,6 +2331,24 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => reviewDocumentDrawerAvailable.value,
|
||||||
|
(available) => {
|
||||||
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) {
|
||||||
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => reviewRiskDrawerAvailable.value,
|
||||||
|
(available) => {
|
||||||
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) {
|
||||||
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => composerDraft.value,
|
() => composerDraft.value,
|
||||||
() => {
|
() => {
|
||||||
@@ -2440,6 +2520,26 @@ export default {
|
|||||||
insightPanelCollapsed.value = !insightPanelCollapsed.value
|
insightPanelCollapsed.value = !insightPanelCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleReviewDocumentDrawer() {
|
||||||
|
if (!reviewDocumentDrawerAvailable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reviewDrawerMode.value =
|
||||||
|
reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS
|
||||||
|
? REVIEW_DRAWER_MODE_REVIEW
|
||||||
|
: REVIEW_DRAWER_MODE_DOCUMENTS
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleReviewRiskDrawer() {
|
||||||
|
if (!reviewRiskDrawerAvailable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reviewDrawerMode.value =
|
||||||
|
reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK
|
||||||
|
? REVIEW_DRAWER_MODE_REVIEW
|
||||||
|
: REVIEW_DRAWER_MODE_RISK
|
||||||
|
}
|
||||||
|
|
||||||
function setInlineReviewFieldError(key, message) {
|
function setInlineReviewFieldError(key, message) {
|
||||||
reviewInlineErrors.value = {
|
reviewInlineErrors.value = {
|
||||||
...reviewInlineErrors.value,
|
...reviewInlineErrors.value,
|
||||||
@@ -2778,7 +2878,10 @@ export default {
|
|||||||
? options.extraContext
|
? options.extraContext
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
|
// 只有在非静默模式下才添加用户消息
|
||||||
|
if (!options.skipUserMessage) {
|
||||||
messages.value.push(createMessage('user', userText, fileNames))
|
messages.value.push(createMessage('user', userText, fileNames))
|
||||||
|
}
|
||||||
|
|
||||||
const pendingMessage = createMessage(
|
const pendingMessage = createMessage(
|
||||||
'assistant',
|
'assistant',
|
||||||
@@ -2807,12 +2910,15 @@ export default {
|
|||||||
let ocrPayload = null
|
let ocrPayload = null
|
||||||
let ocrSummary = ''
|
let ocrSummary = ''
|
||||||
let ocrDocuments = []
|
let ocrDocuments = []
|
||||||
|
let ocrFilePreviews = []
|
||||||
|
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
try {
|
try {
|
||||||
ocrPayload = await recognizeOcrFiles(files)
|
ocrPayload = await recognizeOcrFiles(files)
|
||||||
ocrSummary = buildOcrSummary(ocrPayload)
|
ocrSummary = buildOcrSummary(ocrPayload)
|
||||||
ocrDocuments = normalizeOcrDocuments(ocrPayload)
|
ocrDocuments = normalizeOcrDocuments(ocrPayload)
|
||||||
|
ocrFilePreviews = buildOcrFilePreviews(ocrPayload)
|
||||||
|
rememberFilePreviews(ocrFilePreviews)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('OCR request failed:', error)
|
console.warn('OCR request failed:', error)
|
||||||
}
|
}
|
||||||
@@ -2863,7 +2969,11 @@ export default {
|
|||||||
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
|
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
currentInsight.value = buildAgentInsight(payload, fileNames, filePreviews)
|
currentInsight.value = buildAgentInsight(
|
||||||
|
payload,
|
||||||
|
fileNames,
|
||||||
|
mergeFilePreviews(filePreviews, ocrFilePreviews)
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
pendingMessage.id,
|
pendingMessage.id,
|
||||||
@@ -2958,6 +3068,13 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存草稿直接处理,不显示对话
|
||||||
|
if (actionType === 'save_draft') {
|
||||||
|
await handleSaveDraftDirectly(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一步继续使用对话流程
|
||||||
reviewActionBusy.value = true
|
reviewActionBusy.value = true
|
||||||
try {
|
try {
|
||||||
const baseFields = reviewInlineBaseFields.value.length
|
const baseFields = reviewInlineBaseFields.value.length
|
||||||
@@ -2981,25 +3098,70 @@ export default {
|
|||||||
rawText: [
|
rawText: [
|
||||||
reviewHasUnsavedChanges.value ? buildReviewCorrectionMessage(fields) : '',
|
reviewHasUnsavedChanges.value ? buildReviewCorrectionMessage(fields) : '',
|
||||||
reviewHasUnsavedChanges.value ? documentCorrectionMessage : '',
|
reviewHasUnsavedChanges.value ? documentCorrectionMessage : '',
|
||||||
actionType === 'save_draft'
|
'我已核对右侧识别结果,请进入下一步。'
|
||||||
? '请按当前已识别信息先保存草稿,缺失字段后续再补。'
|
|
||||||
: '我已核对右侧识别结果,请进入下一步。'
|
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
userText:
|
userText: reviewChangedUserText || '我确认当前识别结果,继续下一步。',
|
||||||
reviewChangedUserText
|
|
||||||
|| (actionType === 'save_draft' ? '我先按当前信息保存草稿。' : '我确认当前识别结果,继续下一步。'),
|
|
||||||
files: reviewInlinePendingFiles.value,
|
files: reviewInlinePendingFiles.value,
|
||||||
pendingText: actionType === 'save_draft' ? '正在保存当前草稿...' : '正在进入下一步...',
|
pendingText: '正在进入下一步...',
|
||||||
extraContext: {
|
extraContext: {
|
||||||
review_action: actionType,
|
review_action: actionType,
|
||||||
review_form_values: buildReviewFormValues(fields),
|
review_form_values: buildReviewFormValues(fields),
|
||||||
review_document_form_values: buildReviewDocumentCorrectionContext(reviewDocumentDrafts.value)
|
review_document_form_values: buildReviewDocumentCorrectionContext(reviewDocumentDrafts.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
reviewActionBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:直接保存草稿的函数,不显示对话
|
||||||
|
async function handleSaveDraftDirectly(message) {
|
||||||
|
reviewActionBusy.value = true
|
||||||
|
|
||||||
|
// 记录当前消息数量,用于后续移除 submitComposer 添加的消息
|
||||||
|
const messageCountBefore = messages.value.length
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseFields = reviewInlineBaseFields.value.length
|
||||||
|
? reviewInlineBaseFields.value
|
||||||
|
: cloneReviewEditFields(message?.reviewPayload?.edit_fields)
|
||||||
|
const fields = mergeInlineReviewFields(baseFields, reviewInlineForm.value)
|
||||||
|
|
||||||
|
// 先显示一个临时的"正在保存"消息
|
||||||
|
const savingMessage = createMessage('assistant', '正在保存草稿...', [], { meta: ['处理中'] })
|
||||||
|
messages.value.push(savingMessage)
|
||||||
|
nextTick(scrollToBottom)
|
||||||
|
|
||||||
|
// 调用保存逻辑,不通过对话
|
||||||
|
const payload = await submitComposer({
|
||||||
|
rawText: '请按当前已识别信息先保存草稿,缺失字段后续再补。',
|
||||||
|
userText: '', // 不显示用户消息
|
||||||
|
skipUserMessage: true, // 跳过添加用户消息
|
||||||
|
files: reviewInlinePendingFiles.value,
|
||||||
|
pendingText: '正在保存当前草稿...',
|
||||||
|
extraContext: {
|
||||||
|
review_action: 'save_draft',
|
||||||
|
review_form_values: buildReviewFormValues(fields),
|
||||||
|
review_document_form_values: buildReviewDocumentCorrectionContext(reviewDocumentDrafts.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移除临时消息
|
||||||
|
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
||||||
|
if (tempIndex !== -1) {
|
||||||
|
messages.value.splice(tempIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload?.result?.draft_payload?.claim_no) {
|
||||||
|
// 显示保存成功的消息
|
||||||
|
messages.value.push(
|
||||||
|
createMessage('assistant', `✅ 草稿已保存,单号:${payload.result.draft_payload.claim_no}`, [], {
|
||||||
|
meta: ['草稿已保存']
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if (actionType === 'save_draft' && payload?.result?.draft_payload?.claim_no) {
|
|
||||||
emit(
|
emit(
|
||||||
'draft-saved',
|
'draft-saved',
|
||||||
buildDraftSavedPayload({
|
buildDraftSavedPayload({
|
||||||
@@ -3010,7 +3172,21 @@ export default {
|
|||||||
currentUser: currentUser.value
|
currentUser: currentUser.value
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// 没有返回草稿信息,可能保存失败
|
||||||
|
messages.value.push(createMessage('assistant', '草稿保存完成', [], { meta: ['草稿已保存'] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextTick(scrollToBottom)
|
||||||
|
} catch (error) {
|
||||||
|
// 移除临时消息
|
||||||
|
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
||||||
|
if (tempIndex !== -1) {
|
||||||
|
messages.value.splice(tempIndex, 1)
|
||||||
|
}
|
||||||
|
// 显示错误消息
|
||||||
|
messages.value.push(createMessage('assistant', '❌ 保存草稿失败,请稍后重试。', [], { meta: ['错误'] }))
|
||||||
|
nextTick(scrollToBottom)
|
||||||
} finally {
|
} finally {
|
||||||
reviewActionBusy.value = false
|
reviewActionBusy.value = false
|
||||||
}
|
}
|
||||||
@@ -3046,6 +3222,16 @@ export default {
|
|||||||
latestReviewMessage,
|
latestReviewMessage,
|
||||||
activeReviewPayload,
|
activeReviewPayload,
|
||||||
activeReviewFilePreviews,
|
activeReviewFilePreviews,
|
||||||
|
reviewDrawerMode,
|
||||||
|
isReviewDocumentDrawer,
|
||||||
|
isReviewRiskDrawer,
|
||||||
|
reviewDrawerTitle,
|
||||||
|
reviewDocumentDrawerAvailable,
|
||||||
|
reviewRiskDrawerAvailable,
|
||||||
|
reviewDocumentDrawerLabel,
|
||||||
|
reviewDocumentDrawerIcon,
|
||||||
|
reviewRiskDrawerLabel,
|
||||||
|
reviewRiskDrawerIcon,
|
||||||
activeReviewDocument,
|
activeReviewDocument,
|
||||||
activeReviewDocumentIndex,
|
activeReviewDocumentIndex,
|
||||||
activeReviewDocumentPreview,
|
activeReviewDocumentPreview,
|
||||||
@@ -3119,6 +3305,8 @@ export default {
|
|||||||
resolveKnowledgeRankLabel,
|
resolveKnowledgeRankLabel,
|
||||||
resolveKnowledgeRankTone,
|
resolveKnowledgeRankTone,
|
||||||
toggleInsightPanel,
|
toggleInsightPanel,
|
||||||
|
toggleReviewDocumentDrawer,
|
||||||
|
toggleReviewRiskDrawer,
|
||||||
toggleAttachedFilesExpanded,
|
toggleAttachedFilesExpanded,
|
||||||
removeAttachedFile,
|
removeAttachedFile,
|
||||||
clearAttachedFiles,
|
clearAttachedFiles,
|
||||||
@@ -3144,6 +3332,7 @@ export default {
|
|||||||
saveInlineReviewChanges,
|
saveInlineReviewChanges,
|
||||||
submitComposer,
|
submitComposer,
|
||||||
handleReviewAction,
|
handleReviewAction,
|
||||||
|
handleSaveDraftDirectly,
|
||||||
closeCancelReviewDialog,
|
closeCancelReviewDialog,
|
||||||
confirmCancelReview,
|
confirmCancelReview,
|
||||||
closeEditReviewDialog,
|
closeEditReviewDialog,
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ const EXPENSE_TYPE_OPTIONS = [
|
|||||||
{ value: 'other', label: '其他费用' }
|
{ value: 'other', label: '其他费用' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const DOCUMENT_TYPE_LABELS = {
|
||||||
|
flight_itinerary: '机票/航班行程单',
|
||||||
|
train_ticket: '火车/高铁票',
|
||||||
|
hotel_invoice: '酒店住宿票据',
|
||||||
|
taxi_receipt: '出租车/网约车票据',
|
||||||
|
parking_toll_receipt: '停车/通行费票据',
|
||||||
|
meal_receipt: '餐饮票据',
|
||||||
|
office_invoice: '办公用品票据',
|
||||||
|
meeting_invoice: '会议/会务票据',
|
||||||
|
training_invoice: '培训票据',
|
||||||
|
vat_invoice: '增值税发票',
|
||||||
|
receipt: '一般收据/凭证',
|
||||||
|
other: '其他单据'
|
||||||
|
}
|
||||||
|
|
||||||
const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([
|
const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([
|
||||||
'travel',
|
'travel',
|
||||||
'hotel',
|
'hotel',
|
||||||
@@ -57,6 +72,10 @@ function resolveExpenseTypeLabel(value) {
|
|||||||
return EXPENSE_TYPE_OPTIONS.find((option) => option.value === normalizeExpenseType(value))?.label || '其他费用'
|
return EXPENSE_TYPE_OPTIONS.find((option) => option.value === normalizeExpenseType(value))?.label || '其他费用'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDocumentTypeLabel(value) {
|
||||||
|
return DOCUMENT_TYPE_LABELS[String(value || '').trim()] || DOCUMENT_TYPE_LABELS.other
|
||||||
|
}
|
||||||
|
|
||||||
function isLocationRequiredExpenseType(value) {
|
function isLocationRequiredExpenseType(value) {
|
||||||
return LOCATION_REQUIRED_EXPENSE_TYPES.has(normalizeExpenseType(value))
|
return LOCATION_REQUIRED_EXPENSE_TYPES.has(normalizeExpenseType(value))
|
||||||
}
|
}
|
||||||
@@ -582,6 +601,38 @@ export default {
|
|||||||
return String(metadata?.file_name || item.attachmentHint || '').trim()
|
return String(metadata?.file_name || item.attachmentHint || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAttachmentRecognition(item) {
|
||||||
|
const metadata = resolveAttachmentMeta(item)
|
||||||
|
const documentInfo = metadata?.document_info
|
||||||
|
const requirementCheck = metadata?.requirement_check
|
||||||
|
if (!documentInfo && !requirementCheck) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = Array.isArray(documentInfo?.fields)
|
||||||
|
? documentInfo.fields
|
||||||
|
.map((field) => ({
|
||||||
|
label: String(field?.label || '').trim(),
|
||||||
|
value: String(field?.value || '').trim()
|
||||||
|
}))
|
||||||
|
.filter((field) => field.label && field.value)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentTypeLabel:
|
||||||
|
String(documentInfo?.document_type_label || '').trim()
|
||||||
|
|| resolveDocumentTypeLabel(documentInfo?.document_type),
|
||||||
|
requirementLabel: requirementCheck
|
||||||
|
? (requirementCheck.matches ? '符合当前费用类型' : '不符合当前费用类型')
|
||||||
|
: '待校验附件类型',
|
||||||
|
requirementTone: requirementCheck
|
||||||
|
? (requirementCheck.matches ? 'pass' : 'high')
|
||||||
|
: 'medium',
|
||||||
|
message: String(requirementCheck?.message || '').trim(),
|
||||||
|
fields: fields.slice(0, 4).map((field) => `${field.label}:${field.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildAttachmentRiskNotice(attachment) {
|
function buildAttachmentRiskNotice(attachment) {
|
||||||
const analysis = attachment?.analysis
|
const analysis = attachment?.analysis
|
||||||
const severity = String(analysis?.severity || '').trim()
|
const severity = String(analysis?.severity || '').trim()
|
||||||
@@ -1144,6 +1195,7 @@ export default {
|
|||||||
request,
|
request,
|
||||||
removeExpenseAttachment,
|
removeExpenseAttachment,
|
||||||
resolveAttachmentDisplayName,
|
resolveAttachmentDisplayName,
|
||||||
|
resolveAttachmentRecognition,
|
||||||
resolveExpenseRiskState,
|
resolveExpenseRiskState,
|
||||||
resolveExpenseIssues,
|
resolveExpenseIssues,
|
||||||
savingExpenseId,
|
savingExpenseId,
|
||||||
|
|||||||
Reference in New Issue
Block a user