Files
shanxia2.0/miniprogram/pages/bx/all/list.vue
T

432 lines
13 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">
<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>
<view class="mine-type-tabs stat-scroll mine-type-tabs--triple">
<view
class="stat-card"
:class="{ active: activeTab === 0 }"
@click="onBizTab(0)"
>
<text class="label">全部</text>
</view>
<view
class="stat-card"
:class="{ active: activeTab === 1 }"
@click="onBizTab(1)"
>
<text class="label">备用金</text>
</view>
<view
class="stat-card"
:class="{ active: activeTab === 2 }"
@click="onBizTab(2)"
>
<text class="label">报销申请</text>
</view>
</view>
<view class="search-wrap">
<view class="search-box">
<input
class="search-input"
v-model="keyword"
placeholder="搜索单号 / 申请人"
confirm-type="search"
@confirm="reload"
/>
<view v-if="keyword.trim()" class="search-box__clear" @click="clearKeyword">×</view>
</view>
</view>
<view class="search-wrap" style="padding-top: 0">
<scroll-view scroll-x class="search-filter search-filter--scroll" :show-scrollbar="false">
<view
v-for="opt in statusFilters"
:key="opt.value === '' ? '__all' : opt.value"
class="filter-tag"
:class="{ active: filterStatus === opt.value }"
@click="onStatusFilter(opt.value)"
>
{{ opt.label }}
</view>
</scroll-view>
</view>
<scroll-view
class="page-padded list-scroll"
scroll-y
:style="{ height: listHeight }"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onPullRefresh"
@scrolltolower="onLoadMore"
>
<view v-if="!loading && !displayList.length" class="empty-state empty-state--fill">
<text>暂无记录</text>
</view>
<view
v-for="item in displayList"
:key="item.id + '-' + item.bizType"
class="expense-card"
@click="goDetail(item)"
>
<template v-if="item.bizType === '备用金'">
<view class="ec-top">
<view class="ec-type">
<text class="advance-list-card__title">{{ advanceCardTitle(item) }}</text>
</view>
<text class="advance-ticket-status" :class="ticketHeaderStatusToneClass(item.status)">
{{ statusLabel(item.status) }}
</text>
</view>
<view class="ec-amount-label">申请金额</view>
<view class="ec-amount">{{ formatAmount(item.amount) }}</view>
<view class="advance-card-subs">
<view class="advance-card-sub-row">
<text class="k">实际发放金额</text>
<text class="v">¥ {{ actualIssuedDisplay(item) }}</text>
</view>
<view class="advance-card-sub-row">
<text class="k">实际冲抵</text>
<text class="v">¥ {{ formatAmount(advanceOffsetTotal(item)) }}</text>
</view>
</view>
<view class="list-card-kv">
<view class="list-card-kv__row">
<text class="list-card-kv__k">申请单号</text>
<text class="list-card-kv__v">{{ item.applyCode || '—' }}</text>
</view>
<view class="list-card-kv__row">
<text class="list-card-kv__k">申请人</text>
<text class="list-card-kv__v">{{ item.applicantName || '-' }}</text>
</view>
<view v-if="item.departmentName" class="list-card-kv__row">
<text class="list-card-kv__k">部门</text>
<text class="list-card-kv__v">{{ item.departmentName }}</text>
</view>
<view v-if="item.currentNode" class="list-card-kv__row">
<text class="list-card-kv__k">当前环节</text>
<text class="list-card-kv__v">{{ item.currentNode }}</text>
</view>
<view v-if="item.remark" class="list-card-kv__row">
<text class="list-card-kv__k">备注</text>
<text class="list-card-kv__v">{{ item.remark }}</text>
</view>
</view>
</template>
<template v-else>
<view class="ec-top">
<view class="ec-type">
<text class="reimburse-list-card__title">{{ reimburseCardTitle(item) }}</text>
</view>
<text class="status-tag" :class="workflowListStatusTagClass(item.status)">
{{ statusLabel(item.status) }}
</text>
</view>
<view class="ec-amount-label">报销金额</view>
<view class="ec-amount">{{ formatAmount(item.amount) }}</view>
<view class="reimburse-card-subs">
<view class="reimburse-card-sub-row">
<text class="k">冲抵金额</text>
<text class="v">¥ {{ moneyOrDash(item.offsetAmount ?? item.offset_amount) }}</text>
</view>
<view class="reimburse-card-sub-row">
<text class="k">实付金额</text>
<text class="v">
¥ {{ moneyOrDash(item.payAmount ?? item.actualAmount ?? item.actual_amount) }}
</text>
</view>
</view>
<view class="list-card-kv">
<view class="list-card-kv__row">
<text class="list-card-kv__k">申请单号</text>
<text class="list-card-kv__v">{{ item.applyCode || '—' }}</text>
</view>
<view class="list-card-kv__row">
<text class="list-card-kv__k">申请人</text>
<text class="list-card-kv__v">{{ item.applicantName || '-' }}</text>
</view>
<view v-if="item.departmentName" class="list-card-kv__row">
<text class="list-card-kv__k">部门</text>
<text class="list-card-kv__v">{{ item.departmentName }}</text>
</view>
<view v-if="item.currentNode" class="list-card-kv__row">
<text class="list-card-kv__k">当前环节</text>
<text class="list-card-kv__v">{{ item.currentNode }}</text>
</view>
<view
v-if="resolveExpenseDisplay(item, flatList) !== '-'"
class="list-card-kv__row"
>
<text class="list-card-kv__k">费用项目</text>
<text class="list-card-kv__v">{{ resolveExpenseDisplay(item, flatList) }}</text>
</view>
<view class="list-card-kv__row">
<text class="list-card-kv__k">发票 / 附件</text>
<text class="list-card-kv__v">
{{ item.invoiceCount ?? 0 }} 张 · {{ item.attachCount ?? 0 }} 个
</text>
</view>
<view v-if="item.remark" class="list-card-kv__row">
<text class="list-card-kv__k">备注</text>
<text class="list-card-kv__v">{{ item.remark }}</text>
</view>
</view>
</template>
<view class="ec-meta">
<text>{{ formatApplyDateTime(item) }}</text>
</view>
<view class="ec-bottom all-list-card__bottom" @click.stop>
<view class="op-actions">
<button class="btn btn-sm btn-primary" @click="goDetail(item)">查看</button>
</view>
</view>
</view>
<view v-if="displayList.length" class="list-footer-tip">
{{ finished ? '没有更多了' : loading ? '加载中...' : '上拉加载更多' }}
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { PAGE_PATHS } from '@/config/api'
import { advanceApi, reimburseApi, expenseItemApi } from '@/api/bx'
import {
statusLabel,
ticketHeaderStatusToneClass,
workflowListStatusTagClass,
formatAmount,
formatApplyDateTime,
pickApplyTime,
flattenExpenseTree,
normalizeExpenseItem,
resolveExpenseDisplay
} from '@/utils/bxFormat'
import { getSafeAreaStyle } from '@/utils/safeArea'
const safeAreaStyle = getSafeAreaStyle()
const statusFilters = [
{ label: '全部', value: '' },
{ label: '待提交', value: 'draft' },
{ label: '审批中', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已驳回', value: 'rejected' },
{ label: '已发放', value: 'issued' },
{ label: '已支付', value: 'paid' },
{ label: '已冲抵', value: 'offset' }
]
const activeTab = ref(0)
const keyword = ref('')
const filterStatus = ref('')
const allRows = ref([])
const loading = ref(false)
const refreshing = ref(false)
const finished = ref(false)
const displayPage = ref(1)
const displayPageSize = 20
const listHeight = ref('55vh')
const flatList = ref([])
const displayList = computed(() => {
const end = displayPage.value * displayPageSize
return allRows.value.slice(0, end)
})
function computeListHeight() {
try {
const sys = uni.getSystemInfoSync()
const h = (sys.windowHeight || 600) - 260
listHeight.value = `${Math.max(h, 280)}px`
} catch (e) {
listHeight.value = '55vh'
}
}
function advanceCardTitle(item) {
const r = (item.reason ?? '').toString().trim()
if (r) return r
const p = (item.purpose ?? '').toString().trim()
return p || '—'
}
function reimburseCardTitle(item) {
const t = (item.title ?? '').toString().trim()
if (t) return t
const ex = resolveExpenseDisplay(item, flatList.value)
return ex !== '-' ? ex : '—'
}
function actualIssuedDisplay(item) {
const raw = item.actualAmount ?? item.actual_amount
if (raw == null || raw === '') return '—'
const n = typeof raw === 'string' ? parseFloat(raw) : Number(raw)
if (Number.isNaN(n)) return '—'
return formatAmount(n)
}
function advanceOffsetTotal(item) {
const raw = item.offsetAmount ?? item.offset_amount ?? 0
const n = typeof raw === 'string' ? parseFloat(raw) : Number(raw)
return Number.isNaN(n) ? 0 : n
}
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 mapAdvances(rows) {
return (rows || []).map((r) => ({
...r,
bizType: '备用金',
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user
}))
}
function mapReimburses(rows) {
return (rows || []).map((r) => ({
...r,
bizType: '报销申请',
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user
}))
}
function extractRows(res) {
return res?.data?.rows || res?.data?.items || []
}
function syncFinished() {
finished.value = displayList.value.length >= allRows.value.length
}
async function ensureExpenseFlat() {
if (flatList.value.length) return
try {
const res = await expenseItemApi.tree()
const tree = (res.data || []).map(normalizeExpenseItem)
flatList.value = flattenExpenseTree(tree)
} catch (e) {
flatList.value = []
}
}
async function loadAllData() {
const kw = keyword.value.trim()
const params = { page: 1, pageSize: 280 }
if (filterStatus.value) params.status = filterStatus.value
if (kw) params.keyword = kw
loading.value = true
try {
if (activeTab.value === 1) {
const advRes = await advanceApi.list(params)
allRows.value = mapAdvances(extractRows(advRes))
return
}
if (activeTab.value === 2) {
const reimRes = await reimburseApi.list(params)
allRows.value = mapReimburses(extractRows(reimRes))
return
}
const [advRes, reimRes] = await Promise.all([
advanceApi.list(params),
reimburseApi.list(params)
])
const merged = [
...mapAdvances(extractRows(advRes)),
...mapReimburses(extractRows(reimRes))
]
merged.sort((a, b) => {
const tA = new Date(pickApplyTime(a) || 0).getTime()
const tB = new Date(pickApplyTime(b) || 0).getTime()
return tB - tA
})
allRows.value = merged
} catch (e) {
allRows.value = []
} finally {
loading.value = false
refreshing.value = false
}
}
async function reload() {
displayPage.value = 1
await loadAllData()
syncFinished()
}
function onPullRefresh() {
refreshing.value = true
reload()
}
function onLoadMore() {
if (finished.value || loading.value) return
if (displayList.value.length < allRows.value.length) {
displayPage.value += 1
syncFinished()
} else {
finished.value = true
}
}
async function onBizTab(tab) {
if (activeTab.value === tab) return
activeTab.value = tab
await reload()
}
async function onStatusFilter(value) {
filterStatus.value = value
await reload()
}
function clearKeyword() {
keyword.value = ''
reload()
}
function goBack() {
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
}
function goDetail(item) {
const path =
item.bizType === '备用金' ? PAGE_PATHS.bxAdvanceDetail : PAGE_PATHS.bxReimburseDetail
uni.navigateTo({ url: `${path}?id=${item.id}` })
}
onShow(async () => {
computeListHeight()
await ensureExpenseFlat()
await reload()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
.list-scroll {
box-sizing: border-box;
}
</style>