Files
shanxia2.0/miniprogram/pages/bx/all/list.vue
T
2026-09-23 12:03:07 +08:00

451 lines
14 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="onRefresh"
/>
<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-scroll" :show-scrollbar="false">
<view class="search-filter">
<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>
</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 && !list.length" class="empty-state empty-state--fill">
<text>暂无记录</text>
</view>
<view
v-for="item in list"
: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="list.length" class="list-footer-tip">
{{ finished ? '没有更多了' : loading ? '加载中...' : '上拉加载更多' }}
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } 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 list = ref([])
const loading = ref(false)
const refreshing = ref(false)
const finished = ref(false)
const currentPage = ref(1)
const pageSize = 20
const listHeight = ref('55vh')
const flatList = ref([])
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 extractTotal(res) {
return Number(res?.data?.total ?? 0)
}
function buildParams() {
const params = {
page: currentPage.value,
pageSize
}
if (filterStatus.value) params.status = filterStatus.value
const kw = keyword.value.trim()
if (kw) params.keyword = kw
return params
}
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 fetchPage() {
if (loading.value && !refreshing.value) return
loading.value = true
try {
const params = buildParams()
let rows = []
let total = 0
let pageShort = false
if (activeTab.value === 1) {
const advRes = await advanceApi.list(params)
rows = mapAdvances(extractRows(advRes))
total = extractTotal(advRes)
pageShort = rows.length < pageSize
} else if (activeTab.value === 2) {
const reimRes = await reimburseApi.list(params)
rows = mapReimburses(extractRows(reimRes))
total = extractTotal(reimRes)
pageShort = rows.length < pageSize
} else {
const [advRes, reimRes] = await Promise.all([
advanceApi.list(params),
reimburseApi.list(params)
])
const advRows = mapAdvances(extractRows(advRes))
const reimRows = mapReimburses(extractRows(reimRes))
rows = [...advRows, ...reimRows]
rows.sort((a, b) => {
const tA = new Date(pickApplyTime(a) || 0).getTime()
const tB = new Date(pickApplyTime(b) || 0).getTime()
return tB - tA
})
total = extractTotal(advRes) + extractTotal(reimRes)
pageShort = advRows.length < pageSize && reimRows.length < pageSize
}
if (currentPage.value === 1) {
list.value = rows
} else {
list.value = list.value.concat(rows)
}
currentPage.value += 1
finished.value = pageShort || list.value.length >= total
} catch (e) {
if (currentPage.value === 1) list.value = []
finished.value = true
} finally {
loading.value = false
refreshing.value = false
}
}
function onRefresh() {
currentPage.value = 1
finished.value = false
loading.value = false
list.value = []
fetchPage()
}
function onPullRefresh() {
refreshing.value = true
currentPage.value = 1
finished.value = false
fetchPage()
}
function onLoadMore() {
if (finished.value || loading.value) return
fetchPage()
}
function onBizTab(tab) {
if (activeTab.value === tab) return
activeTab.value = tab
onRefresh()
}
function onStatusFilter(value) {
filterStatus.value = value
onRefresh()
}
function clearKeyword() {
keyword.value = ''
onRefresh()
}
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()
onRefresh()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
.list-scroll {
box-sizing: border-box;
}
</style>