409 lines
9.4 KiB
Vue
409 lines
9.4 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view v-if="loading && !profile" class="state-box">
|
||
<text class="state-text">加载中...</text>
|
||
</view>
|
||
|
||
<block v-else-if="profile">
|
||
<view class="hero">
|
||
<view class="hero-top">
|
||
<text class="hero-name">{{ name }}</text>
|
||
<u-tag :text="partnerTypeLabel(partnerType)" type="primary" size="mini" />
|
||
</view>
|
||
<text class="hero-sub">{{ [profile.contact_person, profile.contact_phone].filter(Boolean).join(' · ') || '未填联系人' }}</text>
|
||
<view class="hero-grid">
|
||
<view class="hero-cell">
|
||
<text class="cell-label">{{ isSupplier ? '未付应付' : '未收应收' }}</text>
|
||
<text class="cell-value owe">¥{{ moneyText(openAmount) }}</text>
|
||
</view>
|
||
<view class="hero-cell">
|
||
<text class="cell-label">{{ isSupplier ? '累计应付' : '累计应收' }}</text>
|
||
<text class="cell-value">¥{{ moneyText(totalAmount) }}</text>
|
||
</view>
|
||
<view class="hero-cell">
|
||
<text class="cell-label">账期</text>
|
||
<text class="cell-value">{{ Number(profile.payment_days) > 0 ? profile.payment_days + ' 天' : '现结' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="block-head">
|
||
<text class="block-title">基础资料</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">等级 / 类型</text>
|
||
<text class="line-value">{{ join([profile.supplier_level || profile.customer_level, profile.supplier_type || profile.customer_type]) }}</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">联系电话</text>
|
||
<text class="line-value link" @tap="callPhone">{{ dash(profile.contact_phone) }}</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">邮箱</text>
|
||
<text class="line-value">{{ dash(profile.contact_email) }}</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">地址</text>
|
||
<text class="line-value">{{ dash(profile.address) }}</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">开票抬头</text>
|
||
<text class="line-value">{{ join([profile.invoice_title, profile.tax_number]) }}</text>
|
||
</view>
|
||
<view class="line">
|
||
<text class="line-label">收款账户</text>
|
||
<text class="line-value">{{ join([profile.bank_name, profile.bank_account]) }}</text>
|
||
</view>
|
||
<view v-if="Number(profile.credit_limit) > 0" class="line">
|
||
<text class="line-label">信用额度</text>
|
||
<text class="line-value">¥{{ moneyText(profile.credit_limit) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="block-head">
|
||
<text class="block-title">未结账单({{ bills.length }} 张)</text>
|
||
<text class="block-tip" @tap="goAllBills">全部 {{ billTotal }} 张</text>
|
||
</view>
|
||
<view v-if="!bills.length" class="empty-line">
|
||
<text>没有未结账单</text>
|
||
</view>
|
||
<view v-for="b in bills" :key="b.id" class="line tap" @tap="goBill(b)">
|
||
<view class="line-label grow">
|
||
<text>{{ b.source_doc_no || `账单#${b.id}` }}</text>
|
||
<text class="sub">{{ b.bill_date }}{{ b.due_date ? ' · 到期 ' + b.due_date : '' }}</text>
|
||
</view>
|
||
<view class="line-value right">
|
||
<text class="bal">¥{{ moneyText(b.balance) }}</text>
|
||
<text v-if="b.overdue_days > 0" class="over">逾期 {{ b.overdue_days }} 天</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="footer-space"></view>
|
||
|
||
<view class="actions">
|
||
<view class="act" @tap="goDocs">
|
||
<text>历史单据</text>
|
||
</view>
|
||
<view class="act primary" @tap="goFund">
|
||
<text>{{ isSupplier ? '去付款' : '去收款' }}</text>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
/**
|
||
* 客商详情:档案 + 未结账单 + 一键收付款。
|
||
* 余额口径直接取往来账(bill)与对账汇总,和 Web 端「应收应付」页一致,
|
||
* 端上不另算一套,避免业务在手机上看到的欠款跟财务对不上。
|
||
*/
|
||
import { computed, ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import {
|
||
fetchBills,
|
||
fetchPartnerSummary,
|
||
getPartner,
|
||
moneyText,
|
||
partnerTypeLabel
|
||
} from '@/api/erp.js'
|
||
|
||
const profile = ref(null)
|
||
const bills = ref([])
|
||
const billTotal = ref(0)
|
||
const summary = ref({})
|
||
const loading = ref(false)
|
||
const partnerId = ref(0)
|
||
const partnerType = ref('customer')
|
||
|
||
const isSupplier = computed(() => partnerType.value === 'supplier')
|
||
const name = computed(() => {
|
||
const p = profile.value || {}
|
||
return p.supplier_name || p.customer_name || p.name || '未命名客商'
|
||
})
|
||
const openAmount = computed(() => {
|
||
const s = summary.value || {}
|
||
return Number(isSupplier.value ? s.pay_open : s.recv_open) || 0
|
||
})
|
||
const totalAmount = computed(() => {
|
||
const s = summary.value || {}
|
||
return Number(isSupplier.value ? s.pay_total : s.recv_total) || 0
|
||
})
|
||
|
||
onLoad(async (query = {}) => {
|
||
partnerId.value = Number(query.id) || 0
|
||
partnerType.value = String(query.partner_type) === 'supplier' ? 'supplier' : 'customer'
|
||
uni.setNavigationBarTitle({ title: partnerTypeLabel(partnerType.value) })
|
||
await load()
|
||
})
|
||
|
||
async function load() {
|
||
if (!partnerId.value) {
|
||
uni.showToast({ title: '缺少客商 ID', icon: 'none' })
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const [p, b, rows] = await Promise.all([
|
||
getPartner(partnerId.value, partnerType.value),
|
||
fetchBills({ partnerId: partnerId.value, partnerType: partnerType.value, settleState: 'open', pageSize: 10 }),
|
||
fetchPartnerSummary(partnerType.value)
|
||
])
|
||
profile.value = p
|
||
bills.value = b.list || []
|
||
billTotal.value = b.total || 0
|
||
// 对账汇总按 partner_type:id 归并,这里只取当前这一家
|
||
summary.value = rows.find((r) => Number(r.partner_id) === partnerId.value) || {}
|
||
} catch (e) {
|
||
uni.showToast({ title: '客商信息加载失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function goFund() {
|
||
const fundType = isSupplier.value ? 2 : 1
|
||
uni.navigateTo({
|
||
url: `/pages/erp/finance/fund-edit?fund_type=${fundType}&partner_id=${partnerId.value}&partner_name=${encodeURIComponent(name.value)}`
|
||
})
|
||
}
|
||
|
||
function goDocs() {
|
||
uni.navigateTo({ url: `/pages/erp/document/index?partner_id=${partnerId.value}` })
|
||
}
|
||
|
||
function goAllBills() {
|
||
const billType = isSupplier.value ? 2 : 1
|
||
uni.navigateTo({ url: `/pages/erp/finance/bill?bill_type=${billType}&partner_id=${partnerId.value}` })
|
||
}
|
||
|
||
function goBill(bill) {
|
||
uni.navigateTo({ url: `/pages/erp/finance/bill-detail?id=${bill.id}` })
|
||
}
|
||
|
||
function callPhone() {
|
||
const phone = String(profile.value?.contact_phone || '').trim()
|
||
if (!phone) return
|
||
uni.makePhoneCall({ phoneNumber: phone, fail: () => {} })
|
||
}
|
||
|
||
function dash(v) {
|
||
const s = String(v ?? '').trim()
|
||
return s || '—'
|
||
}
|
||
|
||
function join(arr) {
|
||
const s = (arr || []).filter(Boolean).join(' / ')
|
||
return s || '—'
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
background: $color-bg-page;
|
||
padding-top: 20rpx;
|
||
}
|
||
|
||
.hero {
|
||
margin: 0 $page-padding-x 24rpx;
|
||
padding: 32rpx;
|
||
border-radius: $radius-lg;
|
||
background: $hero-gradient;
|
||
color: #ffffff;
|
||
box-shadow: $shadow-float;
|
||
}
|
||
|
||
.hero-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.hero-name {
|
||
flex: 1;
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.hero-sub {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 24rpx;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.hero-grid {
|
||
display: flex;
|
||
margin-top: 28rpx;
|
||
padding-top: 22rpx;
|
||
border-top: 1rpx solid rgba(255, 255, 255, 0.22);
|
||
}
|
||
|
||
.hero-cell {
|
||
flex: 1;
|
||
|
||
.cell-label {
|
||
display: block;
|
||
font-size: 22rpx;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.cell-value {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
|
||
&.owe {
|
||
color: #fecaca;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card {
|
||
@include card;
|
||
margin: 0 $page-padding-x 24rpx;
|
||
padding: 24rpx 28rpx;
|
||
}
|
||
|
||
.block-head {
|
||
@include block-head;
|
||
}
|
||
|
||
.block-title {
|
||
@include section-title;
|
||
}
|
||
|
||
.block-tip {
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.line {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
padding: 16rpx 0;
|
||
border-bottom: 1rpx solid $color-divider;
|
||
font-size: 26rpx;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.tap:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.line-label {
|
||
color: $color-text-secondary;
|
||
flex-shrink: 0;
|
||
|
||
&.grow {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
.sub {
|
||
display: block;
|
||
margin-top: 4rpx;
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.line-value {
|
||
flex: 1;
|
||
text-align: right;
|
||
color: $color-text;
|
||
word-break: break-all;
|
||
|
||
&.link {
|
||
color: $color-primary;
|
||
}
|
||
}
|
||
|
||
.right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.bal {
|
||
font-size: 27rpx;
|
||
font-weight: 500;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.over {
|
||
font-size: 21rpx;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.empty-line {
|
||
padding: 40rpx 0;
|
||
text-align: center;
|
||
font-size: 25rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.footer-space {
|
||
height: calc(150rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.actions {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
gap: 16rpx;
|
||
padding: 18rpx $page-padding-x calc(18rpx + env(safe-area-inset-bottom));
|
||
background: $color-card;
|
||
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.06);
|
||
@include dialog-responsive;
|
||
}
|
||
|
||
.act {
|
||
flex: 1;
|
||
height: 76rpx;
|
||
line-height: 76rpx;
|
||
text-align: center;
|
||
border-radius: $radius-full;
|
||
background: $color-bg-page;
|
||
font-size: 26rpx;
|
||
color: $color-text-secondary;
|
||
|
||
&.primary {
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.85;
|
||
}
|
||
}
|
||
|
||
.state-box {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 140rpx 0;
|
||
}
|
||
|
||
.state-text {
|
||
font-size: 28rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
</style>
|