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

455 lines
9.9 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 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>