409 lines
12 KiB
Vue
409 lines
12 KiB
Vue
<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>
|