This commit is contained in:
hero920103 committed 2026-10-19 01:13:17 +08:00
1 parent 5718a00e31
commit 1850ac640e
226 files changed
+37772 -12071

No files matched your search

+454
View File
@@ -0,0 +1,454 @@
<template>
<view v-if="bill" class="page">
<view class="hero">
<view class="hero-top">
<text class="hero-partner">{{ bill.partner_name }}</text>
<u-tag :text="bill.status_label" :type="statusType" size="mini" />
</view>
<text class="hero-label">{{ billTypeText }}金额</text>
<text class="hero-amount">¥{{ moneyText(bill.amount) }}</text>
<view class="hero-grid">
<view class="hero-cell">
<text class="hero-cell-label">已核销</text>
<text class="hero-cell-value">¥{{ moneyText(bill.settled_amount) }}</text>
</view>
<view class="hero-cell">
<text class="hero-cell-label">未{{ bill.bill_type === 1 ? '收' : '付' }}</text>
<text class="hero-cell-value strong">¥{{ moneyText(balance) }}</text>
</view>
<view class="hero-cell">
<text class="hero-cell-label">{{ progressText }}</text>
<text class="hero-cell-value">{{ progressPercent }}%</text>
</view>
</view>
</view>
<view class="blk">
<view class="blk-head">
<text class="blk-title">账单信息</text>
</view>
<view class="kv">
<text class="kv-label">账单编号</text>
<text class="kv-value">{{ bill.id }}</text>
</view>
<view class="kv">
<text class="kv-label">来源单据</text>
<text class="kv-value link" @tap="openDoc">{{ bill.source_doc_no || '手工账单' }}</text>
</view>
<view class="kv">
<text class="kv-label">账单日期</text>
<text class="kv-value">{{ bill.bill_date || '—' }}</text>
</view>
<view class="kv">
<text class="kv-label">账期到期</text>
<text class="kv-value">
{{ bill.due_date || '未设账期' }}
<text v-if="bill.overdue_days > 0" class="kv-warn">(已逾期 {{ bill.overdue_days }} 天)</text>
</text>
</view>
<view v-if="bill.remark" class="kv">
<text class="kv-label">备注</text>
<text class="kv-value">{{ bill.remark }}</text>
</view>
</view>
<view class="blk">
<view class="blk-head">
<text class="blk-title">核销记录</text>
<text class="blk-count">{{ settles.length }} 笔</text>
</view>
<view v-if="!settles.length" class="blk-empty">
<text>尚未核销,登记{{ bill.bill_type === 1 ? '收款' : '付款' }}后自动冲抵</text>
</view>
<view v-for="s in settles" :key="s.id" class="st-row" @tap="openFund(s)">
<view class="st-icon">
<FaIcon
:name="fundOf(s) && fundOf(s).fund_type <= 2 ? 'money-bill-transfer' : 'wallet'"
color="#2563eb"
:size="15"
/>
</view>
<view class="st-main">
<text class="st-title">{{ (fundOf(s) || {}).fund_no || s.partner_bill_no || '核销记录' }}</text>
<text class="st-sub">{{ dayText(s.create_time) }}<text v-if="fundOf(s)"> · {{ fundMethodText(s) }}</text></text>
</view>
<text class="st-amount">¥{{ moneyText(s.amount) }}</text>
</view>
</view>
<view v-if="docItems.length" class="blk">
<view class="blk-head">
<text class="blk-title">来源单据明细</text>
<text class="blk-count">{{ docItems.length }} 行</text>
</view>
<view v-for="it in docItems" :key="it.id" class="di-row">
<view class="di-main">
<text class="di-name">{{ it.product_name }}</text>
<text class="di-sub">{{ [it.product_code, it.spec].filter(Boolean).join(' · ') }}</text>
</view>
<view class="di-right">
<text class="di-amount">¥{{ moneyText(it.amount) }}</text>
<text class="di-qty">{{ qtyText(it.quantity) }} {{ it.unit || '' }} × ¥{{ moneyText(it.unit_price) }}</text>
</view>
</view>
</view>
<view v-if="balance > 0.005" class="footer">
<u-button
type="primary"
color="#2563eb"
shape="circle"
:text="`登记${bill.bill_type === 1 ? '收款' : '付款'}冲抵`"
@click="goSettle"
/>
</view>
</view>
<view v-else class="page">
<view v-if="loading" class="state-box">
<text class="state-text">加载中...</text>
</view>
<view v-else class="state-box">
<u-empty text="账单不存在或已删除" mode="data" />
</view>
</view>
</template>
<script setup>
/**
* 往来账单详情:核销进度、逐笔核销来源、关联的收付款单与原始单据明细。
* funds 是后端按核销记录聚合出来的收付款单字典,核销行靠 fund_bill_id 反查单号与方式。
*/
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import { billStatusType, dayText, fundMethodLabel, getBill, moneyText, qtyText } from '@/api/erp.js'
const bill = ref(null)
const settles = ref([])
const funds = ref([])
const docItems = ref([])
const balance = ref(0)
const loading = ref(true)
const billTypeText = computed(() => (bill.value && bill.value.bill_type === 1 ? '应收' : '应付'))
const statusType = computed(() => billStatusType((bill.value || {}).status))
const progressPercent = computed(() => {
const amount = Number((bill.value || {}).amount || 0)
if (!amount) return 0
return Math.min(100, Math.round((Number(bill.value.settled_amount || 0) / amount) * 100))
})
const progressText = computed(() => (progressPercent.value >= 100 ? '已结清' : '已核销比例'))
onLoad(async (query = {}) => {
const id = Number(query.id || 0)
try {
if (id) {
const data = await getBill(id)
if (data) {
bill.value = data.bill
settles.value = data.settles || []
funds.value = data.funds || []
docItems.value = data.docItems || []
balance.value = Number(data.bill.balance || 0)
}
}
} finally {
loading.value = false
}
uni.setNavigationBarTitle({ title: billTypeText.value + '详情' })
})
function fundOf(s) {
return funds.value.find((f) => Number(f.id) === Number(s.fund_bill_id))
}
function fundMethodText(s) {
const f = fundOf(s)
return f ? fundMethodLabel(f.pay_method) : ''
}
function openDoc() {
if (!bill.value.source_doc_id) return
uni.navigateTo({ url: `/pages/erp/document/detail?id=${bill.value.source_doc_id}` })
}
function openFund(s) {
const f = fundOf(s)
if (!f) return
uni.navigateTo({ url: `/pages/erp/finance/fund-edit?id=${f.id}` })
}
function goSettle() {
const q = `fund_type=${bill.value.bill_type}&partner_id=${bill.value.partner_id}&partner_name=${encodeURIComponent(
bill.value.partner_name || ''
)}&bill_id=${bill.value.id}`
uni.navigateTo({ url: `/pages/erp/finance/fund-edit?${q}` })
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding-bottom: 160rpx;
}
.hero {
margin: 24rpx $page-padding-x 0;
padding: 32rpx 32rpx 28rpx;
border-radius: $radius-lg;
background: $hero-gradient;
color: #ffffff;
box-shadow: $shadow-card;
}
.hero-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.hero-partner {
flex: 1;
min-width: 0;
font-size: 30rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hero-label {
display: block;
margin-top: 22rpx;
font-size: 22rpx;
opacity: 0.85;
}
.hero-amount {
display: block;
margin-top: 6rpx;
font-size: 52rpx;
font-weight: 600;
line-height: 1.2;
}
.hero-grid {
display: flex;
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.25);
}
.hero-cell {
flex: 1;
min-width: 0;
.hero-cell-label {
display: block;
font-size: 21rpx;
opacity: 0.85;
}
.hero-cell-value {
display: block;
margin-top: 6rpx;
font-size: 26rpx;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.strong {
font-weight: 600;
}
}
}
.blk {
@include card;
margin: 20rpx $page-padding-x 0;
padding: 24rpx 28rpx;
}
.blk-head {
@include block-head;
}
.blk-title {
@include section-title;
}
.blk-count {
font-size: 22rpx;
color: $color-text-muted;
}
.blk-empty {
padding: 20rpx 0 8rpx;
font-size: 24rpx;
color: $color-text-muted;
line-height: 1.6;
}
.kv {
display: flex;
gap: 20rpx;
padding: 14rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
padding-bottom: 4rpx;
}
.kv-label {
width: 150rpx;
flex-shrink: 0;
font-size: 25rpx;
color: $color-text-muted;
}
.kv-value {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: $color-text;
word-break: break-all;
&.link {
color: $color-primary;
}
}
.kv-warn {
color: $color-danger;
font-size: 23rpx;
}
}
.st-row {
display: flex;
align-items: center;
gap: 18rpx;
padding: 18rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
}
}
.st-icon {
@include icon-box(60rpx);
}
.st-main {
flex: 1;
min-width: 0;
.st-title {
display: block;
font-size: 27rpx;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.st-sub {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: $color-text-muted;
}
}
.st-amount {
flex-shrink: 0;
font-size: 28rpx;
font-weight: 600;
color: #16a34a;
}
.di-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
padding-bottom: 4rpx;
}
}
.di-main {
flex: 1;
min-width: 0;
.di-name {
display: block;
font-size: 27rpx;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.di-sub {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: $color-text-muted;
}
}
.di-right {
flex-shrink: 0;
text-align: right;
.di-amount {
display: block;
font-size: 27rpx;
font-weight: 600;
color: $color-text;
}
.di-qty {
display: block;
margin-top: 4rpx;
font-size: 21rpx;
color: $color-text-muted;
}
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx $page-padding-x calc(20rpx + env(safe-area-inset-bottom));
background: $color-card;
box-shadow: $shadow-float;
@include dialog-responsive;
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 200rpx 40rpx;
}
.state-text {
font-size: 28rpx;
color: $color-text-muted;
}
</style>
+524
View File
@@ -0,0 +1,524 @@
<template>
<view class="page">
<view class="tab-bar">
<view
v-for="bt in BILL_TYPES"
:key="bt.value"
class="tab"
:class="{ active: billType === bt.value }"
@tap="switchType(bt.value)"
>
<text class="tab-label">{{ bt.label }}</text>
<text class="tab-sum">未{{ bt.value === 1 ? '收' : '付' }} ¥{{ moneyText(openOf(bt.value)) }}</text>
</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 STATE_OPTIONS"
:key="opt.value || 'all'"
class="chip"
:class="{ active: settleState === opt.value }"
@tap="onState(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 v-if="partnerName" class="sum-item" @tap="clearPartner">已筛「{{ partnerName }}」×</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="`没有${billTypeLabel}账单`" />
</view>
<template v-else>
<view v-for="row in list" :key="row.id" class="f-card" @tap="openBill(row)">
<view class="f-top">
<text class="f-partner">{{ row.partner_name }}</text>
<u-tag :text="row.status_label" :type="row.status === 2 ? 'success' : row.status === 1 ? 'primary' : 'warning'" size="mini" />
</view>
<text class="f-sub">{{ row.source_doc_no || '手工账单' }} · {{ row.bill_date }}</text>
<view class="f-amounts">
<view class="f-cell">
<text class="cell-label">{{ billType === 1 ? '应收' : '应付' }}</text>
<text class="cell-value">¥{{ moneyText(row.amount) }}</text>
</view>
<view class="f-cell">
<text class="cell-label">已核销</text>
<text class="cell-value">¥{{ moneyText(row.settled_amount) }}</text>
</view>
<view class="f-cell">
<text class="cell-label">{{ billType === 1 ? '未收' : '未付' }}</text>
<text class="cell-value open">¥{{ moneyText(row.balance) }}</text>
</view>
</view>
<view class="f-foot">
<text class="foot-due">{{ row.due_date ? `到期 ${row.due_date}` : '未设账期' }}</text>
<text v-if="row.overdue_days > 0" class="foot-over">逾期 {{ row.overdue_days }} 天</text>
<text v-else-if="row.status !== 2" class="foot-ok">账期内</text>
</view>
</view>
<view v-if="finished" class="load-done">
<text>{{ list.length ? '没有更多了' : '' }}</text>
</view>
</template>
<view class="fab" @tap="goFund">
<FaIcon name="money-bill-wave" color="#ffffff" :size="18" />
<text>{{ billType === 1 ? '登记收款' : '登记付款' }}</text>
</view>
</view>
</template>
<script setup>
/**
* 应收应付账单列表。
* 「未收/未付合计」用的是接口随列表返回的全量汇总,不是本页累加,翻页也不会跳数。
*/
import { computed, ref } from 'vue'
import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import { BILL_TYPES, SETTLE_STATES, fetchBills, moneyText } from '@/api/erp.js'
const PAGE_SIZE = 20
const STATE_OPTIONS = [{ value: '', label: '全部' }, ...SETTLE_STATES]
const list = ref([])
const total = ref(0)
const page = ref(1)
const loading = ref(false)
const finished = ref(false)
const billType = ref(1)
const partnerId = ref('')
const partnerName = ref('')
const keyword = ref('')
const settleState = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const summary = ref({ openReceivable: 0, openPayable: 0 })
const billTypeLabel = computed(() => (billType.value === 1 ? '应收' : '应付'))
onLoad((query = {}) => {
if (query.bill_type) billType.value = Number(query.bill_type) === 2 ? 2 : 1
if (query.partner_id) {
partnerId.value = String(query.partner_id)
partnerName.value = decodeURIComponent(query.partner_name || '')
}
if (query.settle_state) settleState.value = String(query.settle_state)
})
onShow(() => refresh())
onReachBottom(() => {
if (finished.value || loading.value) return
loadMore()
})
function openOf(type) {
return type === 1 ? summary.value.openReceivable : summary.value.openPayable
}
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 fetchBills({
page: page.value,
pageSize: PAGE_SIZE,
billType: billType.value,
partnerId: partnerId.value,
partnerType: billType.value === 1 ? 'customer' : 'supplier',
settleState: settleState.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
summary.value = data.summary || summary.value
finished.value = list.value.length >= total.value
} catch (e) {
if (page.value > 1) page.value -= 1
} finally {
loading.value = false
}
}
function switchType(value) {
if (billType.value === value) return
billType.value = value
// 应收只看客户、应付只看供应商,跨类型带的客商筛选必须一起清掉
partnerId.value = ''
partnerName.value = ''
refresh()
}
function clearPartner() {
partnerId.value = ''
partnerName.value = ''
refresh()
}
function clearKeyword() {
keyword.value = ''
refresh()
}
function onState(value) {
if (settleState.value === value) return
settleState.value = 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 openBill(row) {
uni.navigateTo({ url: `/pages/erp/finance/bill-detail?id=${row.id}` })
}
function goFund() {
const q = `fund_type=${billType.value}${partnerId.value ? `&partner_id=${partnerId.value}&partner_name=${encodeURIComponent(partnerName.value)}` : ''}`
uni.navigateTo({ url: `/pages/erp/finance/fund-edit?${q}` })
}
</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;
display: flex;
background: $color-card;
box-shadow: $shadow-header;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
padding: 22rpx 0 18rpx;
position: relative;
.tab-label {
font-size: 27rpx;
color: $color-text-muted;
}
.tab-sum {
font-size: 21rpx;
color: $color-text-muted;
}
&.active {
.tab-label {
color: $color-primary;
font-weight: 600;
}
.tab-sum {
color: $color-primary-dark;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 60rpx;
height: 6rpx;
margin-left: -30rpx;
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;
}
.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;
}
.f-card {
@include card;
margin: 0 $page-padding-x 20rpx;
padding: 24rpx 28rpx;
&:active {
opacity: 0.85;
}
}
.f-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.f-partner {
flex: 1;
min-width: 0;
font-size: 29rpx;
font-weight: 500;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.f-sub {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
color: $color-text-muted;
}
.f-amounts {
display: flex;
gap: 12rpx;
margin-top: 18rpx;
padding-top: 16rpx;
border-top: 1rpx solid $color-divider;
}
.f-cell {
flex: 1;
min-width: 0;
.cell-label {
display: block;
font-size: 21rpx;
color: $color-text-muted;
}
.cell-value {
display: block;
margin-top: 6rpx;
font-size: 26rpx;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.open {
font-weight: 600;
color: $color-danger;
}
}
}
.f-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14rpx;
font-size: 22rpx;
}
.foot-due {
color: $color-text-muted;
}
.foot-over {
color: $color-danger;
font-weight: 500;
}
.foot-ok {
color: #16a34a;
}
.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>
+782
View File
@@ -0,0 +1,782 @@
<template>
<view class="page">
<view v-if="fundNo" class="no-bar">
<text class="no-text">单号 {{ fundNo }}</text>
<u-tag :text="fundStatusLabel(status)" :type="fundStatusType(status)" size="mini" />
</view>
<!-- 单据信息 -->
<view class="form-card">
<view class="field">
<text class="field-label">款项类型 <text class="required">*</text></text>
<view v-if="editable" class="type-grid">
<view
v-for="ft in FUND_TYPES"
:key="ft.value"
class="type-cell"
:class="{ active: form.fundType === ft.value }"
@tap="onType(ft.value)"
>
<text>{{ ft.label }}</text>
</view>
</view>
<text v-else class="field-value">{{ typeLabel }}</text>
</view>
<view class="field">
<text class="field-label">业务日期 <text class="required">*</text></text>
<picker v-if="editable" mode="date" :value="form.bizDate" @change="onDate">
<view class="picker-value">
<text>{{ form.bizDate }}</text>
<FaIcon name="calendar-days" color="#909399" :size="16" />
</view>
</picker>
<text v-else class="field-value">{{ form.bizDate }}</text>
</view>
<view class="field">
<text class="field-label">
往来单位<text v-if="needPartner" class="required">*</text>
</text>
<view v-if="editable" class="picker-value" @tap="openPartner">
<text :class="{ placeholder: !form.partnerName }">{{ form.partnerName || chooseText }}</text>
<FaIcon name="chevron-right" color="#909399" :size="16" />
</view>
<text v-else class="field-value">{{ form.partnerName || '—' }}</text>
</view>
<view class="field">
<text class="field-label">金额 <text class="required">*</text></text>
<view class="amount-box">
<text class="amount-sign" :class="income ? 'sign-in' : 'sign-out'">{{ income ? '+' : '-' }}</text>
<input
v-if="editable"
v-model="form.amount"
class="amount-input"
type="digit"
placeholder="0.00"
/>
<text v-else class="field-value">¥{{ moneyText(form.amount) }}</text>
</view>
</view>
<view class="field">
<text class="field-label">结算方式</text>
<view v-if="editable" class="chip-line">
<view
v-for="m in FUND_METHODS"
:key="m.value"
class="mini-chip"
:class="{ active: form.payMethod === m.value }"
@tap="form.payMethod = m.value"
>
<text>{{ m.label }}</text>
</view>
</view>
<text v-else class="field-value">{{ fundMethodLabel(form.payMethod) }}</text>
</view>
<view v-if="editable || form.payAccount" class="field">
<text class="field-label">收付账户</text>
<u-input
v-if="editable"
v-model="form.payAccount"
placeholder="账号 / 商户主体(选填)"
:maxlength="100"
border="none"
/>
<text v-else class="field-value">{{ form.payAccount }}</text>
</view>
<view v-if="editable || form.remark" class="field">
<text class="field-label">备注</text>
<u-textarea
v-if="editable"
v-model="form.remark"
placeholder="付款事由、凭证号等(选填)"
:maxlength="200"
height="80"
border="none"
/>
<text v-else class="field-value">{{ form.remark }}</text>
</view>
</view>
<!-- 核销:收款/付款 + 已选往来单位时才出现 -->
<view v-if="showSettle" class="settle-card">
<view class="settle-head">
<text class="settle-title">核销往来账</text>
<text class="settle-sum">已选 ¥{{ moneyText(pickedSum) }} / {{ moneyText(form.amount) }}</text>
</view>
<text class="settle-tip">
{{ bills.length
? '不勾选则确认时按到期日从早到晚自动冲抵'
: needPartner && !form.partnerId
? '选择往来单位后显示其未结账单'
: '该往来单位没有未结账单,确认后只记资金流水' }}
</text>
<view v-if="billsLoading" class="settle-loading">
<text>正在拉取待核销账单...</text>
</view>
<view v-for="b in bills" :key="b.id" class="bill-row">
<view class="bill-check" :class="{ on: isPicked(b.id) }" @tap="toggleBill(b)">
<FaIcon v-if="isPicked(b.id)" name="check" color="#ffffff" :size="12" />
</view>
<view class="bill-main">
<text class="bill-no">{{ b.source_doc_no || `账单#${b.id}` }}</text>
<text class="bill-sub">
{{ dayText(b.bill_date) }}{{ b.due_date ? ` · 到期 ${dayText(b.due_date)}` : '' }} · 未结 ¥{{ moneyText(b.balance) }}
</text>
</view>
<input
v-if="isPicked(b.id)"
v-model="picked[b.id]"
class="bill-input"
type="digit"
:placeholder="String(b.balance)"
/>
<text v-else class="bill-balance">¥{{ moneyText(b.balance) }}</text>
</view>
<view v-if="bills.length" class="settle-foot">
<text class="foot-link" @tap="pickAll">全部冲抵</text>
<text v-if="pickedCount" class="foot-link" @tap="clearPicks">清空({{ pickedCount }})</text>
</view>
</view>
<view class="footer">
<template v-if="editable">
<view class="btn-line">
<u-button
type="primary"
color="#2563eb"
shape="circle"
:loading="saving"
loading-text="保存中..."
text="保存草稿"
@click="onSave(false)"
/>
</view>
<view class="btn-line gap">
<u-button
type="primary"
color="#16a34a"
shape="circle"
:loading="saving"
loading-text="确认中..."
:text="`确认${typeLabel}`"
@click="onSave(true)"
/>
</view>
<view v-if="fundId" class="del-line" @tap="onDelete">
<text>删除草稿</text>
</view>
</template>
<template v-else>
<view v-if="status === 1" class="btn-line">
<u-button type="error" plain shape="circle" text="作废此单" @click="onVoid" />
</view>
<view v-if="settles.length" class="view-settles">
<text class="vs-title">核销记录</text>
<view v-for="s in settles" :key="s.id" class="vs-row">
<text class="vs-no">{{ s.partner_bill_no || `账单#${s.partner_bill_id}` }}</text>
<text class="vs-amount">¥{{ moneyText(s.amount) }}</text>
</view>
</view>
</template>
</view>
<ErpPicker
:show="picker.show"
:title="partnerTitle"
placeholder="搜索客商"
:options="picker.options"
:model-value="form.partnerId"
remote
:loading="picker.loading"
:tip="picker.tip"
@update:show="picker.show = false"
@search="loadPartners"
@select="onPartner"
/>
</view>
</template>
<script setup>
/**
* 收付款开单 / 查看。
* 草稿只是登记,确认那一刻后端才写核销明细并回写往来账余额;
* 已确认的单不可改,只能作废(余额同步回滚),所以 view 模式复用同一张表单只读渲染。
*/
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import ErpPicker from '@/components/erp/ErpPicker.vue'
import {
FUND_METHODS,
FUND_TYPES,
confirmFund,
dayText,
deleteFund,
fetchOpenBills,
fetchPartners,
fundMethodLabel,
fundStatusLabel,
fundStatusType,
getFund,
moneyText,
saveFund,
voidFund
} from '@/api/erp.js'
function today() {
const d = new Date()
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
const fundId = ref(0)
const fundNo = ref('')
const status = ref(0)
const saving = ref(false)
const bills = ref([])
const billsLoading = ref(false)
const settles = ref([])
const picked = reactive({})
const form = reactive({
fundType: 1,
bizDate: today(),
partnerId: 0,
partnerName: '',
amount: '',
payMethod: 'cash',
payAccount: '',
remark: ''
})
const picker = reactive({ show: false, options: [], loading: false, tip: '' })
const editable = computed(() => !fundId.value || status.value === 0)
const typeLabel = computed(() => (FUND_TYPES.find((t) => Number(t.value) === Number(form.fundType)) || {}).label || '收付款')
const needPartner = computed(() => Number(form.fundType) === 1 || Number(form.fundType) === 2)
const income = computed(() => Number(form.fundType) === 1 || Number(form.fundType) === 3)
const showSettle = computed(() => editable.value && needPartner.value)
const partnerTitle = computed(() => (Number(form.fundType) === 2 ? '选择供应商' : '选择客户'))
const chooseText = computed(() => (Number(form.fundType) === 2 ? '选择供应商' : '选择客户'))
const pickedCount = computed(() => Object.keys(picked).filter((k) => isPicked(k)).length)
const pickedSum = computed(() =>
Object.keys(picked).reduce((sum, k) => (isPicked(k) ? sum + (Number(picked[k]) || 0) : sum), 0)
)
onLoad(async (query = {}) => {
if (query.fund_type) form.fundType = Number(query.fund_type)
if (query.biz_date) form.bizDate = String(query.biz_date)
if (query.partner_id) {
form.partnerId = Number(query.partner_id)
form.partnerName = decodeURIComponent(query.partner_name || '')
}
if (query.id) {
await loadFund(Number(query.id))
} else if (needPartner.value && form.partnerId) {
// 从账单详情跳过来时把这张账单预勾选,金额默认取未结额
await loadBills(Number(query.bill_id || 0))
}
})
async function loadFund(id) {
const data = await getFund(id)
if (!data || !data.fund) {
uni.showToast({ title: '收付款单不存在', icon: 'none' })
setTimeout(() => uni.navigateBack(), 600)
return
}
const f = data.fund
fundId.value = f.id
fundNo.value = f.fund_no
status.value = Number(f.status || 0)
form.fundType = Number(f.fund_type)
form.bizDate = f.biz_date || today()
form.partnerId = Number(f.partner_id || 0)
form.partnerName = f.partner_name || ''
form.amount = String(f.amount ?? '')
form.payMethod = f.pay_method || 'cash'
form.payAccount = f.pay_account || ''
form.remark = f.remark || ''
settles.value = data.settles || []
uni.setNavigationBarTitle({ title: fundStatusLabel(status.value) === '草稿' ? '编辑收付款' : '收付款详情' })
}
async function onType(value) {
if (Number(form.fundType) === Number(value)) return
form.fundType = Number(value)
// 收款只核客户应收、付款只核供应商应付,切换后原往来单位与勾选一并作废
form.partnerId = 0
form.partnerName = ''
clearPicks()
bills.value = []
}
function onDate(e) {
form.bizDate = e.detail.value
}
function openPartner() {
picker.show = true
loadPartners('')
}
async function loadPartners(keyword) {
picker.loading = true
try {
const partnerType = Number(form.fundType) === 2 ? 'supplier' : 'customer'
const { list, total } = await fetchPartners({ partnerType, keyword, onlyEnabled: true, pageSize: 50 })
picker.options = list.map((p) => ({
value: p.partner_id,
label: p.partner_name,
sub: [p.contact_person, p.contact_phone].filter(Boolean).join(' · '),
extra: Number(p.open_amount) > 0 ? `未结 ¥${moneyText(p.open_amount)}` : ''
}))
picker.tip = total > list.length ? `共 ${total} 家,输入关键字继续找` : ''
} finally {
picker.loading = false
}
}
async function onPartner(opt) {
picker.show = false
form.partnerId = Number(opt.value)
form.partnerName = opt.label
clearPicks()
await loadBills(0)
}
async function loadBills(presetBillId) {
if (!needPartner.value || !form.partnerId) return
billsLoading.value = true
try {
bills.value = await fetchOpenBills(form.fundType, form.partnerId)
if (presetBillId) {
const hit = bills.value.find((b) => Number(b.id) === Number(presetBillId))
if (hit) {
picked[hit.id] = String(hit.balance)
if (!Number(form.amount)) form.amount = String(hit.balance)
}
}
} finally {
billsLoading.value = false
}
}
function isPicked(id) {
return picked[id] !== undefined
}
function toggleBill(b) {
if (isPicked(b.id)) delete picked[b.id]
else picked[b.id] = String(b.balance)
}
function pickAll() {
bills.value.forEach((b) => {
picked[b.id] = String(b.balance)
})
const sum = bills.value.reduce((acc, b) => acc + Number(b.balance || 0), 0)
if (!Number(form.amount)) form.amount = String(Math.round(sum * 100) / 100)
}
function clearPicks() {
Object.keys(picked).forEach((k) => delete picked[k])
}
function checkForm() {
if (!(Number(form.amount) > 0)) return '金额必须大于 0'
if (needPartner.value && !form.partnerId) return `请选择${Number(form.fundType) === 2 ? '供应商' : '客户'}`
if (pickedSum.value > Number(form.amount) + 0.005) {
return `核销合计 ¥${moneyText(pickedSum.value)} 不能大于收付金额`
}
return ''
}
async function onSave(confirm) {
const msg = checkForm()
if (msg) {
uni.showToast({ title: msg, icon: 'none' })
return
}
saving.value = true
try {
const res = await saveFund({
id: fundId.value,
fundType: form.fundType,
bizDate: form.bizDate,
partnerId: form.partnerId,
amount: Number(form.amount),
payMethod: form.payMethod,
payAccount: form.payAccount,
remark: form.remark
})
const id = res.id || fundId.value
if (confirm) {
const lines = Object.keys(picked)
.filter((k) => isPicked(k) && Number(picked[k]) > 0)
.map((k) => ({ partnerBillId: Number(k), amount: Number(picked[k]) }))
await confirmFund(id, lines)
uni.showToast({ title: `已确认${typeLabel.value}`, icon: 'success' })
} else {
fundId.value = id
uni.showToast({ title: '草稿已保存', icon: 'success' })
}
setTimeout(() => back(), 600)
} finally {
saving.value = false
}
}
function onVoid() {
uni.showModal({
title: '作废收付款单',
content: '作废后已核销的往来账余额会同步回滚,确定继续?',
confirmColor: '#f56c6c',
success: async (res) => {
if (!res.confirm) return
await voidFund(fundId.value)
uni.showToast({ title: '已作废', icon: 'success' })
setTimeout(() => back(), 600)
}
})
}
async function onDelete() {
uni.showModal({
title: '删除草稿',
content: `确定删除「${fundNo.value || typeLabel.value}」这张草稿?`,
confirmColor: '#f56c6c',
success: async (res) => {
if (!res.confirm) return
await deleteFund(fundId.value)
uni.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => back(), 600)
}
})
}
/** 列表页跳来的栈可能是账单详情,优先回到它的来源列表 */
function back() {
const pages = getCurrentPages()
if (pages.length > 1) uni.navigateBack()
else uni.redirectTo({ url: '/pages/erp/finance/fund' })
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding: 24rpx $page-padding-x 40rpx;
}
.no-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx 20rpx;
.no-text {
font-size: 24rpx;
color: $color-text-muted;
}
}
.form-card {
@include card;
padding: 8rpx 28rpx;
}
.field {
padding: 24rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
}
.field-label {
display: block;
margin-bottom: 14rpx;
font-size: 26rpx;
color: $color-text-secondary;
}
.field-value {
display: block;
font-size: 28rpx;
color: $color-text;
line-height: 1.6;
word-break: break-all;
}
}
.required {
color: $color-danger;
}
.picker-value {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
font-size: 28rpx;
color: $color-text;
.placeholder {
color: $color-text-muted;
}
}
.type-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.type-cell {
flex: 1;
min-width: 140rpx;
height: 68rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-md;
background: #f1f5f9;
font-size: 25rpx;
color: $color-text-secondary;
&.active {
background: $color-primary-bg;
color: $color-primary-dark;
font-weight: 600;
}
}
.amount-box {
display: flex;
align-items: center;
gap: 12rpx;
}
.amount-sign {
font-size: 34rpx;
font-weight: 600;
&.sign-in {
color: #16a34a;
}
&.sign-out {
color: $color-danger;
}
}
.amount-input {
flex: 1;
font-size: 40rpx;
font-weight: 600;
color: $color-text;
}
.chip-line {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.mini-chip {
padding: 10rpx 26rpx;
border-radius: $radius-full;
background: #f1f5f9;
font-size: 24rpx;
color: $color-text-secondary;
&.active {
background: $color-primary-bg;
color: $color-primary-dark;
font-weight: 500;
}
}
.settle-card {
@include card;
margin-top: 24rpx;
padding: 26rpx 28rpx;
}
.settle-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.settle-title {
@include section-title;
}
.settle-sum {
font-size: 23rpx;
color: $color-primary;
}
.settle-tip {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
color: $color-text-muted;
line-height: 1.5;
}
.settle-loading {
padding: 24rpx 0;
font-size: 24rpx;
color: $color-text-muted;
}
.bill-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 0;
border-bottom: 1rpx solid $color-divider;
}
.bill-check {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
border-radius: $radius-sm;
border: 2rpx solid #cbd5e1;
display: flex;
align-items: center;
justify-content: center;
&.on {
background: $color-primary;
border-color: $color-primary;
}
}
.bill-main {
flex: 1;
min-width: 0;
.bill-no {
display: block;
font-size: 26rpx;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bill-sub {
display: block;
margin-top: 4rpx;
font-size: 21rpx;
color: $color-text-muted;
}
}
.bill-input {
width: 160rpx;
flex-shrink: 0;
height: 60rpx;
padding: 0 16rpx;
border-radius: $radius-sm;
background: #f1f5f9;
font-size: 25rpx;
color: $color-text;
text-align: right;
}
.bill-balance {
flex-shrink: 0;
font-size: 25rpx;
color: $color-text-secondary;
}
.settle-foot {
display: flex;
gap: 32rpx;
padding-top: 18rpx;
}
.foot-link {
font-size: 24rpx;
color: $color-primary;
}
.footer {
margin-top: 32rpx;
}
.btn-line {
&.gap {
margin-top: 20rpx;
}
}
.del-line {
display: flex;
justify-content: center;
padding: 26rpx 0 6rpx;
font-size: 24rpx;
color: $color-danger;
}
.view-settles {
margin-top: 32rpx;
@include card;
padding: 24rpx 28rpx;
.vs-title {
display: block;
margin-bottom: 8rpx;
font-size: 28rpx;
font-weight: 600;
color: $color-text;
}
}
.vs-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
}
.vs-no {
flex: 1;
min-width: 0;
font-size: 25rpx;
color: $color-text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vs-amount {
font-size: 26rpx;
font-weight: 600;
color: #16a34a;
}
}
</style>
+520
View File
@@ -0,0 +1,520 @@
<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>