feat 搭建架构骨架

This commit is contained in:
Ironsp
2026-09-24 09:09:47 +08:00
parent dd9f6b77a4
commit 7a2efa3fea
180 changed files with 18967 additions and 9 deletions

View File

@@ -0,0 +1,275 @@
<script setup lang="ts">
import { StatusTag } from '@/components'
import type { FileInfo } from '@/types/api'
import type { ProjectBill } from '@/types/business/project'
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]
}>()
const BILL_VARIANTS: Record<ProjectBill['status'], 'primary' | 'success' | 'warning' | 'danger' | 'info'> = {
'已支付': 'success',
'待支付': 'warning',
'未到期': 'info',
}
</script>
<template>
<section class="project-finance">
<!-- 付款计划 -->
<h2 class="project-finance__title">
付款计划
</h2>
<div class="project-finance__plans">
<article
v-for="bill in paymentPlans"
:key="bill.id"
class="project-finance__plan"
>
<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>
<button
v-if="bill.status === '待支付'"
class="project-finance__pay"
@click="emit('pay', bill.id)"
>
立即支付
</button>
</div>
</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"
>
<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">
{{ formatFileSize(file.size) }} · {{ file.uploadTime }}
</span>
</div>
<div class="project-finance__file-actions">
<button class="project-finance__link" @click="emit('preview', file)">
预览
</button>
<button class="project-finance__link" @click="emit('download', file)">
下载
</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__contract">
<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">
{{ formatFileSize(contractFile.size) }} · {{ contractFile.uploadTime }}
</span>
</div>
<div class="project-finance__file-actions">
<button class="project-finance__link" @click="emit('preview', contractFile)">
预览
</button>
<button class="project-finance__link" @click="emit('download', contractFile)">
下载
</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);
gap: @spacing-base;
padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
&__title {
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
}
&__plans {
.flex(row, flex-start);
flex-wrap: wrap;
gap: @spacing-base;
}
&__plan {
.flex(column, flex-start);
gap: @spacing-sm;
width: 320px;
padding: @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
}
&__plan-head {
.flex(row, space-between, center);
gap: @spacing-sm;
}
&__plan-name {
font-size: @font-size-base;
font-weight: 600;
color: @heading-color;
}
&__plan-meta {
.flex(column, flex-start);
gap: @spacing-xs;
}
&__field {
.flex(row, flex-start);
gap: @spacing-sm;
font-size: @font-size-sm;
}
&__label {
flex-shrink: 0;
}
&__value {
color: @text-color;
.ellipsis();
}
&__amount {
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();
}
&__pay {
height: 28px;
padding: 0 @spacing-base;
border: none;
border-radius: @radius-sm;
background: @primary-color;
color: @white;
font-size: @font-size-sm;
transition: background 0.2s;
&:hover {
background: @primary-hover;
}
}
&__files {
.flex(column, flex-start);
gap: @spacing-sm;
}
&__file,
&__contract {
.flex(row, space-between);
gap: @spacing-base;
padding: @spacing-sm @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
}
&__file-info {
.flex(column, flex-start);
gap: 2px;
min-width: 0;
}
&__file-name {
font-size: @font-size-base;
color: @heading-color;
.ellipsis();
}
&__file-meta {
font-size: @font-size-sm;
}
&__file-actions {
.flex(row, center);
gap: @spacing-sm;
flex-shrink: 0;
}
&__link {
font-size: @font-size-sm;
color: @primary-color;
transition: opacity 0.2s;
&:hover {
opacity: 0.75;
}
}
&__empty {
font-size: @font-size-base;
}
}
</style>