- AppShellRouteView/useAppShell 适配主题皮肤与会话入口 - TravelRequestDetailView/travelRequestDetailSetup 差旅详情适配,travel-request-detail-view.css 调整 - TravelReimbursementCreateView/useTravelReimbursementCreateViewLifecycle 创建视图适配 - 更新 app-shell-financial-assistant-entry/travel-request-detail-risk-advice 测试
859 lines
43 KiB
Vue
859 lines
43 KiB
Vue
<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>支持 JPG、PNG、PDF;确认后系统会逐张识别并归集到草稿明细。</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>
|