项目中心-支付模块
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { StatusTag } from '@/components'
|
||||
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 {
|
||||
@@ -18,54 +18,82 @@ const emit = defineEmits<{
|
||||
download: [file: FileInfo]
|
||||
}>()
|
||||
|
||||
const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
'已支付': 'success',
|
||||
'待支付': 'warning',
|
||||
'未到期': 'info',
|
||||
/**
|
||||
* 是否可跳收银台支付。
|
||||
*
|
||||
* <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"
|
||||
: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 }}</span>
|
||||
<StatusTag :label="bill.status" :variant="BILL_VARIANTS[bill.status]" />
|
||||
</div>
|
||||
<div class="project-finance__plan-meta">
|
||||
<div class="project-finance__field">
|
||||
<span class="project-finance__label text-secondary">关联项目</span>
|
||||
<span class="project-finance__value">{{ bill.projectName }}</span>
|
||||
</div>
|
||||
<div class="project-finance__field">
|
||||
<span class="project-finance__label text-secondary">账单号</span>
|
||||
<span class="project-finance__value">{{ bill.billNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-finance__amount">
|
||||
¥{{ formatMoney(bill.amount) }}
|
||||
</div>
|
||||
<div class="project-finance__plan-foot">
|
||||
<span v-if="bill.status === '已支付' && bill.tradeNo" class="project-finance__trade text-secondary">
|
||||
交易流水号:{{ bill.tradeNo }}
|
||||
<span class="project-finance__plan-name">
|
||||
{{ bill.planName }}<template v-if="bill.periodRatio"> 比例{{ bill.periodRatio }}%</template>
|
||||
</span>
|
||||
<button
|
||||
v-if="bill.status === '待支付'"
|
||||
class="project-finance__pay"
|
||||
@click="emit('pay', bill.id)"
|
||||
>
|
||||
立即支付
|
||||
</button>
|
||||
<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">
|
||||
暂无付款计划
|
||||
@@ -82,17 +110,38 @@ const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warn
|
||||
: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 text-secondary">
|
||||
{{ file.size ? formatFileSize(file.size) + ' · ' : '' }}{{ file.uploadTime }}
|
||||
</span>
|
||||
<span class="project-finance__file-meta">{{ fileMeta(file) }}</span>
|
||||
</div>
|
||||
<div class="project-finance__file-actions">
|
||||
<button class="project-finance__link" @click="emit('preview', file)">
|
||||
<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__link" @click="emit('download', file)">
|
||||
<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>
|
||||
@@ -106,18 +155,39 @@ const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warn
|
||||
<h2 class="project-finance__title">
|
||||
合同文件
|
||||
</h2>
|
||||
<div v-if="contractFile" class="project-finance__contract">
|
||||
<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 text-secondary">
|
||||
{{ contractFile.size ? formatFileSize(contractFile.size) + ' · ' : '' }}{{ contractFile.uploadTime }}
|
||||
</span>
|
||||
<span class="project-finance__file-meta">{{ fileMeta(contractFile) }}</span>
|
||||
</div>
|
||||
<div class="project-finance__file-actions">
|
||||
<button class="project-finance__link" @click="emit('preview', contractFile)">
|
||||
<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__link" @click="emit('download', contractFile)">
|
||||
<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>
|
||||
@@ -137,107 +207,166 @@ const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warn
|
||||
background: @white;
|
||||
box-shadow: @shadow-base;
|
||||
|
||||
// 区块标题:与「项目进度/项目文件/售后信息」同款(16px/600 + 左侧蓝色竖条)
|
||||
&__title {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: 600;
|
||||
color: @heading-color;
|
||||
.section-title();
|
||||
}
|
||||
|
||||
// 只读回落态(后端未重启、响应无 bills)的说明行
|
||||
&__note {
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
// ---------- 付款计划 ----------
|
||||
|
||||
// 设计稿:卡片**竖排单列、定宽左对齐**(不是横排多列网格)
|
||||
&__plans {
|
||||
.flex(row, flex-start);
|
||||
flex-wrap: wrap;
|
||||
gap: @spacing-base;
|
||||
.flex(column, flex-start, flex-start);
|
||||
gap: @spacing-sm;
|
||||
}
|
||||
|
||||
&__plan {
|
||||
.flex(column, flex-start, stretch);
|
||||
gap: @spacing-sm;
|
||||
width: 320px;
|
||||
padding: @spacing-base;
|
||||
border: 1px solid @border-color;
|
||||
border-radius: @radius-base;
|
||||
background: @bg-color-light;
|
||||
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, center);
|
||||
.flex(row, space-between, flex-start);
|
||||
gap: @spacing-sm;
|
||||
}
|
||||
|
||||
&__plan-name {
|
||||
font-size: @font-size-base;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: @heading-color;
|
||||
.ellipsis();
|
||||
}
|
||||
|
||||
&__plan-meta {
|
||||
// 状态徽标:实底白字的小圆角块(设计 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: @spacing-xs;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__field {
|
||||
.flex(row, flex-start);
|
||||
gap: @spacing-sm;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: @text-color;
|
||||
line-height: 17px;
|
||||
color: @finance-meta-color;
|
||||
.ellipsis();
|
||||
}
|
||||
|
||||
// 金额:设计稿在卡片**右侧**、字号最大且随状态变色(不是左下角恒定深色)
|
||||
&__amount {
|
||||
flex-shrink: 0;
|
||||
font-size: @font-size-xl;
|
||||
font-weight: 600;
|
||||
color: @heading-color;
|
||||
}
|
||||
|
||||
&__plan-foot {
|
||||
.flex(row, space-between, center);
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
&__trade {
|
||||
font-size: @font-size-sm;
|
||||
.ellipsis();
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 「立即支付」:设计稿是卡内**满宽**金色胶囊(不是右下角的小蓝按钮)
|
||||
&__pay {
|
||||
height: 28px;
|
||||
padding: 0 @spacing-base;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: @radius-sm;
|
||||
background: @primary-color;
|
||||
border-radius: @radius-round;
|
||||
background: @finance-pending;
|
||||
color: @white;
|
||||
font-size: @font-size-sm;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: @primary-hover;
|
||||
background: @finance-cta-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 发票 / 合同文件 ----------
|
||||
|
||||
&__files {
|
||||
.flex(column, flex-start, stretch);
|
||||
gap: @spacing-sm;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__file,
|
||||
&__contract {
|
||||
.flex(row, space-between);
|
||||
gap: @spacing-base;
|
||||
// 设计稿:整行通铺浅灰底、无描边无圆角(区别于卡片)
|
||||
&__file {
|
||||
.flex(row, space-between, center);
|
||||
gap: @spacing-sm;
|
||||
padding: @spacing-sm @spacing-base;
|
||||
border: 1px solid @border-color;
|
||||
border-radius: @radius-base;
|
||||
background: @bg-color-light;
|
||||
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;
|
||||
}
|
||||
@@ -250,21 +379,25 @@ const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warn
|
||||
|
||||
&__file-meta {
|
||||
font-size: @font-size-sm;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
&__file-actions {
|
||||
.flex(row, center);
|
||||
gap: @spacing-sm;
|
||||
.flex(row, flex-end, center);
|
||||
gap: @spacing-base;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__link {
|
||||
&__act {
|
||||
.flex(row, center, center);
|
||||
gap: 4px;
|
||||
font-size: @font-size-sm;
|
||||
color: @primary-color;
|
||||
transition: opacity 0.2s;
|
||||
color: @text-color;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user