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

+408
View File
@@ -0,0 +1,408 @@
<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>