Files
shanxia2.0/miniprogram/pages/bx/reimburse/detail.vue
T

394 lines
12 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>
<view class="bx-page bx-page--form">
<view class="topbar" :style="safeAreaStyle">
<view class="topbar-left">
<view class="topbar-back" @click="goBack">
<text class="topbar-back-icon">‹</text>
</view>
</view>
<text class="topbar-title">报销详情</text>
<view class="topbar-right" />
</view>
<template v-if="detail">
<view class="advance-ticket-shell">
<view class="advance-ticket-stack">
<view class="advance-ticket-top">
<text class="advance-ticket-code">{{ detail.applyCode || '—' }}</text>
<text
class="advance-ticket-status"
:class="ticketHeaderStatusToneClass(detail.status)"
>
{{ statusLabel(detail.status) }}
</text>
</view>
<view class="advance-ticket-tear">
<view class="advance-ticket-tear__line" />
</view>
<view class="advance-ticket-bottom">
<view class="advance-ticket-reason">{{ detail.title || '—' }}</view>
<view class="advance-ticket-amount">{{ formatAmount(detail.amount) }}</view>
</view>
</view>
</view>
<view class="detail-tabs">
<view
class="detail-tab"
:class="{ active: detailTab === 0 }"
@click="detailTab = 0"
>
基础信息
</view>
<view
class="detail-tab"
:class="{ active: detailTab === 1 }"
@click="detailTab = 1"
>
审批信息
</view>
</view>
<view v-if="detailTab === 0" class="page-padded advance-detail-stack">
<view class="detail-info-card">
<view class="adv-field">
<text class="adv-field__label">申请单号</text>
<text class="adv-field__value">{{ detail.applyCode || '—' }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">报销标题</text>
<text class="adv-field__value">{{ detail.title || '—' }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">申请人</text>
<text class="adv-field__value">{{ detail.applicantName || '—' }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">冲抵金额</text>
<text class="adv-field__value">
{{ moneyOrDash(detail.offsetAmount ?? detail.offset_amount) }}
</text>
</view>
<view class="adv-field">
<text class="adv-field__label">实付金额</text>
<text class="adv-field__value">
{{
moneyOrDash(
detail.payAmount ?? detail.actualAmount ?? detail.actual_amount
)
}}
</text>
</view>
<view class="adv-field">
<text class="adv-field__label">当前节点</text>
<text class="adv-field__value">{{ detail.currentNode || '—' }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">申请时间</text>
<text class="adv-field__value">{{ formatApplyDateTime(detail) }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">报销说明</text>
<text class="adv-field__value adv-field__value--multiline">
{{ detail.remark || '—' }}
</text>
</view>
</view>
<view
v-if="detail.details && detail.details.length"
class="detail-info-card"
>
<view class="detail-info-card__title">费用项信息</view>
<view
v-for="(d, idx) in detail.details"
:key="idx"
class="reimburse-detail-group"
>
<view v-if="detail.details.length > 1" class="reimburse-detail-head">
明细 {{ idx + 1 }}
</view>
<view class="adv-field">
<text class="adv-field__label">费用项目</text>
<text class="adv-field__value">{{ resolveExpenseDisplay(d, flatList) }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">金额</text>
<text class="adv-field__value adv-field__value--accent">
¥ {{ formatAmount(d.amount) }}
</text>
</view>
<view v-if="d.remark" class="adv-field">
<text class="adv-field__label">报销事由</text>
<text class="adv-field__value adv-field__value--multiline">{{ d.remark }}</text>
</view>
<view class="adv-field">
<text class="adv-field__label">发生时间</text>
<text class="adv-field__value">{{ d.occurDate || '—' }}</text>
</view>
</view>
<view class="adv-field reimburse-detail-total-row">
<text class="adv-field__label">合计</text>
<text class="adv-field__value adv-field__value--accent">
¥ {{ totalAmount }}
</text>
</view>
</view>
<view class="detail-info-card">
<view class="detail-info-card__title">申请附件</view>
<view v-if="attachments.length" class="att-grid">
<view
v-for="file in attachments"
:key="file.id"
class="att-img"
@click="previewAtt(file)"
>
<image
v-if="file.previewUrl"
:src="file.previewUrl"
mode="aspectFill"
/>
<view v-else class="detail-section-empty" style="border: none; padding: 8px">
文件
</view>
</view>
</view>
<view v-else class="detail-section-empty">暂无申请附件</view>
</view>
</view>
<view v-else class="page-padded advance-detail-stack">
<view class="detail-info-card" style="padding: 14px 16px">
<template v-if="approvals.length">
<view v-for="(a, i) in approvals" :key="a.id ?? i" class="flow-step">
<view class="flow-avatar" :class="avatarClass(a)">
{{ (a.approverName || a.nodeName || a.nodeLabel || '审')[0] }}
</view>
<view class="flow-info">
<view class="flow-name">
{{ a.nodeName || a.nodeLabel || a.approverName || '—' }}
</view>
<view v-if="a.approverName" class="flow-role">{{ a.approverName }}</view>
<view v-if="a.approvalTime" class="flow-time">
{{ formatDateTime(a.approvalTime) }}
</view>
<view v-if="a.approvalOpinion" class="adv-flow-opinion">
{{ a.approvalOpinion }}
</view>
</view>
</view>
</template>
<view v-else class="detail-section-empty">暂无审批记录</view>
</view>
</view>
</template>
<view v-else class="page-body-loading">加载中...</view>
<view v-if="showActions" class="bottom-action-bar">
<template v-if="detail?.status === 'draft'">
<button
class="btn btn-primary"
style="flex: 1"
:disabled="actionLoading.submit"
@click="handleSubmit"
>
提交审批
</button>
<button
class="btn btn-danger"
style="flex: 1"
:disabled="actionLoading.delete"
@click="handleDelete"
>
删除
</button>
</template>
<template v-if="detail?.status === 'rejected'">
<button class="btn btn-primary" style="flex: 1" @click="handleResubmit">
重新提交
</button>
</template>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { PAGE_PATHS } from '@/config/api'
import { reimburseApi, attachmentApi, expenseItemApi } from '@/api/bx'
import {
import { getSafeAreaStyle } from '@/utils/safeArea'
const safeAreaStyle = getSafeAreaStyle()
statusLabel,
ticketHeaderStatusToneClass,
formatAmount,
formatDateTime,
formatApplyDateTime,
normalizeExpenseItem,
flattenExpenseTree,
resolveExpenseDisplay
} from '@/utils/bxFormat'
const detailId = ref('')
const detail = ref(null)
const detailTab = ref(0)
const approvals = ref([])
const attachments = ref([])
const flatList = ref([])
const actionLoading = reactive({ submit: false, delete: false })
const showActions = computed(() => {
if (!detail.value) return false
const status = detail.value.status
return status === 'draft' || status === 'rejected'
})
const totalAmount = computed(() => {
const rows = detail.value?.details || []
const sum = rows.reduce((s, d) => s + Number(d.amount || 0), 0)
return formatAmount(sum)
})
function moneyOrDash(raw) {
if (raw == null || raw === '') return '—'
const n = typeof raw === 'string' ? parseFloat(raw) : Number(raw)
if (Number.isNaN(n) || n === 0) return '—'
return '¥ ' + formatAmount(n)
}
function goBack() {
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.bxReimburseApply }) })
}
function avatarClass(a) {
const s = a.approvalStatus || a.status
if (s === 'approved') return 'av-done'
if (s === 'pending') return 'av-current'
if (s === 'rejected') return 'av-reject'
return 'av-pending'
}
function previewAtt(file) {
if (!file.previewUrl) return
uni.previewImage({ urls: [file.previewUrl], current: file.previewUrl })
}
async function loadExpenseFlat() {
try {
const res = await expenseItemApi.tree()
flatList.value = flattenExpenseTree((res.data || []).map(normalizeExpenseItem))
} catch (e) {
flatList.value = []
}
}
async function loadDetail() {
const id = detailId.value
if (!id) return
try {
const res = await reimburseApi.getById(id)
detail.value = res.data
} catch (e) {
uni.showToast({ title: '加载详情失败', icon: 'none' })
return
}
try {
const histRes = await reimburseApi.getApprovalHistory(id)
const hist = histRes.data || []
if (Array.isArray(hist) && hist.length) {
approvals.value = hist
} else {
const apprRes = await reimburseApi.getApprovals(id)
const nodes = apprRes.data || []
approvals.value = nodes.slice().sort((a, b) => (a.nodeOrder ?? 0) - (b.nodeOrder ?? 0))
}
} catch (e) {
try {
const apprRes = await reimburseApi.getApprovals(id)
const nodes = apprRes.data || []
approvals.value = nodes.slice().sort((a, b) => (a.nodeOrder ?? 0) - (b.nodeOrder ?? 0))
} catch (err) {
approvals.value = []
}
}
try {
const attRes = await attachmentApi.listByBiz('reimburse_apply', id)
const list = (attRes.data || []).map((a) => ({ ...a, previewUrl: '' }))
await Promise.all(
list
.filter((a) => (a.fileType || '').startsWith('image/'))
.map(async (a) => {
try {
const pRes = await attachmentApi.preview(a.id)
a.previewUrl = pRes.data?.url || ''
} catch (e) {
// skip
}
})
)
attachments.value = list
} catch (e) {
attachments.value = []
}
}
async function handleSubmit() {
if (!detail.value) return
actionLoading.submit = true
try {
await reimburseApi.submit(detail.value.id)
uni.showToast({ title: '提交成功', icon: 'success' })
await loadDetail()
} catch (e) {
// toasted
} finally {
actionLoading.submit = false
}
}
function handleDelete() {
if (!detail.value) return
uni.showModal({
title: '确认删除',
content: '确定要删除该报销申请吗?删除后不可恢复。',
confirmText: '确定删除',
confirmColor: '#ef4444',
success: async (res) => {
if (!res.confirm) return
actionLoading.delete = true
try {
await reimburseApi.delete(detail.value.id)
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => goBack(), 500)
} catch (e) {
// toasted
} finally {
actionLoading.delete = false
}
}
})
}
function handleResubmit() {
if (!detail.value) return
uni.navigateTo({
url: `${PAGE_PATHS.bxReimburseApplyForm}?id=${detail.value.id}`
})
}
onLoad((query) => {
detailId.value = query?.id ? String(query.id) : ''
loadExpenseFlat()
loadDetail()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
</style>