Files
2026-10-19 01:13:17 +08:00

409 lines
9.4 KiB
Vue
Raw Permalink 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 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>