402 lines
13 KiB
Vue
402 lines
13 KiB
Vue
<template>
|
|
<view class="bx-page bx-page--form">
|
|
<view class="topbar" :style="safeAreaStyle">
|
|
<view class="topbar-left">
|
|
<text class="topbar-back" @click="goBack">←</text>
|
|
<text class="topbar-title">备用金详情</text>
|
|
</view>
|
|
<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.reason || '—' }}</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
|
|
class="detail-tab"
|
|
:class="{ active: detailTab === 2 }"
|
|
@click="detailTab = 2"
|
|
>
|
|
发放信息
|
|
</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 adv-field__value--multiline">{{ detail.reason || '—' }}</text>
|
|
</view>
|
|
<view class="adv-field">
|
|
<text class="adv-field__label">实际发放金额</text>
|
|
<text
|
|
class="adv-field__value"
|
|
:class="{ 'adv-field__value--accent': detail.actualAmount != null }"
|
|
>
|
|
{{
|
|
detail.actualAmount != null
|
|
? '¥ ' + formatAmount(detail.actualAmount)
|
|
: '—'
|
|
}}
|
|
</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">{{ detail.currentNode || '—' }}</text>
|
|
</view>
|
|
<view class="adv-field">
|
|
<text class="adv-field__label">申请时间</text>
|
|
<text class="adv-field__value">{{ formatApplyDateTime(detail) }}</text>
|
|
</view>
|
|
<view v-if="detail.remark" class="adv-field">
|
|
<text class="adv-field__label">备注</text>
|
|
<text class="adv-field__value adv-field__value--multiline">{{ detail.remark }}</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-if="detailTab === 1" 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>
|
|
|
|
<view v-else class="page-padded advance-detail-stack">
|
|
<template v-if="isAdvanceIssued">
|
|
<view class="detail-info-card">
|
|
<view class="adv-field">
|
|
<text class="adv-field__label">发放时间</text>
|
|
<text class="adv-field__value">{{ formatDateTime(detail.issueTime) || '—' }}</text>
|
|
</view>
|
|
<view class="adv-field">
|
|
<text class="adv-field__label">发放操作人</text>
|
|
<text class="adv-field__value">{{ detail.issueUserName || '—' }}</text>
|
|
</view>
|
|
<view v-if="detail.issueRemark" class="adv-field">
|
|
<text class="adv-field__label">发放备注</text>
|
|
<text class="adv-field__value adv-field__value--multiline">{{ detail.issueRemark }}</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
<view v-else class="detail-info-card" style="padding: 28px 16px">
|
|
<view 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" @click="handleEdit">编辑</button>
|
|
<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="handleEdit">编辑</button>
|
|
<button
|
|
class="btn btn-primary"
|
|
style="flex: 1"
|
|
:disabled="actionLoading.submit"
|
|
@click="handleSubmit"
|
|
>
|
|
提交审批
|
|
</button>
|
|
</template>
|
|
<template v-if="canWithdraw">
|
|
<button
|
|
class="btn btn-ghost"
|
|
style="flex: 1"
|
|
:disabled="actionLoading.withdraw"
|
|
@click="handleWithdraw"
|
|
>
|
|
撤回
|
|
</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 { advanceApi, attachmentApi } from '@/api/bx'
|
|
import {
|
|
statusLabel,
|
|
ticketHeaderStatusToneClass,
|
|
formatAmount,
|
|
formatDateTime,
|
|
formatApplyDateTime
|
|
} from '@/utils/bxFormat'
|
|
import { getSafeAreaStyle } from '@/utils/safeArea'
|
|
import { useUserStore } from '@/stores/user'
|
|
|
|
const safeAreaStyle = getSafeAreaStyle()
|
|
const userStore = useUserStore()
|
|
|
|
const detailId = ref('')
|
|
const detail = ref(null)
|
|
const detailTab = ref(0)
|
|
const approvals = ref([])
|
|
const attachments = ref([])
|
|
const actionLoading = reactive({ submit: false, delete: false, withdraw: false })
|
|
|
|
function currentUserId() {
|
|
const u = userStore.userInfo || {}
|
|
return String(u.id || u.userId || '')
|
|
}
|
|
|
|
const canWithdraw = computed(() => {
|
|
const d = detail.value
|
|
if (!d) return false
|
|
return d.status === 'pending' && String(d.applicantId || '') === currentUserId()
|
|
})
|
|
|
|
const isAdvanceIssued = computed(() => {
|
|
const d = detail.value
|
|
return !!(d && (d.issueStatus === 'issued' || d.status === 'issued'))
|
|
})
|
|
|
|
const showActions = computed(() => {
|
|
if (!detail.value) return false
|
|
const status = detail.value.status
|
|
return status === 'draft' || status === 'rejected' || canWithdraw.value
|
|
})
|
|
|
|
function goBack() {
|
|
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.bxAdvanceApply }) })
|
|
}
|
|
|
|
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 loadDetail() {
|
|
const id = detailId.value
|
|
if (!id) return
|
|
try {
|
|
const res = await advanceApi.getById(id)
|
|
detail.value = res.data
|
|
} catch (e) {
|
|
uni.showToast({ title: '加载详情失败', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
try {
|
|
const histRes = await advanceApi.getApprovalHistory(id)
|
|
const hist = histRes.data || []
|
|
if (Array.isArray(hist) && hist.length) {
|
|
approvals.value = hist
|
|
} else {
|
|
const apprRes = await advanceApi.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 advanceApi.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('advance', 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 advanceApi.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 advanceApi.delete(detail.value.id)
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
setTimeout(() => goBack(), 500)
|
|
} catch (e) {
|
|
// toasted
|
|
} finally {
|
|
actionLoading.delete = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function handleEdit() {
|
|
if (!detail.value) return
|
|
uni.navigateTo({
|
|
url: `${PAGE_PATHS.bxAdvanceApplyForm}?id=${detail.value.id}`
|
|
})
|
|
}
|
|
|
|
function handleWithdraw() {
|
|
if (!detail.value) return
|
|
uni.showModal({
|
|
title: '确认撤回',
|
|
content: '确定撤回该备用金申请?撤回后可修改再提交。',
|
|
confirmText: '确定撤回',
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
actionLoading.withdraw = true
|
|
try {
|
|
await advanceApi.withdraw(detail.value.id)
|
|
uni.showToast({ title: '已撤回', icon: 'success' })
|
|
} catch (e) {
|
|
// toasted by bxRequest
|
|
} finally {
|
|
actionLoading.withdraw = false
|
|
await loadDetail()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
onLoad((query) => {
|
|
detailId.value = query?.id ? String(query.id) : ''
|
|
loadDetail()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import '@/styles/bx-theme.scss';
|
|
</style>
|