Files
X-Financial/web/src/views/TravelReimbursementCreateView.vue
caoxiaozhu 8b72f4e962 feat(web): update views
- AppShellRouteView.vue: update app shell route view
- AuditView.vue: update audit view
- EmployeeManagementView.vue: update employee management view
- PoliciesView.vue: update policies view
- RequestsView.vue: update requests view
- TravelReimbursementCreateView.vue: update travel form view
- TravelRequestDetailView.vue: update travel detail view
2026-05-13 03:33:11 +00:00

553 lines
28 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-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 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 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 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.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" 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>
<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 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>
{{ file.name }}
</span>
</div>
<div class="composer-foot">
<div class="composer-tools">
<button type="button" class="tool-btn" :disabled="submitting || reviewActionBusy" aria-label="上传附件" @click="triggerFileUpload">
<i class="mdi mdi-paperclip"></i>
</button>
<span class="composer-tip">Enter 换行Ctrl + Enter 发送</span>
</div>
<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>
</div>
</form>
</section>
<Transition name="insight-panel">
<aside v-if="showInsightPanel" class="insight-panel">
<div 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">
<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>{{ currentInsight.metricLabel }}</span>
<strong>{{ currentInsight.metricValue }}</strong>
</div>
</div>
<Transition name="insight-switch" mode="out-in">
<div :key="currentInsight.intent + currentInsight.title" class="insight-body">
<template v-if="currentInsight.intent === 'agent' && currentInsight.agent">
<template v-if="activeReviewPayload">
<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>
</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>
<section class="review-side-card review-side-risk-card">
<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>
<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.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>
</Transition>
</div>
</div>
</section>
</div>
</Transition>
<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="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>