Files
X-Financial/web/src/views/TravelRequestDetailView.vue
caoxiaozhu d5a8f84703 refactor(web): 应用外壳/差旅详情/报销创建视图适配主题与多 task
- AppShellRouteView/useAppShell 适配主题皮肤与会话入口
- TravelRequestDetailView/travelRequestDetailSetup 差旅详情适配,travel-request-detail-view.css 调整
- TravelReimbursementCreateView/useTravelReimbursementCreateViewLifecycle 创建视图适配
- 更新 app-shell-financial-assistant-entry/travel-request-detail-risk-advice 测试
2026-06-26 22:42:29 +08:00

859 lines
43 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>
<section class="approval-page">
<div class="approval-detail">
<div class="detail-scroll">
<TravelRequestDetailHero :profile="profile" :hero-fact-items="heroFactItems" />
<TravelRequestProgressCard
:is-application-document="isApplicationDocument"
:progress-steps="progressSteps"
:current-progress-ring-motion="currentProgressRingMotion"
/>
<div class="detail-grid">
<section class="detail-left">
<TravelRequestRelatedApplicationCard
:is-application-document="isApplicationDocument"
:related-application-fact-items="relatedApplicationFactItems"
/>
<article class="detail-card panel">
<div class="detail-card-head">
<div>
<h3 class="detail-card-title-with-icon">
<i v-if="isApplicationDocument" class="mdi mdi-file-document-outline"></i>
<span>{{ isApplicationDocument ? '申请详情' : '费用明细' }}</span>
</h3>
<p>
{{
isApplicationDocument
? '展示本次申请的事实信息、职级规则测算和用户预估费用。'
: isTravelRequest
? '按出行时间逐笔核对票据与差旅规则。'
: '按业务发生时间逐笔核对票据、用途说明与 AI 识别结果。'
}}
</p>
</div>
<div v-if="!isApplicationDocument" class="detail-card-actions">
<button v-if="canOpenAiEntry" class="smart-entry-btn" type="button" :disabled="actionBusy" @click="triggerSmartEntryUpload">
<i class="mdi mdi-robot-outline"></i>
<span>{{ uploadingExpenseId ? '识别中' : '智能录入' }}</span>
</button>
</div>
</div>
<div v-if="isApplicationDocument" class="application-detail-facts">
<div
v-for="item in applicationDetailFactItems"
:key="item.key"
class="application-detail-fact"
:class="{
highlight: item.highlight,
emphasis: item.emphasis,
editable: canEditApplicationDetailItem(item),
editing: isApplicationDetailEditing(item)
}"
>
<span>{{ item.label }}</span>
<strong>
<template v-if="isApplicationDetailEditing(item)">
<ElDatePicker
v-if="resolveApplicationDetailEditorControl(item) === 'date'"
v-model="applicationDetailEditor.draftValue"
class="application-detail-editor-control"
type="date"
value-format="YYYY-MM-DD"
format="YYYY/MM/DD"
popper-class="detail-editor-date-popper"
:clearable="false"
:disabled="applicationDetailEditor.saving"
@click.stop
/>
<EnterpriseSelect
v-else-if="resolveApplicationDetailEditorControl(item) === 'select'"
v-model="applicationDetailEditor.draftValue"
class="application-detail-editor-control application-detail-editor-select"
:options="APPLICATION_TRANSPORT_MODE_OPTIONS"
clearable
:teleported="false"
:disabled="applicationDetailEditor.saving"
@click.stop
/>
<ElInput
v-else
v-model="applicationDetailEditor.draftValue"
class="application-detail-editor-control"
clearable
:disabled="applicationDetailEditor.saving"
@click.stop
@keydown.enter.stop.prevent="saveApplicationDetailEdit(item)"
@keydown.esc.stop.prevent="cancelApplicationDetailEditor"
/>
<button
class="application-detail-edit-confirm"
type="button"
title="保存"
aria-label="保存"
:disabled="applicationDetailEditor.saving"
@click.stop="saveApplicationDetailEdit(item)"
>
<i :class="applicationDetailEditor.saving ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-check'"></i>
</button>
<button
class="application-detail-edit-cancel"
type="button"
title="取消"
aria-label="取消"
:disabled="applicationDetailEditor.saving"
@click.stop="cancelApplicationDetailEditor"
>
<i class="mdi mdi-close"></i>
</button>
</template>
<template v-else>
<span class="application-detail-fact-value">{{ item.value }}</span>
<button
v-if="canEditApplicationDetailItem(item)"
class="application-detail-edit-btn"
type="button"
title="编辑"
aria-label="编辑"
:disabled="actionBusy"
@click.stop="openApplicationDetailEditor(item)"
>
<i class="mdi mdi-pencil-outline"></i>
</button>
</template>
</strong>
</div>
</div>
<TravelRequestBudgetAnalysis
v-if="showBudgetAnalysis"
:claim-id="request.claimId"
/>
<div v-if="showApplicationLeaderOpinion" class="application-leader-opinion">
<div class="application-leader-opinion-head">
<span><i class="mdi mdi-account-tie-outline"></i>领导意见</span>
<strong v-if="leaderApprovalReadonlyMeta">{{ leaderApprovalReadonlyMeta }}</strong>
</div>
<div
v-if="hasLeaderApprovalEvents"
class="application-leader-opinion-timeline"
:class="{ 'is-single': hasSingleLeaderApprovalEvent }"
aria-label="领导批复事件流"
>
<article
v-for="event in leaderApprovalEvents"
:key="event.id"
class="application-leader-opinion-event"
:class="event.tone"
>
<time class="application-leader-opinion-event-time" :datetime="event.time || undefined">
<strong>{{ event.dateLabel }}</strong>
<em v-if="event.timeLabel">{{ event.timeLabel }}</em>
</time>
<div class="application-leader-opinion-event-rail">
<span class="application-leader-opinion-event-status" :title="event.title">
<i :class="event.type === 'returned' ? 'mdi mdi-arrow-u-left-top' : 'mdi mdi-check-circle-outline'"></i>
</span>
</div>
<div class="application-leader-opinion-record">
<header class="application-leader-opinion-record-head">
<div class="application-leader-opinion-record-title">
<strong>{{ event.title }}</strong>
</div>
<dl class="application-leader-opinion-record-meta">
<div>
<dt>审批人</dt>
<dd>{{ event.operator }}</dd>
</div>
<div>
<dt>节点</dt>
<dd>{{ event.role }}</dd>
</div>
</dl>
</header>
<p>{{ event.opinion }}</p>
<footer v-if="event.returnCount" class="application-leader-opinion-event-foot">
<span v-if="event.returnCount"> {{ event.returnCount }} 次退回</span>
</footer>
</div>
</article>
</div>
</div>
<div v-if="!isApplicationDocument" class="detail-expense-table">
<div v-if="smartEntryRecognitionBusy" class="expense-recognition-banner">
<i class="mdi mdi-loading mdi-spin"></i>
<span>{{ smartEntryRecognitionText }}</span>
</div>
<div v-if="standardAdjustmentBusy" class="expense-recognition-banner standard-adjustment-banner">
<i class="mdi mdi-loading mdi-spin"></i>
<span>正在重新测算费用请稍候明细和合计会在后台完成后自动更新</span>
</div>
<div v-if="submitBusy" class="expense-recognition-banner submit-progress-banner">
<i class="mdi mdi-loading mdi-spin"></i>
<span>正在提交审批请稍候系统正在完成自动检测预算占用和审批流转</span>
</div>
<table>
<thead>
<tr>
<th class="col-filled-at">填写时间</th>
<th class="col-time">发生时间</th>
<th class="col-type">费用项目</th>
<th class="col-desc">说明</th>
<th class="col-amount">金额</th>
<th class="col-attachment">附件材料</th>
<th class="col-risk-note">异常说明</th>
<th v-if="isEditableRequest" class="col-action">操作</th>
</tr>
</thead>
<tbody>
<template v-for="item in expenseItems" :key="item.id">
<tr :class="{ 'system-generated-row': item.isSystemGenerated }">
<td class="expense-filled-at col-filled-at">
<strong>{{ item.filledAt }}</strong>
<span>条款填写时间</span>
</td>
<td :class="['expense-time col-time', { 'has-major-risk': hasExpenseRiskIndicator(item) }]">
<div class="expense-time-content">
<button
v-if="hasExpenseRiskIndicator(item)"
class="expense-risk-indicator"
type="button"
:title="resolveExpenseRiskIndicatorTitle(item)"
:aria-label="resolveExpenseRiskIndicatorTitle(item)"
@click="focusExpenseRisk(item)"
>
<i class="mdi mdi-alert"></i>
</button>
<span v-else class="expense-risk-indicator-placeholder" aria-hidden="true"></span>
<div class="expense-time-value">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor">
<ElDatePicker
v-model="expenseEditor.itemDate"
class="editor-date-picker editor-control"
type="date"
value-format="YYYY-MM-DD"
format="YYYY/MM/DD"
popper-class="detail-editor-date-popper"
:clearable="false"
/>
<span>{{ item.dayLabel }}</span>
</div>
</template>
<template v-else>
<strong>{{ item.time }}</strong>
<span>{{ item.dayLabel }}</span>
</template>
</div>
</div>
</td>
<td class="expense-type col-type">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor">
<EnterpriseSelect v-model="expenseEditor.itemType" class="editor-select" :options="expenseTypeOptions" />
<span>编辑费用项目</span>
</div>
</template>
<template v-else>
<strong>{{ item.name }}</strong>
<span>{{ item.category }}</span>
</template>
</td>
<td class="expense-desc col-desc">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor">
<ElInput
v-model="expenseEditor.itemReason"
class="editor-input-control editor-control"
clearable
:placeholder="resolveExpenseReasonPlaceholder(expenseEditor.itemType)"
/>
<span>{{ resolveExpenseReasonHelper(expenseEditor.itemType) }}</span>
</div>
</template>
<template v-else>
<strong>{{ item.desc }}</strong>
<span>{{ item.detail }}</span>
</template>
</td>
<td class="expense-amount col-amount">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor">
<ElInput
v-model="expenseEditor.itemAmount"
class="editor-amount-input editor-control"
type="number"
inputmode="decimal"
min="0"
step="0.01"
placeholder="输入金额"
>
<template #prefix></template>
</ElInput>
<span>自动格式化</span>
</div>
</template>
<template v-else>
<div v-if="item.hasStandardAdjustment" class="expense-adjusted-amount">
<span class="expense-original-amount">{{ item.originalAmountDisplay || item.amount }}</span>
<strong class="expense-reimbursable-amount">
<span class="expense-reimbursable-label">职级测算</span>
{{ item.reimbursableAmountDisplay }}
</strong>
<em v-if="item.employeeAbsorbedAmountDisplay">自担 {{ item.employeeAbsorbedAmountDisplay }}</em>
</div>
<strong v-else>{{ item.amount }}</strong>
<span v-if="item.tone !== 'ok'" :class="['over-tag', item.tone]">{{ item.status }}</span>
</template>
</td>
<td class="expense-attachment col-attachment">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor editor-stack">
<div v-if="uploadingExpenseId === item.id" class="system-attachment-note pending">
<i class="mdi mdi-loading mdi-spin"></i>
<span>识别中</span>
</div>
<div class="attachment-action-group">
<button
v-if="isEditableRequest && !item.invoiceId && !item.isSystemGenerated"
class="icon-action upload"
type="button"
title="上传单据"
aria-label="上传单据"
:disabled="actionBusy"
@click="triggerExpenseUpload(item)"
>
<i :class="uploadingExpenseId === item.id ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-file-upload-outline'"></i>
</button>
<button
v-if="canPreviewAttachment(item)"
class="icon-action preview"
type="button"
:title="resolveAttachmentPreviewTitle(item)"
:aria-label="resolveAttachmentPreviewTitle(item)"
@click="openAttachmentPreview(item)"
>
<i class="mdi mdi-eye-outline"></i>
</button>
<button
v-if="isEditableRequest && item.invoiceId && !item.isSystemGenerated"
class="icon-action danger"
type="button"
title="删除附件"
aria-label="删除附件"
:disabled="deletingAttachmentId === item.id"
@click="removeExpenseAttachment(item)"
>
<i :class="deletingAttachmentId === item.id ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-close-thick'"></i>
</button>
</div>
</div>
</template>
<template v-else>
<div v-if="uploadingExpenseId === item.id" class="system-attachment-note pending">
<i class="mdi mdi-loading mdi-spin"></i>
<span>识别中</span>
</div>
<div v-else-if="item.isSystemGenerated" class="system-attachment-note">
<i class="mdi mdi-calculator-variant-outline"></i>
<span>无需附件</span>
</div>
<div v-else class="attachment-action-group">
<button
v-if="isEditableRequest && !item.invoiceId && !item.isSystemGenerated"
class="icon-action upload"
type="button"
title="上传单据"
aria-label="上传单据"
:disabled="actionBusy"
@click="triggerExpenseUpload(item)"
>
<i :class="uploadingExpenseId === item.id ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-file-upload-outline'"></i>
</button>
<button
v-if="canPreviewAttachment(item)"
class="icon-action preview"
type="button"
:title="resolveAttachmentPreviewTitle(item)"
:aria-label="resolveAttachmentPreviewTitle(item)"
@click="openAttachmentPreview(item)"
>
<i class="mdi mdi-eye-outline"></i>
</button>
<button
v-if="isEditableRequest && item.invoiceId && !item.isSystemGenerated"
class="icon-action danger"
type="button"
title="删除附件"
aria-label="删除附件"
:disabled="deletingAttachmentId === item.id"
@click="removeExpenseAttachment(item)"
>
<i :class="deletingAttachmentId === item.id ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-close-thick'"></i>
</button>
</div>
</template>
</td>
<td class="expense-risk-note col-risk-note">
<template v-if="editingExpenseId === item.id">
<div class="cell-editor">
<ElInput
v-model="expenseEditor.itemNote"
class="risk-note-editor-input editor-control"
type="textarea"
:rows="1"
resize="none"
placeholder="如票据存在异常或风险,请补充原因"
/>
<span>非必填若有异常则说明</span>
</div>
</template>
<template v-else>
<strong v-if="item.itemNote">{{ item.itemNote }}</strong>
<span v-else-if="hasExpenseRiskOrAbnormal(item)" class="risk-note-missing">待补充异常说明</span>
<span v-else>无异常说明</span>
</template>
</td>
<td v-if="isEditableRequest" class="expense-action-cell col-action">
<div v-if="item.isSystemGenerated" class="system-row-lock">
<i class="mdi mdi-lock-outline"></i>
<span>系统计算</span>
</div>
<div v-else-if="editingExpenseId === item.id" class="row-action-group">
<button
class="inline-action primary"
type="button"
:disabled="actionBusy"
@click="saveExpenseEdit(item)"
>
{{ savingExpenseId === item.id ? '保存中' : '保存' }}
</button>
<button
class="inline-action danger"
type="button"
:disabled="actionBusy"
@click="removeExpenseItem(item)"
>
{{ deletingExpenseId === item.id ? '删除中' : '删除' }}
</button>
</div>
<div v-else class="row-action-group">
<button
class="inline-action"
type="button"
:disabled="actionBusy"
@click="startExpenseEdit(item)"
>
编辑
</button>
<button
class="inline-action danger"
type="button"
:disabled="actionBusy"
@click="removeExpenseItem(item)"
>
{{ deletingExpenseId === item.id ? '删除中' : '删除' }}
</button>
</div>
</td>
</tr>
</template>
<tr v-if="!expenseItems.length" class="empty-row">
<td :colspan="expenseTableColumnCount" class="empty-row-cell">
当前还没有费用明细请通过智能录入上传票据后由系统自动归集
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="expenseItems.length && !isApplicationDocument" class="expense-total-under-table">
<span>金额合计</span>
<strong>{{ expenseTotal }}</strong>
</div>
</article>
<article v-if="showAiAdvicePanel" class="detail-card panel validation-card">
<div class="validation-head">
<div>
<h3>{{ aiAdviceTitle }}</h3>
<p v-if="aiAdviceHint">{{ aiAdviceHint }}</p>
</div>
<span :class="['validation-pill', aiAdvice.tone]">{{ aiAdvice.badge }}</span>
</div>
<p class="validation-summary">{{ aiAdvice.summary }}</p>
<div v-if="aiAdvice.sections.length" class="validation-sections">
<section
v-for="section in aiAdvice.sections"
:key="section.kind"
:class="['validation-section', `validation-section--${section.kind}`]"
>
<h4 class="validation-section-title">{{ section.title }}</h4>
<ul v-if="section.kind !== 'risk'" class="validation-list">
<li v-for="item in section.items" :key="item">{{ item }}</li>
</ul>
<div v-else class="risk-advice-list">
<article
v-for="card in section.items"
:key="card.id"
:id="resolveRiskCardDomId(card)"
:data-risk-card-id="card.id"
:class="['risk-advice-card', card.tone, { 'is-highlighted': isHighlightedRiskCard(card) }]"
>
<div class="risk-advice-card-main">
<div class="risk-advice-card-head">
<span>{{ card.label }}</span>
<strong>{{ card.title }}</strong>
</div>
<p class="risk-advice-point">{{ card.risk }}</p>
</div>
<div class="risk-advice-compact-meta">
<span v-if="card.ruleBasis?.length">{{ card.ruleBasis[0] }}</span>
<em>{{ card.suggestion }}</em>
</div>
</article>
</div>
</section>
</div>
</article>
<StageRiskAdviceCard
v-if="showStageRiskAdvice"
:request="request"
:expense-items="expenseItems"
:ai-advice="aiAdvice"
:is-application-document="isApplicationDocument"
/>
</section>
</div>
</div>
<footer class="detail-actions">
<button class="back-action" type="button" @click="emit('backToRequests')">
<i class="mdi mdi-arrow-left"></i>
<span>{{ backLabel }}</span>
</button>
<div v-if="isEditableRequest" class="approval-action-group" aria-label="申请操作">
<button class="reject-action" type="button" :disabled="actionBusy" @click="handleDeleteRequest">
<i class="mdi mdi-trash-can-outline"></i>
{{ deleteBusy ? '删除中' : deleteActionLabel }}
</button>
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
<i :class="submitActionIcon"></i>
{{ submitActionLabel }}
</button>
</div>
<div v-else-if="canReturnRequest || canApproveRequest || canPayRequest || canDeleteRequest" class="approval-action-group" aria-label="单据管理操作">
<button
v-if="canReturnRequest"
class="return-action"
type="button"
:disabled="actionBusy"
@click="handleReturnRequest"
>
<i class="mdi mdi-undo"></i>
{{ returnBusy ? '退回中' : isApplicationDocument ? '退回申请' : '退回单据' }}
</button>
<button
v-if="canApproveRequest"
class="approve-action"
type="button"
:disabled="actionBusy"
@click="handleApproveRequest"
>
<i class="mdi mdi-check-circle-outline"></i>
{{ approveBusy ? approveBusyLabel : approveActionLabel }}
</button>
<button
v-if="canPayRequest"
class="approve-action"
type="button"
:disabled="actionBusy"
@click="handlePayRequest"
>
<i class="mdi mdi-cash-check"></i>
{{ payBusy ? '付款中' : '确认付款' }}
</button>
<button
v-if="canDeleteRequest"
class="reject-action"
type="button"
:disabled="actionBusy"
@click="handleDeleteRequest"
>
<i class="mdi mdi-trash-can-outline"></i>
{{ deleteBusy ? '删除中' : isApplicationDocument ? '删除申请' : '删除单据' }}
</button>
</div>
<p v-else class="detail-action-hint">
{{ isApplicationDocument ? '当前申请单已进入流程,详情页仅展示状态与申请信息。' : '当前单据已进入流程,详情页仅展示状态与费用明细。' }}
</p>
</footer>
</div>
<input
ref="expenseUploadInput"
class="expense-upload-input"
type="file"
accept="image/*,.pdf"
@change="handleExpenseFileChange"
/>
<input
ref="smartEntryUploadInput"
class="expense-upload-input"
type="file"
accept="image/*,.pdf"
multiple
@change="handleSmartEntryFileChange"
/>
<ConfirmDialog
:open="smartEntryUploadDialogOpen"
badge="智能录入"
title="上传报销附件"
description="请选择需要识别并归集到当前草稿的票据附件,确认前可以清除或重新选择。"
cancel-text="取消"
confirm-text="确认识别"
busy-text="识别中"
confirm-icon="mdi mdi-file-search-outline"
:busy="smartEntryUploadBusy"
@close="closeSmartEntryUploadDialog"
@confirm="confirmSmartEntryUpload"
>
<div class="smart-entry-upload-panel">
<button
class="smart-entry-upload-picker"
type="button"
:disabled="smartEntryUploadBusy"
@click="chooseSmartEntryFile"
>
<i class="mdi mdi-tray-arrow-up"></i>
<span>{{ smartEntrySelectedFileCount ? '重新选择附件' : '选择附件' }}</span>
</button>
<div class="smart-entry-upload-file">
<i :class="smartEntrySelectedFileCount ? 'mdi mdi-file-check-outline' : 'mdi mdi-file-outline'"></i>
<div>
<strong>{{ smartEntrySelectedFileSummary || '尚未选择附件' }}</strong>
<span>支持 JPGPNGPDF确认后系统会逐张识别并归集到草稿明细</span>
<ul v-if="smartEntrySelectedFileNames.length" class="smart-entry-upload-list">
<li v-for="fileName in smartEntrySelectedFileNames" :key="fileName">{{ fileName }}</li>
</ul>
</div>
<button
v-if="smartEntrySelectedFileCount"
class="smart-entry-upload-clear"
type="button"
:disabled="smartEntryUploadBusy"
@click="clearSmartEntryFile"
>
清除
</button>
</div>
</div>
</ConfirmDialog>
<Transition name="shared-confirm">
<div v-if="attachmentPreviewOpen" class="attachment-preview-mask" role="presentation" @click.self="closeAttachmentPreview">
<section class="attachment-preview-card" role="dialog" aria-modal="true" @click.stop>
<div class="attachment-preview-head">
<div>
<span class="attachment-preview-badge">附件预览</span>
<h4>{{ attachmentPreviewName || '当前附件' }}</h4>
</div>
<div class="attachment-preview-toolbar">
<button
v-if="canNavigateAttachmentPreview"
class="attachment-preview-nav"
type="button"
title="上一份附件"
:disabled="attachmentPreviewLoading"
@click="goToPreviousAttachmentPreview"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<span v-if="attachmentPreviewIndexLabel" class="attachment-preview-count">{{ attachmentPreviewIndexLabel }}</span>
<button
v-if="canNavigateAttachmentPreview"
class="attachment-preview-nav"
type="button"
title="下一份附件"
:disabled="attachmentPreviewLoading"
@click="goToNextAttachmentPreview"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<button class="attachment-preview-close" type="button" @click="closeAttachmentPreview">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="attachment-preview-body">
<div class="attachment-source-pane">
<div v-if="attachmentPreviewLoading" class="attachment-preview-state">
<i class="mdi mdi-loading mdi-spin"></i>
<span>正在加载附件预览</span>
</div>
<div v-else-if="attachmentPreviewError" class="attachment-preview-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<span>{{ attachmentPreviewError }}</span>
</div>
<img v-else-if="attachmentPreviewUrl && attachmentPreviewMediaType.startsWith('image/')" :src="attachmentPreviewUrl" :alt="attachmentPreviewName || '附件图片'" class="attachment-preview-image" />
<iframe v-else-if="attachmentPreviewUrl && attachmentPreviewMediaType === 'application/pdf'" :src="attachmentPreviewUrl" class="attachment-preview-frame" title="附件预览"></iframe>
<div v-else class="attachment-preview-state">
<i class="mdi mdi-file-outline"></i>
<span>当前附件暂不支持直接预览</span>
</div>
</div>
<aside class="attachment-insight-pane">
<div class="attachment-insight-head">
<span>识别信息</span>
<strong>{{ currentAttachmentPreviewInsight?.documentTypeLabel || '待识别' }}</strong>
</div>
<div v-if="currentAttachmentPreviewInsight" class="attachment-insight-content">
<div class="attachment-insight-pills">
<span :class="['attachment-recognition-pill', currentAttachmentPreviewInsight.requirementTone]">{{ currentAttachmentPreviewInsight.requirementLabel }}</span>
</div>
<p v-if="currentAttachmentPreviewInsight.message" class="attachment-recognition-message">{{ currentAttachmentPreviewInsight.message }}</p>
<div v-if="currentAttachmentPreviewInsight.fields.length" class="attachment-insight-section">
<span>字段结果</span>
<ul><li v-for="field in currentAttachmentPreviewInsight.fields" :key="field">{{ field }}</li></ul>
</div>
<div v-if="currentAttachmentPreviewInsight.ruleBasis.length" class="attachment-insight-section">
<span>规则依据</span>
<ul><li v-for="basis in currentAttachmentPreviewInsight.ruleBasis" :key="basis">{{ basis }}</li></ul>
</div>
<div v-if="currentAttachmentPreviewRiskCards.length" class="attachment-insight-section risk">
<span>风险点</span>
<article v-for="card in currentAttachmentPreviewRiskCards" :key="card.id" :class="['attachment-risk-card', card.tone]">
<strong>{{ card.risk }}</strong>
<p>{{ card.suggestion }}</p>
</article>
</div>
</div>
<div v-else class="attachment-preview-state compact">
<i class="mdi mdi-file-search-outline"></i>
<span>预览打开后会在这里展示票据字段规则依据和风险提示</span>
</div>
</aside>
</div>
</section>
</div>
</Transition>
<ConfirmDialog
:open="submitConfirmDialogOpen"
badge="提交确认"
badge-tone="warning"
size="review"
:title="`确认提交 ${request.id} 吗?`"
:description="submitConfirmDescription"
cancel-text="返回核对"
:secondary-text="submitConfirmSecondaryText"
secondary-tone="warning"
secondary-icon="mdi mdi-calculator-variant-outline"
:confirm-text="submitConfirmText"
busy-text="提交中..."
confirm-tone="primary"
confirm-icon="mdi mdi-send-circle-outline"
:busy="submitBusy"
@close="closeSubmitConfirmDialog"
@secondary="confirmStandardAdjustment"
@confirm="confirmSubmitRequest"
>
<div class="submit-confirm-summary" aria-label="提交前核对摘要">
<div class="submit-confirm-row"><span>单据编号</span><strong>{{ request.documentNo || request.id }}</strong></div>
<div class="submit-confirm-row"><span>{{ isApplicationDocument ? '申请类型' : '报销类型' }}</span><strong>{{ request.typeLabel }}</strong></div>
<div class="submit-confirm-row"><span>{{ isApplicationDocument ? '预计金额' : '报销金额' }}</span><strong>{{ submitConfirmAmountDisplay }}</strong></div>
<div v-if="!isApplicationDocument" class="submit-confirm-row"><span>费用明细</span><strong>{{ expenseItems.length }} / {{ uploadedExpenseCount }} 张单据</strong></div>
</div>
</ConfirmDialog>
<ConfirmDialog
:open="riskOverrideDialogOpen"
badge="异常说明"
:badge-tone="riskOverrideBadgeTone"
size="review"
:title="riskOverrideDialogTitle"
:description="riskOverrideDialogDescription"
:cancel-text="riskOverrideCancelText"
:confirm-text="riskOverrideConfirmText"
busy-text="处理中..."
:confirm-tone="riskOverrideConfirmTone"
:confirm-icon="riskOverrideConfirmIcon"
:busy="riskOverrideBusy"
@close="closeRiskOverrideDialog"
@confirm="confirmRiskOverrideDialog"
>
<div v-if="currentSubmitRiskWarning" class="risk-override-panel" aria-label="异常说明">
<div class="risk-override-card-shell">
<button
type="button"
class="risk-override-side-nav risk-override-side-nav--previous"
:disabled="submitRiskReviewWarnings.length <= 1 || riskOverrideBusy"
aria-label="上一条风险"
@click="goToPreviousSubmitRisk"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<article :class="['risk-override-card', currentSubmitRiskWarning.tone]">
<div class="risk-override-card-head">
<span>{{ currentSubmitRiskWarning.label }}</span>
<strong>{{ currentSubmitRiskWarning.title }}</strong>
</div>
<p>{{ currentSubmitRiskWarning.risk }}</p>
<div v-if="currentSubmitRiskWarningNotes.length" class="risk-override-notes">
<span>已填写异常说明</span>
<strong v-for="note in currentSubmitRiskWarningNotes" :key="note">{{ note }}</strong>
</div>
</article>
<button
type="button"
class="risk-override-side-nav risk-override-side-nav--next"
:disabled="submitRiskReviewWarnings.length <= 1 || riskOverrideBusy"
aria-label="下一条风险"
@click="goToNextSubmitRisk"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<div class="risk-override-index">{{ riskOverrideIndexLabel }}</div>
<div class="risk-override-guidance">
<strong>{{ riskOverrideGuidanceTitle }}</strong>
<span>{{ riskOverrideGuidanceText }}</span>
</div>
</div>
</ConfirmDialog>
<TravelRequestDeleteDialog :open="deleteDialogOpen" :badge="deleteActionLabel" :title="deleteDialogTitle" :description="deleteDialogDescription" :busy="deleteBusy" @close="closeDeleteDialog" @confirm="confirmDeleteRequest" />
<TravelRequestApprovalDialog
:open="approveConfirmDialogOpen"
:badge="approvalConfirmBadge"
:title="approveConfirmTitle"
:description="approvalConfirmDescription"
:confirm-text="approveConfirmText"
:busy-text="approveBusyText"
:busy="approveBusy"
:opinion-title="approvalOpinionTitle"
v-model:opinion="leaderOpinion"
:opinion-placeholder="approvalOpinionPlaceholder"
:opinion-hint="approvalOpinionHint"
:opinion-required="requiresApprovalOpinion"
:risk-confirm-required="approvalRiskConfirmRequired"
v-model:risk-confirmed="approvalRiskConfirmed"
:risk-confirm-items="approvalRiskConfirmItems"
@close="closeApproveConfirmDialog"
@confirm="confirmApproveRequest"
/>
<ConfirmDialog
:open="payConfirmDialogOpen"
badge="付款确认"
badge-tone="warning"
:title="`确认 ${request.id} 已付款吗?`"
description="确认后该报销单会进入已付款状态,并汇总到归档视图。"
cancel-text="返回核对"
confirm-text="确认已付款"
busy-text="付款中..."
confirm-tone="primary"
confirm-icon="mdi mdi-cash-check"
:busy="payBusy"
@close="closePayConfirmDialog"
@confirm="confirmPayRequest"
/>
<TravelRequestReturnDialog :open="returnDialogOpen" :title="`确认退回 ${request.id} 吗?`" :description="returnDialogDescription" :busy="returnBusy" :application="isApplicationDocument" @close="closeReturnDialog" @confirm="confirmReturnRequest" />
</section>
</template>
<script src="./scripts/TravelRequestDetailView.js"></script>
<style scoped src="../assets/styles/views/travel-request-detail-view.css"></style>
<style scoped src="../assets/styles/views/travel-request-detail-view-part2.css"></style>
<style scoped src="../assets/styles/views/travel-request-detail-responsive.css"></style>
<style src="../assets/styles/views/travel-request-detail-date-popper.css"></style>