feat(web): update travel reimbursement view

- travel-reimbursement-create-view.css: update form styles
- TravelReimbursementCreateView.vue: update view component
- scripts/TravelReimbursementCreateView.js: update view logic
This commit is contained in:
caoxiaozhu
2026-05-12 07:22:11 +00:00
parent bff20d8eb3
commit 4d748bcdeb
3 changed files with 678 additions and 240 deletions

View File

@@ -1,7 +1,7 @@
<template>
<Teleport to="body">
<Transition name="assistant-modal">
<div class="assistant-overlay" @click.self="emit('close')">
<div class="assistant-overlay">
<section class="assistant-modal">
<header class="assistant-header">
<div class="assistant-header-main">
@@ -43,7 +43,10 @@
:class="message.role"
>
<span class="message-avatar">
<i :class="message.role === 'assistant' ? 'mdi mdi-robot-excited-outline' : 'mdi mdi-account-circle-outline'"></i>
<img
:src="message.role === 'assistant' ? aiAvatar : userAvatar"
:alt="message.role === 'assistant' ? 'AI 助手头像' : '用户头像'"
/>
</span>
<div class="message-bubble">
@@ -53,18 +56,18 @@
</header>
<p>{{ message.text }}</p>
<div v-if="message.role === 'assistant' && message.meta?.length" class="message-meta-row">
<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.riskFlags?.length" class="message-detail-block">
<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.citations?.length" class="message-detail-block">
<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">
@@ -77,7 +80,7 @@
</div>
</div>
<div v-if="message.role === 'assistant' && message.suggestedActions?.length" class="message-detail-block">
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.suggestedActions?.length" class="message-detail-block">
<strong>建议动作</strong>
<div class="message-detail-chip-row">
<span
@@ -91,35 +94,27 @@
</div>
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block">
<strong>系统识别</strong>
<p class="review-summary">{{ message.reviewPayload.intent_summary }}</p>
<div v-if="message.reviewPayload.slot_cards?.length" class="review-mini-grid">
<article
v-for="item in message.reviewPayload.slot_cards.slice(0, 4)"
:key="`${message.id}-${item.key}`"
class="review-slot-card compact"
:class="item.status"
>
<span>{{ item.label }}</span>
<strong>{{ item.value || '待补充' }}</strong>
</article>
</div>
<div v-if="message.reviewPayload.confirmation_actions?.length" class="action-list compact">
<article
<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}`"
class="action-card"
type="button"
class="review-inline-btn"
:class="item.emphasis"
:disabled="reviewActionBusy"
@click="handleReviewAction(message, item)"
>
<div>
<strong>{{ item.label }}</strong>
<p>{{ item.description }}</p>
</div>
</article>
{{ item.label }}
</button>
</div>
<div v-if="message.reviewPayload.missing_slots?.length" class="review-inline-note">
当前仍需补充{{ message.reviewPayload.missing_slots.join('') }}
</div>
</div>
<div v-if="message.role === 'assistant' && message.draftPayload" class="draft-preview">
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload" class="draft-preview">
<header>
<strong>{{ message.draftPayload.title }}</strong>
<span>待人工确认</span>
@@ -152,7 +147,8 @@
v-model="composerDraft"
rows="3"
:placeholder="composerPlaceholder"
:disabled="submitting"
:disabled="submitting || reviewActionBusy"
@keydown.enter.exact.stop
@keydown.ctrl.enter.prevent="submitComposer"
/>
@@ -165,13 +161,13 @@
<div class="composer-foot">
<div class="composer-tools">
<button type="button" class="tool-btn" :disabled="submitting" aria-label="上传附件" @click="triggerFileUpload">
<button type="button" class="tool-btn" :disabled="submitting || reviewActionBusy" aria-label="上传附件" @click="triggerFileUpload">
<i class="mdi mdi-paperclip"></i>
</button>
<span class="composer-tip">Ctrl + Enter 发送</span>
<span class="composer-tip">Enter 换行Ctrl + Enter 发送</span>
</div>
<button class="send-btn" type="submit" :disabled="!canSubmit" aria-label="发送">
<button class="send-btn" type="submit" :disabled="!canSubmit || reviewActionBusy" aria-label="发送">
<i :class="submitting ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
</button>
</div>
@@ -184,11 +180,15 @@
<div class="insight-head">
<div>
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
<h3>{{ currentInsight.title }}</h3>
<p>{{ currentInsight.summary }}</p>
<h3>{{ activeReviewPayload ? '报销识别核对' : currentInsight.title }}</h3>
<p>{{ activeReviewPayload?.intent_summary || currentInsight.summary }}</p>
</div>
<div class="confidence-card">
<div class="confidence-card" v-if="activeReviewPayload">
<span>当前状态</span>
<strong>{{ activeReviewPayload.can_proceed ? '可进入下一步' : '待补充' }}</strong>
</div>
<div class="confidence-card" v-else>
<span>{{ currentInsight.metricLabel }}</span>
<strong>{{ currentInsight.metricValue }}</strong>
</div>
@@ -197,71 +197,80 @@
<Transition name="insight-switch" mode="out-in">
<div :key="currentInsight.intent + currentInsight.title" class="insight-body">
<template v-if="currentInsight.intent === 'agent' && currentInsight.agent">
<section class="insight-card primary">
<div class="card-head">
<h4>调度结果</h4>
<span class="status-pill" :class="currentInsight.agent.statusTone">{{ currentInsight.agent.statusLabel }}</span>
</div>
<div class="metric-grid">
<div class="metric-item">
<span>运行 ID</span>
<strong>{{ currentInsight.agent.runId }}</strong>
<template v-if="activeReviewPayload">
<section class="insight-card primary">
<div class="card-head">
<h4>识别结论</h4>
</div>
<div class="metric-item">
<span>执行 Agent</span>
<strong>{{ currentInsight.agent.selectedAgent }}</strong>
<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>
<div class="metric-item">
<span>场景 / 意图</span>
<strong>{{ currentInsight.agent.scenario }} / {{ currentInsight.agent.intent }}</strong>
</div>
<div class="metric-item">
<span>权限</span>
<strong>{{ currentInsight.agent.permissionLevel }}</strong>
</div>
</div>
</section>
</section>
<section v-if="currentInsight.agent.reviewPayload" class="insight-card">
<div class="card-head">
<h4>识别结果</h4>
</div>
<div class="note-block">
<span>{{ currentInsight.agent.reviewPayload.intent }}</span>
<strong>{{ currentInsight.agent.reviewPayload.intent_summary }}</strong>
<p v-if="currentInsight.agent.reviewPayload.missing_slots?.length">
当前仍缺少{{ currentInsight.agent.reviewPayload.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">
<article
v-for="item in recognizedSlotCards"
:key="item.key"
class="review-slot-card"
:class="item.status"
>
<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>
</div>
<p v-if="item.evidence">{{ item.evidence }}</p>
<p v-else-if="item.hint">{{ item.hint }}</p>
</article>
</div>
</section>
<section class="insight-card">
<div class="card-head">
<h4>运行明细</h4>
</div>
<div class="metric-grid">
<div class="metric-item">
<span>路由原因</span>
<strong>{{ currentInsight.agent.routeReason }}</strong>
<section v-if="missingSlotCards.length" class="insight-card">
<div class="card-head">
<h4>待补字段</h4>
</div>
<div class="metric-item">
<span>工具调用</span>
<strong>{{ currentInsight.agent.toolCount }} / 失败 {{ currentInsight.agent.failedToolCount }}</strong>
<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>
</div>
<div class="metric-item">
<span>确认要求</span>
<strong>{{ currentInsight.agent.requiresConfirmation ? '需要人工确认' : '无需确认' }}</strong>
</div>
<div class="metric-item">
<span>降级状态</span>
<strong>{{ currentInsight.agent.degraded ? '已降级' : '正常' }}</strong>
</div>
</div>
</section>
</section>
</template>
<section v-if="currentInsight.agent.reviewPayload?.risk_briefs?.length" class="insight-card">
<div class="card-head">
<h4>历史风险与注意事项</h4>
<h4>风险与注意事项</h4>
</div>
<div class="review-brief-list">
<article
@@ -276,27 +285,6 @@
</div>
</section>
<section v-if="currentInsight.agent.reviewPayload?.slot_cards?.length" class="insight-card">
<div class="card-head">
<h4>待确认字段</h4>
</div>
<div class="review-slot-grid">
<article
v-for="item in currentInsight.agent.reviewPayload.slot_cards"
:key="item.key"
class="review-slot-card"
:class="item.status"
>
<header>
<span>{{ item.label }}</span>
<small>{{ item.source }}</small>
</header>
<strong>{{ item.value || '待补充' }}</strong>
<p v-if="item.hint">{{ item.hint }}</p>
</article>
</div>
</section>
<section v-if="currentInsight.agent.reviewPayload?.claim_groups?.length" class="insight-card">
<div class="card-head">
<h4>分单建议</h4>
@@ -338,15 +326,15 @@
<span class="message-action-chip">{{ item.scene_label }}</span>
</header>
<div class="document-preview" :class="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.kind || 'file'">
<div class="document-preview" :class="resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.kind || 'file'">
<img
v-if="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.kind === 'image'"
:src="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.url"
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(currentInsight.agent.filePreviews, item.filename)?.kind === 'pdf' ? 'PDF' : '附件' }}</span>
<span>{{ resolveDocumentPreview(activeReviewFilePreviews, item.filename)?.kind === 'pdf' ? 'PDF' : '附件' }}</span>
</div>
</div>
@@ -366,58 +354,9 @@
</div>
</section>
<section v-if="currentInsight.agent.reviewPayload?.confirmation_actions?.length" class="insight-card">
<div class="card-head">
<h4>确认动作</h4>
</div>
<div class="action-list">
<article
v-for="item in currentInsight.agent.reviewPayload.confirmation_actions"
:key="`${item.action_type}-${item.label}`"
class="action-card"
:class="item.emphasis"
>
<div>
<strong>{{ item.label }}</strong>
<p>{{ item.description || item.action_type }}</p>
</div>
</article>
</div>
</section>
<section v-if="currentInsight.agent.selectedCapabilityCodes?.length" class="insight-card">
<div class="card-head">
<h4>命中能力</h4>
</div>
<div class="capability-chip-row">
<span v-for="item in currentInsight.agent.selectedCapabilityCodes" :key="item" class="capability-chip">
{{ item }}
</span>
</div>
</section>
<section v-if="currentInsight.agent.fileNames?.length" class="insight-card">
<div class="card-head">
<h4>附件上下文</h4>
</div>
<ul class="bullet-list">
<li>本次对话已带入 {{ currentInsight.agent.fileNames.length }} 份附件名称</li>
<li v-for="item in currentInsight.agent.fileNames" :key="item">{{ item }}</li>
</ul>
</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>
<section v-if="currentInsight.agent.citations?.length" class="insight-card">
<div class="card-head">
<h4>引用依据</h4>
<h4>制度依据</h4>
</div>
<div class="citation-stack">
<article v-for="item in currentInsight.agent.citations" :key="item.code" class="citation-card">
@@ -430,30 +369,26 @@
</div>
</section>
<section v-if="currentInsight.agent.suggestedActions?.length" class="insight-card">
<div class="card-head">
<h4>建议动作</h4>
</div>
<div class="action-list">
<article v-for="item in currentInsight.agent.suggestedActions" :key="item.label" class="action-card">
<div>
<strong>{{ item.label }}</strong>
<p>{{ item.description || item.action_type }}</p>
</div>
</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.draftPayload" class="insight-card">
<div class="card-head">
<h4>草稿内容</h4>
</div>
<div class="note-block">
<span>{{ currentInsight.agent.draftPayload.draft_type }}</span>
<strong>{{ currentInsight.agent.draftPayload.title }}</strong>
<p>{{ currentInsight.agent.draftPayload.body }}</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>
@@ -463,6 +398,71 @@
</section>
</div>
</Transition>
<Transition name="assistant-modal">
<div v-if="reviewCancelDialogOpen" class="assistant-overlay review-overlay">
<section class="review-confirm-modal">
<header>
<span class="assistant-badge warning">取消核对</span>
<h3>确认放弃本次识别结果</h3>
<p>关闭后将退出当前核对窗口本次尚未确认的修改不会继续保留</p>
</header>
<div class="review-confirm-actions">
<button type="button" class="secondary-dialog-btn" :disabled="reviewActionBusy" @click="closeCancelReviewDialog">返回继续核对</button>
<button type="button" class="danger-dialog-btn" :disabled="reviewActionBusy" @click="confirmCancelReview">确认取消</button>
</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>