feat(web): update Vue components
- App.vue: update app root component - SidebarRail.vue: update sidebar rail component - TravelReimbursementCreateView.vue: update travel form view
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
<ToastNotification :toast-text="toastText" />
|
||||
<div class="app-desktop-shell" :style="desktopScaleStyle">
|
||||
<div class="app-desktop-stage">
|
||||
<RouterView />
|
||||
<ToastNotification :toast-text="toastText" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import './assets/styles/global.css'
|
||||
@@ -12,6 +17,50 @@ import ToastNotification from './components/shared/ToastNotification.vue'
|
||||
import { useToast } from './composables/useToast.js'
|
||||
|
||||
const { toastText } = useToast()
|
||||
|
||||
const DESKTOP_BASE_WIDTH = 1600
|
||||
const DESKTOP_BASE_HEIGHT = 920
|
||||
const DESKTOP_MIN_SCALE = 0.82
|
||||
|
||||
const viewportWidth = ref(typeof window === 'undefined' ? DESKTOP_BASE_WIDTH : window.innerWidth)
|
||||
const viewportHeight = ref(typeof window === 'undefined' ? DESKTOP_BASE_HEIGHT : window.innerHeight)
|
||||
|
||||
const desktopScaleStyle = computed(() => {
|
||||
const scale = resolveDesktopScale(viewportWidth.value, viewportHeight.value)
|
||||
const inverseScale = Number((1 / scale).toFixed(4))
|
||||
|
||||
return {
|
||||
'--desktop-ui-scale': scale.toFixed(4),
|
||||
'--desktop-ui-inverse-scale': inverseScale.toFixed(4),
|
||||
'--desktop-stage-width': `${Math.round(viewportWidth.value * inverseScale)}px`,
|
||||
'--desktop-stage-height': `${Math.round(viewportHeight.value * inverseScale)}px`
|
||||
}
|
||||
})
|
||||
|
||||
function resolveDesktopScale(width, height) {
|
||||
if (width < 960) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const scaleByWidth = width / DESKTOP_BASE_WIDTH
|
||||
const scaleByHeight = height / DESKTOP_BASE_HEIGHT
|
||||
|
||||
return Number(Math.min(1, Math.max(DESKTOP_MIN_SCALE, Math.min(scaleByWidth, scaleByHeight))).toFixed(4))
|
||||
}
|
||||
|
||||
function updateViewport() {
|
||||
viewportWidth.value = window.innerWidth
|
||||
viewportHeight.value = window.innerHeight
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateViewport()
|
||||
window.addEventListener('resize', updateViewport, { passive: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateViewport)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style src="./assets/styles/app.css"></style>
|
||||
|
||||
@@ -103,7 +103,7 @@ const displayCompanyName = computed(() => props.companyName || 'X-Financial')
|
||||
.rail {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100dvh;
|
||||
height: var(--desktop-stage-height, 100dvh);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
background:
|
||||
|
||||
@@ -3,58 +3,62 @@
|
||||
<Transition name="assistant-modal">
|
||||
<div class="assistant-overlay">
|
||||
<section class="assistant-modal">
|
||||
<header class="assistant-header">
|
||||
<div class="assistant-header-main">
|
||||
<span class="assistant-badge">AI Workspace</span>
|
||||
<div>
|
||||
<h2>统一对话工作台</h2>
|
||||
<p>个人工作台、发起报销、智能录入统一走这里,右侧会根据你的意图实时切换状态视图。</p>
|
||||
<div class="assistant-modal-stage">
|
||||
<header class="assistant-header">
|
||||
<div class="assistant-header-main">
|
||||
<span class="assistant-badge">AI Workspace</span>
|
||||
<div>
|
||||
<h2>统一对话工作台</h2>
|
||||
<p>个人工作台、发起报销、智能录入统一走这里,右侧会根据你的意图实时切换状态视图。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assistant-header-actions">
|
||||
<span class="source-pill">{{ sourceLabel }}</span>
|
||||
<button class="close-btn" type="button" aria-label="关闭对话工作台" @click="emit('close')">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="assistant-layout" :class="{ 'has-insight': showInsightPanel }">
|
||||
<section class="dialog-panel">
|
||||
<div class="dialog-toolbar">
|
||||
<button
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.label"
|
||||
type="button"
|
||||
class="shortcut-chip"
|
||||
@click="runShortcut(shortcut.prompt)"
|
||||
>
|
||||
<i :class="shortcut.icon"></i>
|
||||
<span>{{ shortcut.label }}</span>
|
||||
<div class="assistant-header-actions">
|
||||
<span class="source-pill">
|
||||
<i class="mdi mdi-arrow-u-left-top"></i>
|
||||
{{ sourceLabel }}
|
||||
</span>
|
||||
<button class="close-btn" type="button" aria-label="关闭对话工作台" @click="emit('close')">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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="assistant-layout" :class="{ 'has-insight': showInsightPanel }">
|
||||
<section class="dialog-panel">
|
||||
<div class="dialog-toolbar">
|
||||
<button
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.label"
|
||||
type="button"
|
||||
class="shortcut-chip"
|
||||
@click="runShortcut(shortcut.prompt)"
|
||||
>
|
||||
<i :class="shortcut.icon"></i>
|
||||
<span>{{ shortcut.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="message-bubble">
|
||||
<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>{{ message.text }}</p>
|
||||
<p v-if="!(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>
|
||||
@@ -93,24 +97,90 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block">
|
||||
<strong>核对提示</strong>
|
||||
<p class="review-summary">{{ message.reviewPayload.body_message || '相关识别信息已在右侧展示,请核对。' }}</p>
|
||||
<div v-if="message.reviewPayload.confirmation_actions?.length" class="review-inline-actions">
|
||||
<button
|
||||
v-for="item in message.reviewPayload.confirmation_actions"
|
||||
:key="`${message.id}-${item.action_type}-${item.label}`"
|
||||
type="button"
|
||||
class="review-inline-btn"
|
||||
:class="item.emphasis"
|
||||
:disabled="reviewActionBusy"
|
||||
@click="handleReviewAction(message, item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="message.reviewPayload.missing_slots?.length" class="review-inline-note">
|
||||
当前仍需补充:{{ message.reviewPayload.missing_slots.join('、') }}
|
||||
<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-check-decagram"></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>
|
||||
|
||||
<div class="review-flow-card">
|
||||
<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 class="review-section-head review-flow-head">
|
||||
<strong>待补充内容</strong>
|
||||
<span>{{ buildReviewTodoItems(message.reviewPayload).length ? `${buildReviewTodoItems(message.reviewPayload).length} 项` : '已齐全' }}</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div v-if="resolveReviewPrimaryAction(message.reviewPayload) || message.draftPayload?.claim_no" class="review-footer-actions">
|
||||
<div class="review-footer-btn-row">
|
||||
<button
|
||||
v-if="resolveReviewPrimaryAction(message.reviewPayload)"
|
||||
type="button"
|
||||
class="review-footer-btn primary"
|
||||
:disabled="reviewActionBusy"
|
||||
@click="handleReviewAction(message, resolveReviewPrimaryAction(message.reviewPayload))"
|
||||
>
|
||||
{{ buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="review-footer-btn"
|
||||
:disabled="submitting || reviewActionBusy"
|
||||
@click="triggerFileUpload"
|
||||
>
|
||||
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="message.draftPayload?.claim_no"
|
||||
type="button"
|
||||
class="review-footer-btn"
|
||||
:disabled="submitting || reviewActionBusy"
|
||||
@click="queryDraftByClaimNo(message.draftPayload.claim_no)"
|
||||
>
|
||||
查看草稿 {{ message.draftPayload.claim_no }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -128,30 +198,30 @@
|
||||
{{ file }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</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 class="composer-shell">
|
||||
<textarea
|
||||
v-model="composerDraft"
|
||||
rows="3"
|
||||
:placeholder="composerPlaceholder"
|
||||
:disabled="submitting || reviewActionBusy"
|
||||
@keydown.enter.exact.stop
|
||||
@keydown.ctrl.enter.prevent="submitComposer"
|
||||
<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 class="composer-shell">
|
||||
<textarea
|
||||
v-model="composerDraft"
|
||||
rows="3"
|
||||
:placeholder="composerPlaceholder"
|
||||
:disabled="submitting || reviewActionBusy"
|
||||
@keydown.enter.exact.stop
|
||||
@keydown.ctrl.enter.prevent="submitComposer"
|
||||
/>
|
||||
|
||||
<div v-if="attachedFiles.length" class="composer-files">
|
||||
<span v-for="file in attachedFiles" :key="file.name" class="file-chip active">
|
||||
<i class="mdi mdi-paperclip"></i>
|
||||
@@ -171,24 +241,26 @@
|
||||
<i :class="submitting ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<Transition name="insight-panel">
|
||||
<aside v-if="showInsightPanel" class="insight-panel">
|
||||
<div class="insight-head">
|
||||
<Transition name="insight-panel">
|
||||
<aside v-if="showInsightPanel" class="insight-panel">
|
||||
<div class="insight-head" :class="{ 'review-mode': activeReviewPayload }">
|
||||
<div>
|
||||
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
|
||||
<h3>{{ activeReviewPayload ? '报销识别核对' : currentInsight.title }}</h3>
|
||||
<p>{{ activeReviewPayload?.intent_summary || currentInsight.summary }}</p>
|
||||
<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">
|
||||
<h3>报销识别核对</h3>
|
||||
<span class="insight-head-badge">实时意图分析</span>
|
||||
</div>
|
||||
<h3 v-if="!activeReviewPayload">{{ currentInsight.title }}</h3>
|
||||
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="confidence-card" v-if="activeReviewPayload">
|
||||
<span>当前状态</span>
|
||||
<strong>{{ activeReviewPayload.can_proceed ? '可进入下一步' : '待补充' }}</strong>
|
||||
</div>
|
||||
<div class="confidence-card" v-else>
|
||||
<div class="confidence-card" v-if="!activeReviewPayload">
|
||||
<span>{{ currentInsight.metricLabel }}</span>
|
||||
<strong>{{ currentInsight.metricValue }}</strong>
|
||||
</div>
|
||||
@@ -198,163 +270,142 @@
|
||||
<div :key="currentInsight.intent + currentInsight.title" class="insight-body">
|
||||
<template v-if="currentInsight.intent === 'agent' && currentInsight.agent">
|
||||
<template v-if="activeReviewPayload">
|
||||
<section class="insight-card primary">
|
||||
<div class="card-head">
|
||||
<h4>识别结论</h4>
|
||||
<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>
|
||||
<div class="note-block review-conclusion">
|
||||
<strong>{{ activeReviewPayload.intent_summary }}</strong>
|
||||
<p>{{ activeReviewPayload.body_message }}</p>
|
||||
<p v-if="activeReviewPayload.missing_slots?.length">
|
||||
当前仍缺少:{{ activeReviewPayload.missing_slots.join('、') }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="recognizedSlotCards.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>已识别字段</h4>
|
||||
</div>
|
||||
<div class="review-slot-grid">
|
||||
<section class="review-side-grid compact">
|
||||
<article
|
||||
v-for="item in recognizedSlotCards"
|
||||
v-for="item in reviewFactCards"
|
||||
:key="item.key"
|
||||
class="review-slot-card"
|
||||
:class="item.status"
|
||||
class="review-side-metric-card"
|
||||
:class="{ editable: item.editor, editing: reviewInlineEditorKey === item.key }"
|
||||
@click="openInlineReviewEditor(item.key)"
|
||||
>
|
||||
<header>
|
||||
<span>{{ item.label }}</span>
|
||||
<small>{{ item.source_label || item.source }}</small>
|
||||
</header>
|
||||
<strong>{{ item.value || '待补充' }}</strong>
|
||||
<div class="review-slot-meta-list">
|
||||
<div v-if="item.raw_value && item.raw_value !== item.value" class="review-slot-meta-item">
|
||||
<span>原始表达</span>
|
||||
<strong>{{ item.raw_value }}</strong>
|
||||
</div>
|
||||
<div v-if="item.normalized_value" class="review-slot-meta-item">
|
||||
<span>标准值</span>
|
||||
<strong>{{ item.normalized_value }}</strong>
|
||||
</div>
|
||||
<div class="review-slot-meta-item">
|
||||
<span>置信度</span>
|
||||
<strong>{{ Math.round((item.confidence || 0) * 100) }}%</strong>
|
||||
</div>
|
||||
<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.key === 'occurred_date'">
|
||||
<input
|
||||
v-model="reviewInlineForm.occurred_date"
|
||||
class="review-inline-input"
|
||||
type="date"
|
||||
@click.stop
|
||||
@change="commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="reviewInlineEditorKey === item.key && item.key === 'amount'">
|
||||
<input
|
||||
v-model="reviewInlineForm.amount"
|
||||
class="review-inline-input"
|
||||
type="text"
|
||||
placeholder="例如 200.00元"
|
||||
@click.stop
|
||||
@blur="commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="reviewInlineEditorKey === item.key && item.key === 'customer_name'">
|
||||
<input
|
||||
v-model="reviewInlineForm.customer_name"
|
||||
class="review-inline-input"
|
||||
type="text"
|
||||
placeholder="请输入客户名称"
|
||||
@click.stop
|
||||
@blur="commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="reviewInlineEditorKey === item.key && item.key === 'scene'">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<strong v-else>{{ item.value }}</strong>
|
||||
</div>
|
||||
<p v-if="item.evidence">{{ item.evidence }}</p>
|
||||
<p v-else-if="item.hint">{{ item.hint }}</p>
|
||||
<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 REVIEW_OTHER_CATEGORY_OPTIONS"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="review-other-category-option"
|
||||
:class="{ active: reviewSelectedOtherCategory === item.label }"
|
||||
@click="selectReviewOtherCategory(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="missingSlotCards.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>待补字段</h4>
|
||||
</div>
|
||||
<div class="review-slot-grid">
|
||||
<article
|
||||
v-for="item in missingSlotCards"
|
||||
:key="item.key"
|
||||
class="review-slot-card missing"
|
||||
>
|
||||
<header>
|
||||
<span>{{ item.label }}</span>
|
||||
<small>待用户补充</small>
|
||||
</header>
|
||||
<strong>待补充</strong>
|
||||
<p>{{ item.hint || '请补充该字段后再继续。' }}</p>
|
||||
</article>
|
||||
<section class="review-side-card review-side-risk-card">
|
||||
<div class="review-side-head">
|
||||
<strong>合规提醒 / 风险评分</strong>
|
||||
<span class="review-side-risk-score">{{ reviewRiskScore }}/100</span>
|
||||
</div>
|
||||
<p class="review-side-risk-summary">{{ reviewRiskSummary }}</p>
|
||||
<ul class="review-side-risk-list">
|
||||
<li v-for="item in reviewRiskItems" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
<button type="button" class="review-side-link" :disabled="submitting || reviewActionBusy" @click="explainCurrentReviewRisk">
|
||||
查看全部风险项
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<button
|
||||
v-if="reviewInlineDirty"
|
||||
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.reviewPayload?.risk_briefs?.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>风险与注意事项</h4>
|
||||
</div>
|
||||
<div class="review-brief-list">
|
||||
<article
|
||||
v-for="item in currentInsight.agent.reviewPayload.risk_briefs"
|
||||
:key="`${item.title}-${item.level}`"
|
||||
class="review-brief-card"
|
||||
:class="item.level"
|
||||
>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.content }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentInsight.agent.reviewPayload?.claim_groups?.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>分单建议</h4>
|
||||
</div>
|
||||
<div class="review-claim-list">
|
||||
<article
|
||||
v-for="item in currentInsight.agent.reviewPayload.claim_groups"
|
||||
:key="item.group_code"
|
||||
class="review-claim-card"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.scene_label }}</span>
|
||||
</header>
|
||||
<p>{{ item.rationale }}</p>
|
||||
<div class="message-detail-chip-row">
|
||||
<span class="message-action-chip">票据 {{ item.document_indexes.join('、') || '待补' }}</span>
|
||||
<span class="message-action-chip">金额 {{ item.amount_total.toFixed(2) }} 元</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentInsight.agent.reviewPayload?.document_cards?.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>票据识别结果</h4>
|
||||
</div>
|
||||
<div class="review-document-list">
|
||||
<article
|
||||
v-for="item in currentInsight.agent.reviewPayload.document_cards"
|
||||
:key="`${item.index}-${item.filename}`"
|
||||
class="review-document-card"
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<strong>票据 {{ item.index }}</strong>
|
||||
<span>{{ item.filename }}</span>
|
||||
</div>
|
||||
<span class="message-action-chip">{{ item.scene_label }}</span>
|
||||
</header>
|
||||
|
||||
<div class="document-preview" :class="resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.kind || 'file'">
|
||||
<img
|
||||
v-if="resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.kind === 'image'"
|
||||
:src="resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.url"
|
||||
:alt="item.filename"
|
||||
/>
|
||||
<div v-else class="document-preview-placeholder">
|
||||
<i class="mdi mdi-file-document-outline"></i>
|
||||
<span>{{ resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.kind === 'pdf' ? 'PDF' : '附件' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{{ item.summary || '暂无摘要。' }}</p>
|
||||
|
||||
<div v-if="item.fields?.length" class="review-doc-field-grid">
|
||||
<article v-for="field in item.fields" :key="`${item.filename}-${field.label}`" class="review-doc-field-card">
|
||||
<span>{{ field.label }}</span>
|
||||
<strong>{{ field.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="item.warnings?.length" class="message-detail-chip-row">
|
||||
<span v-for="warning in item.warnings" :key="warning" class="message-risk-chip">{{ warning }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentInsight.agent.citations?.length" class="insight-card">
|
||||
<section v-if="currentInsight.agent.citations?.length && !activeReviewPayload" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>制度依据</h4>
|
||||
</div>
|
||||
@@ -392,8 +443,9 @@
|
||||
</template>
|
||||
</div>
|
||||
</Transition>
|
||||
</aside>
|
||||
</Transition>
|
||||
</aside>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user