Files
cz-web/src/views/project/cpns/ProjectFinance.vue
2026-09-30 08:04:16 +08:00

409 lines
12 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.
<script setup lang="ts">
import type { FileInfo } from '@/types/api'
import type { ProjectBill } from '@/types/business/project'
import { fileLabelOf, fileToneOf } from '@/utils/fileMeta'
import { formatFileSize, formatMoney } from '@/utils/format'
interface Props {
paymentPlans: ProjectBill[]
invoiceRecords: FileInfo[]
contractFile?: FileInfo
}
defineProps<Props>()
const emit = defineEmits<{
pay: [billId: string]
preview: [file: FileInfo]
download: [file: FileInfo]
}>()
/**
* 是否可跳收银台支付。
*
* <p>用「待支付 + id 是纯数字」判断,而不是过去的「来源是账单」——本栈的支付口径就是
* **计划 id**(收银台 `?ids=` 与 `vm_finance_payment.bill_id` 同一口径),计划卡本就该能付;
* 数字校验只是防止空 id 拼出 `?ids=`。</p>
*/
function canPay(bill: ProjectBill): boolean {
return bill.status === '待支付' && /^\d+$/.test(bill.id)
}
/**
* 三态配色类(设计稿口径,挂到 `.project-finance__plan` 上,由后代选择器分发):
* 已支付 = 绿、待支付 = 橙、未到期 = 灰,**徽标实底 + 金额文字同色**。
*/
const TONE_CLASS: Record<ProjectBill['status'], string> = {
'已支付': 'is-paid',
'待支付': 'is-pending',
'未到期': 'is-idle',
}
/** 文件行副标题(设计稿:「2.4MB · 上传于 2026-8-17 17:00」) */
function fileMeta(file: FileInfo): string {
const size = file.size ? formatFileSize(file.size) : ''
const time = file.uploadTime ? `上传于 ${file.uploadTime}` : ''
return [size, time].filter(Boolean).join(' · ')
}
</script>
<template>
<section class="project-finance">
<!-- 付款计划(数据源 vm_bill_plan):卡内三态 = 已支付 / 待支付 / 未到期 -->
<h2 class="project-finance__title">
付款计划
</h2>
<div class="project-finance__plans">
<article
v-for="bill in paymentPlans"
:key="bill.id || bill.planName"
class="project-finance__plan"
:class="TONE_CLASS[bill.status]"
>
<div class="project-finance__plan-head">
<span class="project-finance__plan-name">
{{ bill.planName }}<template v-if="bill.periodRatio"> 比例{{ bill.periodRatio }}%</template>
</span>
<span class="project-finance__badge">{{ bill.status }}</span>
</div>
<div class="project-finance__plan-body">
<div class="project-finance__fields">
<!-- 交易流水号只在已支付时出现(设计稿:已支付那张卡多一行流水号) -->
<p v-if="bill.status === '已支付' && bill.tradeNo" class="project-finance__field">
交易流水号:{{ bill.tradeNo }}
</p>
<p class="project-finance__field">
账单号:{{ bill.billNo || '—' }}
</p>
<p class="project-finance__field">
关联项目:{{ bill.projectName || '—' }}
</p>
</div>
<div class="project-finance__amount">
¥{{ formatMoney(bill.amount) }}
</div>
</div>
<!-- 支付口径 = 计划 id(与收银台 ?ids= / vm_finance_payment.bill_id 一致) -->
<button
v-if="canPay(bill)"
class="project-finance__pay"
type="button"
@click="emit('pay', bill.id)"
>
立即支付
</button>
</article>
<p v-if="paymentPlans.length === 0" class="project-finance__empty text-secondary">
暂无付款计划
</p>
</div>
<!-- 发票记录 -->
<h2 class="project-finance__title">
发票记录
</h2>
<ul class="project-finance__files">
<li
v-for="(file, index) in invoiceRecords"
:key="`invoice-${index}`"
class="project-finance__file"
>
<span
class="project-finance__file-badge"
:style="{ color: fileToneOf(file), backgroundColor: `${fileToneOf(file)}1a` }"
>
{{ fileLabelOf(file) }}
</span>
<div class="project-finance__file-info">
<span class="project-finance__file-name" :title="file.name">{{ file.name }}</span>
<span class="project-finance__file-meta">{{ fileMeta(file) }}</span>
</div>
<div class="project-finance__file-actions">
<button class="project-finance__act" type="button" @click="emit('preview', file)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M1.5 8s2.4-4.5 6.5-4.5S14.5 8 14.5 8s-2.4 4.5-6.5 4.5S1.5 8 1.5 8Z"
stroke="currentColor"
stroke-width="1.2"
/>
<circle cx="8" cy="8" r="1.8" stroke="currentColor" stroke-width="1.2" />
</svg>
预览
</button>
<button class="project-finance__act" type="button" @click="emit('download', file)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 2v8m0 0 3-3m-3 3L5 7M3 13h10"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
下载
</button>
</div>
</li>
<li v-if="invoiceRecords.length === 0" class="project-finance__empty text-secondary">
暂无发票记录
</li>
</ul>
<!-- 合同文件 -->
<h2 class="project-finance__title">
合同文件
</h2>
<div v-if="contractFile" class="project-finance__file">
<span
class="project-finance__file-badge"
:style="{ color: fileToneOf(contractFile), backgroundColor: `${fileToneOf(contractFile)}1a` }"
>
{{ fileLabelOf(contractFile) }}
</span>
<div class="project-finance__file-info">
<span class="project-finance__file-name" :title="contractFile.name">{{ contractFile.name }}</span>
<span class="project-finance__file-meta">{{ fileMeta(contractFile) }}</span>
</div>
<div class="project-finance__file-actions">
<button class="project-finance__act" type="button" @click="emit('preview', contractFile)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M1.5 8s2.4-4.5 6.5-4.5S14.5 8 14.5 8s-2.4 4.5-6.5 4.5S1.5 8 1.5 8Z"
stroke="currentColor"
stroke-width="1.2"
/>
<circle cx="8" cy="8" r="1.8" stroke="currentColor" stroke-width="1.2" />
</svg>
预览
</button>
<button class="project-finance__act" type="button" @click="emit('download', contractFile)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 2v8m0 0 3-3m-3 3L5 7M3 13h10"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
下载
</button>
</div>
</div>
<p v-else class="project-finance__empty text-secondary">
暂无合同文件
</p>
</section>
</template>
<style lang="less" scoped>
.project-finance {
.flex(column, flex-start, stretch);
gap: @spacing-base;
padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
// 区块标题:与「项目进度/项目文件/售后信息」同款(16px/600 + 左侧蓝色竖条)
&__title {
.section-title();
}
// 只读回落态(后端未重启、响应无 bills)的说明行
&__note {
font-size: @font-size-sm;
}
// ---------- 付款计划 ----------
// 设计稿:卡片**竖排单列、定宽左对齐**(不是横排多列网格)
&__plans {
.flex(column, flex-start, flex-start);
gap: @spacing-sm;
}
&__plan {
.flex(column, flex-start, stretch);
gap: @spacing-sm;
width: @finance-card-width;
padding: 12px;
border-radius: @finance-card-radius;
background: @white;
// 设计稿用阴影立卡片、**不描边**;阴影比页面默认更淡,只做「浮起」暗示
box-shadow: @finance-card-shadow;
// 三态配色(徽标实底白字 + 金额同色)
&.is-paid {
.project-finance__badge {
background: @finance-paid;
}
.project-finance__amount {
color: @finance-paid;
}
}
&.is-pending {
.project-finance__badge {
background: @finance-pending;
}
.project-finance__amount {
color: @finance-pending-amount;
}
}
&.is-idle {
.project-finance__badge {
background: @finance-idle;
}
.project-finance__amount {
color: @finance-idle;
}
}
}
&__plan-head {
.flex(row, space-between, flex-start);
gap: @spacing-sm;
}
&__plan-name {
font-size: @font-size-base;
font-weight: 700;
letter-spacing: 0.8px;
color: @heading-color;
.ellipsis();
}
// 状态徽标:实底白字的小圆角块(设计 48×22 / 圆角 4 / 10 设计px 字号)
&__badge {
.flex(row, center, center);
flex-shrink: 0;
padding: 2px @spacing-sm;
border-radius: @radius-sm;
font-size: 11px;
line-height: 16px;
color: @white;
}
&__plan-body {
.flex(row, space-between, flex-start);
gap: @spacing-base;
}
&__fields {
.flex(column, flex-start, stretch);
gap: 4px;
min-width: 0;
}
&__field {
font-size: @font-size-sm;
line-height: 17px;
color: @finance-meta-color;
.ellipsis();
}
// 金额:设计稿在卡片**右侧**、字号最大且随状态变色(不是左下角恒定深色)
&__amount {
flex-shrink: 0;
font-size: @font-size-xl;
font-weight: 700;
line-height: 28px;
white-space: nowrap;
}
// 「立即支付」:设计稿是卡内**满宽**金色胶囊(不是右下角的小蓝按钮)
&__pay {
height: 22px;
border: none;
border-radius: @radius-round;
background: @finance-pending;
color: @white;
font-size: @font-size-sm;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: @finance-cta-hover;
}
}
// ---------- 发票 / 合同文件 ----------
&__files {
.flex(column, flex-start, stretch);
gap: @spacing-sm;
margin: 0;
padding: 0;
list-style: none;
}
// 设计稿:整行通铺浅灰底、无描边无圆角(区别于卡片)
&__file {
.flex(row, space-between, center);
gap: @spacing-sm;
padding: @spacing-sm @spacing-base;
background: @finance-row-bg;
}
// 文件类型标识:按后缀着色的小方块(与需求提报页附件行同一套色板)
&__file-badge {
.flex(row, center, center);
flex-shrink: 0;
width: 34px;
height: 34px;
border-radius: @radius-sm;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2px;
}
&__file-info {
.flex(column, flex-start, stretch);
flex: 1;
gap: 2px;
min-width: 0;
}
&__file-name {
font-size: @font-size-base;
color: @heading-color;
.ellipsis();
}
&__file-meta {
font-size: @font-size-sm;
color: @text-color-secondary;
}
&__file-actions {
.flex(row, flex-end, center);
gap: @spacing-base;
flex-shrink: 0;
}
&__act {
.flex(row, center, center);
gap: 4px;
font-size: @font-size-sm;
color: @text-color;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: @primary-color;
}
}
&__empty {
font-size: @font-size-base;
}
}
</style>