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

473 lines
15 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">
<view class="topbar-back" @click="goBack">
<text class="topbar-back-icon">‹</text>
</view>
</view>
<text class="topbar-title">我的申请单</text>
<view class="topbar-right" />
</view>
<view class="fab-add" @click="onFabAdd">+</view>
<view class="mine-type-tabs stat-scroll">
<view
class="stat-card"
:class="{ active: activeBiz === 'advance' }"
@click="onBizChange('advance')"
>
<text class="label">备用金</text>
</view>
<view
class="stat-card"
:class="{ active: activeBiz === 'reimburse' }"
@click="onBizChange('reimburse')"
>
<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 search-filter--scroll" :show-scrollbar="false">
<view
v-for="opt in statusFilters"
:key="opt.value === '' ? '__all' : opt.value"
class="filter-tag"
:class="{ active: statusFilter === opt.value }"
@click="onStatusChange(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="!list.length && !loading" class="empty-state empty-state--fill">
<text>{{ activeBiz === 'advance' ? '暂无备用金申请' : '暂无报销申请' }}</text>
</view>
<!-- 备用金卡片 -->
<template v-if="activeBiz === 'advance'">
<view
v-for="item in list"
:key="'adv-' + item.id"
class="expense-card"
@click="goDetail(item, 'advance')"
>
<view class="ec-top">
<view class="ec-type">
<text class="advance-list-card__title">{{ advanceCardTitle(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="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 || item.code || '—' }}</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.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>
<view class="ec-meta">
<text>{{ formatApplyDateTime(item) }}</text>
</view>
<view class="ec-bottom" @click.stop>
<view v-if="item.status === 'draft'" class="op-actions">
<button class="btn btn-sm btn-danger" @click="handleDelete(item, 'advance')">
删除
</button>
<button class="btn btn-sm btn-primary" @click="handleSubmit(item, 'advance')">
提交
</button>
</view>
<view v-else-if="item.status === 'rejected'" class="op-actions">
<button class="btn btn-sm btn-primary" @click="goDetail(item, 'advance')">
查看
</button>
<button class="btn btn-sm btn-primary" @click="handleSubmit(item, 'advance')">
重新提交
</button>
</view>
<view v-else class="op-actions">
<button class="btn btn-sm btn-primary" @click="goDetail(item, 'advance')">
查看
</button>
</view>
</view>
</view>
</template>
<!-- 报销卡片 -->
<template v-else>
<view
v-for="item in list"
:key="'reim-' + item.id"
class="expense-card"
@click="goDetail(item, 'reimburse')"
>
<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 || item.code || '—' }}</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="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 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 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>
<view class="ec-meta">
<text>{{ formatApplyDateTime(item) }}</text>
</view>
<view class="ec-bottom" @click.stop>
<view v-if="item.status === 'draft'" class="op-actions">
<button class="btn btn-sm btn-danger" @click="handleDelete(item, 'reimburse')">
删除
</button>
<button class="btn btn-sm btn-primary" @click="handleSubmit(item, 'reimburse')">
提交
</button>
</view>
<view v-else-if="item.status === 'rejected'" class="op-actions">
<button class="btn btn-sm btn-primary" @click="goDetail(item, 'reimburse')">
查看
</button>
<button class="btn btn-sm btn-primary" @click="handleSubmit(item, 'reimburse')">
重新提交
</button>
</view>
<view v-else class="op-actions">
<button class="btn btn-sm btn-primary" @click="goDetail(item, 'reimburse')">
查看
</button>
</view>
</view>
</view>
</template>
<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 {
import { getSafeAreaStyle } from '@/utils/safeArea'
const safeAreaStyle = getSafeAreaStyle()
statusLabel,
workflowListStatusTagClass,
formatAmount,
formatApplyDateTime,
flattenExpenseTree,
normalizeExpenseItem,
resolveExpenseDisplay
} from '@/utils/bxFormat'
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 activeBiz = ref('advance')
const keyword = ref('')
const statusFilter = ref('')
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const refreshing = 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)
}
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 = {
page: currentPage.value,
pageSize
}
if (statusFilter.value) params.status = statusFilter.value
if (keyword.value.trim()) params.keyword = keyword.value.trim()
const api = activeBiz.value === 'advance' ? advanceApi : reimburseApi
const res = await api.my(params)
const rows = res.data?.rows || res.data?.items || []
const total = res.data?.total || 0
if (currentPage.value === 1) {
list.value = rows
} else {
list.value = list.value.concat(rows)
}
currentPage.value += 1
finished.value = rows.length < pageSize || list.value.length >= total
} catch (e) {
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 onBizChange(biz) {
if (activeBiz.value === biz) return
activeBiz.value = biz
onRefresh()
}
function onStatusChange(value) {
statusFilter.value = value
onRefresh()
}
function clearKeyword() {
keyword.value = ''
onRefresh()
}
function goBack() {
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
}
function onFabAdd() {
uni.showActionSheet({
itemList: ['备用金申请', '报销申请'],
success: (res) => {
if (res.tapIndex === 0) {
uni.navigateTo({ url: PAGE_PATHS.bxAdvanceApplyForm })
} else if (res.tapIndex === 1) {
uni.navigateTo({ url: PAGE_PATHS.bxReimburseApplyForm })
}
}
})
}
function goDetail(item, biz) {
const path =
biz === 'advance' ? PAGE_PATHS.bxAdvanceDetail : PAGE_PATHS.bxReimburseDetail
uni.navigateTo({ url: `${path}?id=${item.id}` })
}
async function handleSubmit(item, biz) {
try {
const api = biz === 'advance' ? advanceApi : reimburseApi
await api.submit(item.id)
uni.showToast({ title: '提交成功', icon: 'success' })
onRefresh()
} catch (e) {
// toasted
}
}
function handleDelete(item, biz) {
uni.showModal({
title: '删除确认',
content: '确定要删除该申请单吗?删除后不可恢复。',
confirmText: '确定删除',
confirmColor: '#ef4444',
success: async (res) => {
if (!res.confirm) return
try {
const api = biz === 'advance' ? advanceApi : reimburseApi
await api.delete(item.id)
uni.showToast({ title: '删除成功', icon: 'success' })
onRefresh()
} catch (e) {
// toasted
}
}
})
}
onShow(async () => {
computeListHeight()
await ensureExpenseFlat()
onRefresh()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
.list-scroll {
box-sizing: border-box;
}
</style>