- 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
286 lines
13 KiB
Vue
286 lines
13 KiB
Vue
<template>
|
||
<section class="approval-page">
|
||
<div class="approval-detail">
|
||
<div class="detail-scroll">
|
||
<article class="detail-hero panel">
|
||
<div class="applicant-card">
|
||
<div class="portrait">{{ profile.avatar }}</div>
|
||
<div>
|
||
<h2>{{ profile.name }} <span>{{ request.typeLabel }}</span></h2>
|
||
<p>{{ profile.department }} <strong>申请时间 {{ request.applyTime }}</strong></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-for="stat in heroStats" :key="stat.label" class="hero-stat">
|
||
<span>{{ stat.label }}</span>
|
||
<strong v-if="stat.kind === 'text'">{{ stat.value }}</strong>
|
||
<b v-else :class="[stat.className, stat.tone]">{{ stat.value }}</b>
|
||
</div>
|
||
|
||
<div class="hero-summary-panel">
|
||
<div v-for="item in heroSummaryItems" :key="item.label" class="hero-summary-item">
|
||
<div class="hero-summary-label">
|
||
<span class="hero-summary-icon"><i :class="item.icon"></i></span>
|
||
<span>{{ item.label }}</span>
|
||
</div>
|
||
<strong>{{ item.value }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="progress-block">
|
||
<div class="progress-head">
|
||
<h3>{{ isTravelRequest ? '差旅进度' : '报销进度' }}</h3>
|
||
</div>
|
||
<div class="progress-line" :style="{ '--progress-columns': progressSteps.length }">
|
||
<div
|
||
v-for="step in progressSteps"
|
||
:key="step.label"
|
||
class="progress-step"
|
||
:class="{ active: step.active, current: step.current }"
|
||
>
|
||
<span>
|
||
<i
|
||
v-if="step.current"
|
||
v-motion
|
||
class="current-progress-ring"
|
||
:initial="currentProgressRingMotion.initial"
|
||
:enter="currentProgressRingMotion.enter"
|
||
aria-hidden="true"
|
||
></i>
|
||
<i v-if="step.done" class="mdi mdi-check"></i>
|
||
<template v-else>{{ step.index }}</template>
|
||
</span>
|
||
<strong>{{ step.label }}</strong>
|
||
<small>{{ step.time }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<div class="detail-grid">
|
||
<section class="detail-left">
|
||
<article class="detail-card panel">
|
||
<div class="detail-card-head">
|
||
<div>
|
||
<h3>费用明细</h3>
|
||
<p>
|
||
{{ isTravelRequest ? '按出行时间逐笔核对票据与差旅规则。' : '按业务发生时间逐笔核对票据、用途说明与系统校验。' }}
|
||
</p>
|
||
</div>
|
||
<button class="smart-entry-btn" type="button" @click="openAiEntry">
|
||
<i class="mdi mdi-robot-outline"></i>
|
||
<span>智能录入</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="detail-expense-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>费用项目</th>
|
||
<th>说明</th>
|
||
<th>金额</th>
|
||
<th>附件材料</th>
|
||
<th>系统校验</th>
|
||
<th v-if="isDraftRequest">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-for="item in expenseItems" :key="item.id">
|
||
<tr>
|
||
<td class="expense-time">
|
||
<template v-if="editingExpenseId === item.id">
|
||
<div class="cell-editor">
|
||
<input v-model="expenseEditor.itemDate" class="editor-input" type="date" />
|
||
<span>{{ item.dayLabel }}</span>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<strong>{{ item.time }}</strong>
|
||
<span>{{ item.dayLabel }}</span>
|
||
</template>
|
||
</td>
|
||
<td class="expense-type">
|
||
<template v-if="editingExpenseId === item.id">
|
||
<div class="cell-editor">
|
||
<select v-model="expenseEditor.itemType" class="editor-select">
|
||
<option v-for="option in expenseTypeOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
<span>编辑费用项目</span>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<strong>{{ item.name }}</strong>
|
||
<span>{{ item.category }}</span>
|
||
</template>
|
||
</td>
|
||
<td class="expense-desc">
|
||
<template v-if="editingExpenseId === item.id">
|
||
<div class="cell-editor editor-stack">
|
||
<input v-model="expenseEditor.itemReason" class="editor-input" type="text" placeholder="输入费用说明" />
|
||
<input v-model="expenseEditor.itemLocation" class="editor-input" type="text" placeholder="输入业务地点" />
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<strong>{{ item.desc }}</strong>
|
||
<span>{{ item.detail }}</span>
|
||
</template>
|
||
</td>
|
||
<td class="expense-amount">
|
||
<template v-if="editingExpenseId === item.id">
|
||
<div class="cell-editor">
|
||
<input v-model="expenseEditor.itemAmount" class="editor-input" type="number" min="0" step="0.01" placeholder="输入金额" />
|
||
<span>保存后自动格式化为人民币</span>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<strong>{{ item.amount }}</strong>
|
||
<span v-if="item.tone !== 'ok'" :class="['over-tag', item.tone]">{{ item.status }}</span>
|
||
</template>
|
||
</td>
|
||
<td class="expense-attachment">
|
||
<template v-if="editingExpenseId === item.id">
|
||
<div class="cell-editor editor-stack">
|
||
<input v-model="expenseEditor.invoiceId" class="editor-input" type="text" placeholder="输入票据标识或附件名称" />
|
||
<span>示例:invoice-2026-0513.jpg</span>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="expense-attachment-main">
|
||
<span :class="['attachment-pill', item.attachmentTone]">{{ item.attachmentStatus }}</span>
|
||
<button
|
||
v-if="item.attachments.length"
|
||
class="inline-action"
|
||
type="button"
|
||
@click="toggleExpenseAttachments(item.id)"
|
||
>
|
||
{{ expandedExpenseId === item.id ? '收起附件' : '查看附件' }}
|
||
</button>
|
||
</div>
|
||
<span class="attachment-hint">{{ item.attachmentHint }}</span>
|
||
</template>
|
||
</td>
|
||
<td class="expense-risk">
|
||
<template v-if="showExpenseRisk(item)">
|
||
<span :class="['risk-inline-tag', resolveExpenseIssues(item).length ? 'medium' : item.riskTone]">
|
||
{{ resolveExpenseIssues(item).length ? '待补充' : item.riskLabel }}
|
||
</span>
|
||
<p>{{ resolveExpenseIssues(item).length ? resolveExpenseIssues(item).join(',') : item.riskText }}</p>
|
||
</template>
|
||
</td>
|
||
<td v-if="isDraftRequest" class="expense-action-cell">
|
||
<div v-if="editingExpenseId === item.id" class="row-action-group">
|
||
<button
|
||
class="inline-action primary"
|
||
type="button"
|
||
:disabled="savingExpenseId === item.id || submitBusy || deleteBusy"
|
||
@click="saveExpenseEdit(item)"
|
||
>
|
||
{{ savingExpenseId === item.id ? '保存中' : '保存' }}
|
||
</button>
|
||
<button
|
||
class="inline-action"
|
||
type="button"
|
||
:disabled="savingExpenseId === item.id || submitBusy || deleteBusy"
|
||
@click="cancelExpenseEdit"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
<button
|
||
v-else
|
||
class="inline-action"
|
||
type="button"
|
||
:disabled="actionBusy"
|
||
@click="startExpenseEdit(item)"
|
||
>
|
||
编辑
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="expandedExpenseId === item.id" class="expense-expand-row">
|
||
<td :colspan="isDraftRequest ? 7 : 6">
|
||
<div class="expense-files">
|
||
<span v-for="file in item.attachments" :key="file" class="expense-file-chip">
|
||
<i class="mdi mdi-paperclip"></i>
|
||
{{ file }}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
<tr class="total-row">
|
||
<td :colspan="isDraftRequest ? 4 : 3">合计</td>
|
||
<td>{{ expenseTotal }}</td>
|
||
<td>{{ uploadedExpenseCount }} 项已关联票据</td>
|
||
<td>{{ expenseSummaryText }}</td>
|
||
<td v-if="isDraftRequest">-</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</article>
|
||
|
||
<article v-if="isDraftRequest" class="detail-card panel validation-card">
|
||
<div class="validation-head">
|
||
<h3>提交校验</h3>
|
||
<span :class="['validation-pill', validationTone]">{{ canSubmit ? '可提交' : '待完善' }}</span>
|
||
</div>
|
||
<p class="validation-summary">{{ validationSummary }}</p>
|
||
<ul v-if="draftBlockingIssues.length" class="validation-list">
|
||
<li v-for="issue in draftBlockingIssues" :key="issue">{{ issue }}</li>
|
||
</ul>
|
||
</article>
|
||
|
||
<article class="detail-card panel">
|
||
<h3>申请说明</h3>
|
||
<div class="detail-note">{{ detailNote }}</div>
|
||
</article>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="detail-actions">
|
||
<button class="back-action" type="button" @click="emit('backToRequests')">
|
||
<i class="mdi mdi-arrow-left"></i>
|
||
<span>返回报销列表</span>
|
||
</button>
|
||
<div v-if="isDraftRequest" class="approval-action-group" aria-label="申请操作">
|
||
<button class="reject-action" type="button" :disabled="actionBusy" @click="handleDeleteDraft">
|
||
<i class="mdi mdi-trash-can-outline"></i>
|
||
{{ deleteBusy ? '删除中' : '删除草稿' }}
|
||
</button>
|
||
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
|
||
<i class="mdi mdi-send-circle-outline"></i>
|
||
{{ submitBusy ? '提交中' : '提交审批' }}
|
||
</button>
|
||
</div>
|
||
<p v-else class="detail-action-hint">当前单据已进入流程,详情页仅展示状态与费用明细。</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<ConfirmDialog
|
||
:open="deleteDialogOpen"
|
||
badge="删除草稿"
|
||
badge-tone="danger"
|
||
:title="`确认删除草稿 ${request.id} 吗?`"
|
||
description="删除后该草稿及其当前费用明细将不可恢复,请确认本次操作。"
|
||
cancel-text="取消"
|
||
confirm-text="确认删除"
|
||
busy-text="删除中..."
|
||
confirm-tone="danger"
|
||
confirm-icon="mdi mdi-trash-can-outline"
|
||
:busy="deleteBusy"
|
||
@close="closeDeleteDialog"
|
||
@confirm="confirmDeleteDraft"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<script src="./scripts/TravelRequestDetailView.js"></script>
|
||
|
||
<style scoped src="../assets/styles/views/travel-request-detail-view.css"></style>
|