Files
yunzerwebsiteallinone/uniapp/pages/erp/finance/fund.vue
T
2026-10-19 01:13:17 +08:00

521 lines
11 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="page">
<view class="tab-bar">
<scroll-view class="tab-scroll" scroll-x :show-scrollbar="false">
<view class="tab-list">
<view
v-for="ft in TYPE_TABS"
:key="ft.value || 'all'"
class="tab"
:class="{ active: fundType === ft.value }"
@tap="switchType(ft.value)"
>
<text>{{ ft.label }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="filter-bar">
<view class="search-box">
<FaIcon name="magnifying-glass" color="#94a3b8" :size="16" />
<input
v-model="keyword"
class="search-input"
placeholder="搜索单号 / 往来单位"
confirm-type="search"
@confirm="refresh"
/>
<text v-if="keyword" class="search-clear" @tap="clearKeyword">清空</text>
</view>
<view class="chip-row">
<view
v-for="opt in STATUS_OPTIONS"
:key="String(opt.value)"
class="chip"
:class="{ active: status === opt.value }"
@tap="onStatus(opt.value)"
>
<text>{{ opt.label }}</text>
</view>
</view>
<view class="date-row">
<picker mode="date" :value="dateFrom" @change="onDate('from', $event)">
<view class="date-cell">
<FaIcon name="calendar-days" color="#94a3b8" :size="14" />
<text>{{ dateFrom || '开始日期' }}</text>
</view>
</picker>
<text class="date-sep">至</text>
<picker mode="date" :value="dateTo" @change="onDate('to', $event)">
<view class="date-cell">
<FaIcon name="calendar-days" color="#94a3b8" :size="14" />
<text>{{ dateTo || '结束日期' }}</text>
</view>
</picker>
<text v-if="dateFrom || dateTo" class="date-clear" @tap="clearDates">重置</text>
</view>
</view>
<view class="sum-bar">
<text class="sum-item">共 {{ total }} 张单据</text>
<text class="sum-item">本页 {{ sumText }}</text>
</view>
<view v-if="loading && !list.length" class="state-box">
<text class="state-text">加载中...</text>
</view>
<view v-else-if="!list.length" class="state-box">
<u-empty mode="search" text="没有收付款单" />
</view>
<template v-else>
<view v-for="row in list" :key="row.id" class="d-card" @tap="openFund(row)">
<view class="d-icon" :class="incomeOf(row.fund_type) ? 'icon-in' : 'icon-out'">
<FaIcon
:name="incomeOf(row.fund_type) ? 'arrow-down-long' : 'arrow-up-long'"
:color="incomeOf(row.fund_type) ? '#16a34a' : '#ef4444'"
:size="15"
/>
</view>
<view class="d-main">
<view class="d-line">
<text class="d-partner">{{ row.partner_name || typeLabelOf(row.fund_type) }}</text>
<u-tag :text="fundStatusLabel(row.status)" :type="fundStatusType(row.status)" size="mini" />
</view>
<text class="d-sub">
{{ row.fund_no }} · {{ typeLabelOf(row.fund_type) }} · {{ fundMethodLabel(row.pay_method) }}
</text>
<text v-if="row.status === 0" class="d-draft">草稿未核销,确认后才冲抵往来账</text>
</view>
<view class="d-right">
<text class="d-amount" :class="incomeOf(row.fund_type) ? 'amt-in' : 'amt-out'">
{{ incomeOf(row.fund_type) ? '+' : '-' }}¥{{ moneyText(row.amount) }}
</text>
<text class="d-date">{{ row.biz_date }}</text>
</view>
</view>
<view v-if="finished" class="load-done">
<text>{{ list.length ? '没有更多了' : '' }}</text>
</view>
</template>
<view class="fab" @tap="createFund">
<FaIcon name="plus" color="#ffffff" :size="18" />
<text>登记收付款</text>
</view>
</view>
</template>
<script setup>
/**
* 收付款单列表。四类单据共用一页,靠 fund_type 分流。
* 草稿只登记不核销,确认后后端才写核销明细并回写往来账余额。
*/
import { computed, ref } from 'vue'
import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import { FUND_STATUS_OPTIONS, FUND_TYPES, fetchFunds, fundMethodLabel, fundStatusLabel, fundStatusType, moneyText } from '@/api/erp.js'
const PAGE_SIZE = 20
const TYPE_TABS = [{ value: '', label: '全部' }, ...FUND_TYPES]
const STATUS_OPTIONS = [{ value: '', label: '全部状态' }, ...FUND_STATUS_OPTIONS]
const list = ref([])
const total = ref(0)
const page = ref(1)
const loading = ref(false)
const finished = ref(false)
const fundType = ref('')
const status = ref('')
const keyword = ref('')
const dateFrom = ref('')
const dateTo = ref('')
/** 从客商页带过来时只看这一家的收付款 */
const partnerScope = ref('')
const sumText = computed(() => {
const sum = list.value.reduce((acc, r) => acc + (Number(r.amount) || 0), 0)
return `合计 ¥${moneyText(sum)}`
})
onLoad((query = {}) => {
if (query.fund_type) fundType.value = String(query.fund_type)
if (query.status) status.value = String(query.status)
if (query.partner_id) partnerScope.value = String(query.partner_id)
})
onShow(() => refresh())
onReachBottom(() => {
if (finished.value || loading.value) return
loadMore()
})
function incomeOf(type) {
return Number(type) === 1 || Number(type) === 3
}
function typeLabelOf(type) {
return (FUND_TYPES.find((t) => Number(t.value) === Number(type)) || {}).label || '收付款'
}
async function refresh() {
page.value = 1
finished.value = false
list.value = []
await loadPage()
}
async function loadMore() {
page.value += 1
await loadPage()
}
async function loadPage() {
loading.value = true
try {
const data = await fetchFunds({
page: page.value,
pageSize: PAGE_SIZE,
fundType: fundType.value,
status: status.value,
partnerId: partnerScope.value,
keyword: keyword.value.trim(),
dateFrom: dateFrom.value,
dateTo: dateTo.value
})
const rows = data.list || []
list.value = page.value === 1 ? rows : [...list.value, ...rows]
total.value = data.total || 0
finished.value = list.value.length >= total.value
} catch (e) {
if (page.value > 1) page.value -= 1
} finally {
loading.value = false
}
}
function switchType(value) {
if (fundType.value === value) return
fundType.value = value
refresh()
}
function onStatus(value) {
if (status.value === value) return
status.value = value
refresh()
}
function clearKeyword() {
keyword.value = ''
refresh()
}
function onDate(key, e) {
const val = e.detail.value
if (key === 'from') dateFrom.value = val
else dateTo.value = val
if (dateFrom.value && dateTo.value && dateFrom.value > dateTo.value) dateFrom.value = dateTo.value
refresh()
}
function clearDates() {
dateFrom.value = ''
dateTo.value = ''
refresh()
}
function openFund(row) {
uni.navigateTo({ url: `/pages/erp/finance/fund-edit?id=${row.id}` })
}
function createFund() {
uni.navigateTo({ url: `/pages/erp/finance/fund-edit${fundType.value ? `?fund_type=${fundType.value}` : ''}` })
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding-bottom: 160rpx;
}
.tab-bar {
position: sticky;
top: 0;
z-index: 10;
background: $color-card;
box-shadow: $shadow-header;
}
.tab-scroll {
white-space: nowrap;
box-sizing: border-box;
}
.tab-list {
display: inline-flex;
padding: 0 12rpx;
}
.tab {
padding: 26rpx 28rpx 22rpx;
position: relative;
text {
font-size: 26rpx;
color: $color-text-muted;
}
&.active {
text {
color: $color-primary;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 6rpx;
left: 50%;
width: 40rpx;
height: 6rpx;
margin-left: -20rpx;
border-radius: 3rpx;
background: $color-primary;
}
}
}
.filter-bar {
padding: 20rpx $page-padding-x 8rpx;
}
.search-box {
display: flex;
align-items: center;
gap: 12rpx;
height: 72rpx;
padding: 0 24rpx;
background: $color-card;
border-radius: $radius-full;
box-shadow: $shadow-card;
.search-input {
flex: 1;
font-size: 26rpx;
color: $color-text;
}
.search-clear {
font-size: 24rpx;
color: $color-text-muted;
}
}
.chip-row {
display: flex;
gap: 14rpx;
margin-top: 18rpx;
flex-wrap: wrap;
}
.chip {
padding: 10rpx 22rpx;
border-radius: $radius-full;
background: $color-card;
font-size: 23rpx;
color: $color-text-secondary;
box-shadow: $shadow-card;
&.active {
background: #2563eb;
color: #ffffff;
}
}
.date-row {
display: flex;
align-items: center;
gap: 14rpx;
margin-top: 16rpx;
}
.date-cell {
display: flex;
align-items: center;
gap: 8rpx;
height: 56rpx;
padding: 0 20rpx;
border-radius: $radius-md;
background: $color-card;
font-size: 23rpx;
color: $color-text-secondary;
}
.date-sep {
font-size: 22rpx;
color: $color-text-muted;
}
.date-clear {
font-size: 23rpx;
color: $color-primary;
}
.sum-bar {
display: flex;
justify-content: space-between;
margin: 6rpx $page-padding-x 16rpx;
padding: 0 8rpx;
}
.sum-item {
font-size: 24rpx;
color: $color-text-muted;
}
.d-card {
display: flex;
align-items: center;
gap: 18rpx;
@include card;
margin: 0 $page-padding-x 20rpx;
padding: 24rpx 28rpx;
&:active {
opacity: 0.85;
}
}
.d-icon {
width: 64rpx;
height: 64rpx;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.icon-in {
background: rgba(22, 163, 74, 0.1);
}
&.icon-out {
background: rgba(239, 68, 68, 0.1);
}
}
.d-main {
flex: 1;
min-width: 0;
}
.d-line {
display: flex;
align-items: center;
gap: 12rpx;
}
.d-partner {
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 500;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.d-sub {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: $color-text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.d-draft {
display: block;
margin-top: 6rpx;
font-size: 21rpx;
color: #f59e0b;
}
.d-right {
flex-shrink: 0;
text-align: right;
}
.d-amount {
display: block;
font-size: 30rpx;
font-weight: 600;
&.amt-in {
color: #16a34a;
}
&.amt-out {
color: $color-text;
}
}
.d-date {
display: block;
margin-top: 6rpx;
font-size: 21rpx;
color: $color-text-muted;
}
.load-done {
display: flex;
justify-content: center;
padding: 8rpx 0 24rpx;
font-size: 24rpx;
color: $color-text-muted;
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 40rpx;
}
.state-text {
font-size: 28rpx;
color: $color-text-muted;
}
.fab {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: calc(48rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
gap: 10rpx;
height: 84rpx;
padding: 0 44rpx;
border-radius: $radius-full;
background: #2563eb;
color: #ffffff;
font-size: 27rpx;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.4);
&:active {
opacity: 0.85;
}
}
</style>