feat 合同中心

This commit is contained in:
Ironsp
2026-10-09 10:32:49 +08:00
parent f73103a2a6
commit bb897ccc94
12 changed files with 211 additions and 70 deletions

View File

@@ -12,18 +12,18 @@ import { request, serializeQuery } from '@/utils/request'
import { formatDate } from '@/utils/format'
// ─────────────────────────────────────────────────────────────────────────────
// GET /web/requirementPool/list → ProjectRequirementUserController#list
// → ProjectRequirementServiceImpl#queryUserPageList
// { requirementPage: { total, rows: RdProjectVo[] }, requirementStatistics: null }
// key → like projectName OR projectNo OR requirementDesc
// pageNum/pageSize → PageQuery
// GET /web/requirementPool/list → 控制器 ProjectRequirementUserController#list
// → 服务层 ProjectRequirementServiceImpl#queryUserPageList
// 返回 { requirementPage: { total, rows: RdProjectVo[] }, requirementStatistics: null }
// key → 模糊匹配 projectName / projectNo / requirementDesc
// pageNum/pageSize → 分页参数 PageQuery
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// GET /web/requirementPool/getFilterConditions
// → ProjectRequirementUserController#getFilterConditions
// → ProjectRequirementServiceImpl#getFilterConditions
// businessTypeCounts: [{ businessTypeId, businessTypeLabel, count }] }
// → 控制器 ProjectRequirementUserController#getFilterConditions
// → 服务层 ProjectRequirementServiceImpl#getFilterConditions
// 业务类型统计 businessTypeCounts: [{ businessTypeId, businessTypeLabel, count }]
// ─────────────────────────────────────────────────────────────────────────────
function pad(value: number): string {
@@ -87,12 +87,12 @@ export function getCompletedProjects(): Promise<RdProjectVo[]> {
}
// ─────────────────────────────────────────────────────────────────────────────
// GET /web/requirementPool/get/progress/{id} → ProjectRequirementProgressVo
// GET /web/requirementPool/get/files/{id} → ProjectRequirementFilesVo
// GET /web/requirementPool/get/finance/{id} → ProjectRequirementFinanceVo
// GET /web/requirementPool/get/afterSales/{id} → ProjectRequirementAfterSalesVo
// → ProjectRequirementUserController#getProgress / #getFiles / #getFinance / #getAfterSales
// → ProjectRequirementServiceImpl#queryProgress / queryFiles / queryFinance / queryAfterSales
// GET /web/requirementPool/get/progress/{id} → 进度详情 ProjectRequirementProgressVo
// GET /web/requirementPool/get/files/{id} → 文件资料 ProjectRequirementFilesVo
// GET /web/requirementPool/get/finance/{id} → 财务信息 ProjectRequirementFinanceVo
// GET /web/requirementPool/get/afterSales/{id} → 售后信息 ProjectRequirementAfterSalesVo
// → 控制器 ProjectRequirementUserController#getProgress / #getFiles / #getFinance / #getAfterSales
// → 服务层 ProjectRequirementServiceImpl#queryProgress / queryFiles / queryFinance / queryAfterSales
//
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import type { FileInfo } from '@/types/api'
import { fileLabelOf, fileToneOf } from '@/utils/fileMeta'
import { formatFileSize } from '@/utils/format'
interface Props {
files: FileInfo[]
showActions?: boolean
/** 左侧文件类型徽标(设计稿 #28 附件行有,#29/#27 同款) */
showBadge?: boolean
}
withDefaults(defineProps<Props>(), { showActions: true })
withDefaults(defineProps<Props>(), { showActions: true, showBadge: false })
const emit = defineEmits<{
preview: [file: FileInfo]
@@ -19,6 +22,13 @@ const emit = defineEmits<{
<template>
<ul v-if="files.length > 0" class="file-list">
<li v-for="(file, index) in files" :key="`${file.name}-${index}`" class="file-list__item">
<span
v-if="showBadge"
class="file-list__badge"
:style="{ color: fileToneOf(file), backgroundColor: `${fileToneOf(file)}1a` }"
>
{{ fileLabelOf(file) }}
</span>
<div class="file-list__info">
<span class="file-list__name" :title="file.name">{{ file.name }}</span>
<span class="file-list__meta text-secondary">
@@ -54,6 +64,18 @@ const emit = defineEmits<{
background: @demand-upload-bg;
}
// 设计稿 #29/#27/#28 附件行:徽标 33.5×33.5。
&__badge {
.flex(row, center, center);
flex-shrink: 0;
width: 33.5px;
height: 33.5px;
border-radius: @radius-sm;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.2px;
}
&__info {
.flex(column, flex-start);
gap: 2px;

View File

@@ -139,6 +139,7 @@
@finance-meta-color: #6b7280;
@finance-row-bg: #f7f8fa;
@finance-paid: #16a34a;
@finance-paid-bg: #e7f7ec;
@finance-pending: #f59e0b;
@finance-pending-amount: #f59e0b;
@finance-idle: #b3bfc4;

View File

@@ -306,11 +306,12 @@ onMounted(() => {
<style lang="less" scoped>
.after-sales {
.flex(column, flex-start, stretch);
padding-bottom: 104px;
padding-bottom: 40px;
background: @white;
// 设计稿 el40 面包屑 y=116,header 底 72 → padding-top 44px。
&__breadcrumb {
padding-top: 41px;
padding-top: 44px;
font-size: 24px;
font-weight: 500;
line-height: 27px;
@@ -330,12 +331,14 @@ onMounted(() => {
color: @project-ink;
}
// 设计稿 #26:副标题文字 x=728~1185,中心 ≈ 957 ≈ 内容区中心 960(居中)。
&__subtitle {
margin-top: 8px;
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-label;
text-align: center;
}
&__card {
@@ -394,6 +397,11 @@ onMounted(() => {
&__field-head {
.flex(row, flex-start, center);
gap: 14px;
// 「附件上传」与 hint、按钮同行居左,不参与 __label 的居中对齐。
.after-sales__label {
text-align: left;
}
}
&__field-hint {
@@ -424,12 +432,13 @@ onMounted(() => {
}
}
// 设计稿 #26:字段标签水平居中(「售后类型*」x=931~988,中心 ≈ 输入框中心 960)。
&__label {
font-size: 13px;
font-weight: 500;
line-height: 17px;
color: @project-ink;
text-align: left;
text-align: center;
}
&__required {
@@ -509,19 +518,14 @@ onMounted(() => {
display: none;
}
// 设计稿#26:协议行 y=779~790、按钮同排,均在卡片3之后的正常流里
// (无吸底、无上边框分隔线)。撑满内容区宽度。
&__footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
.flex(row, space-between, center);
gap: @spacing-base;
height: 80px;
margin-top: 20px;
height: 38px;
box-sizing: border-box;
padding: 0 121px 0 170px;
border-top: 1px solid @project-divider;
background: @white;
}
&__footer-tip {
@@ -550,13 +554,15 @@ onMounted(() => {
gap: 8px;
flex-shrink: 0;
// 设计稿 #26:「申请售后」按钮盒 x=1775~1847(w=72)、y=772~798(h=26),
// 右边界 1847距内容区右 1850 = 3px。三个按钮同款规格、间距 8px。
:deep(.app-button) {
height: 38px;
padding: 0 18px;
border-radius: 8px;
height: 26px;
padding: 0 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
line-height: 18px;
line-height: 26px;
}
:deep(.app-button--default) {

View File

@@ -88,12 +88,19 @@ function onDeleteFile(): void {
<div class="project-after-sales__field">
<div class="project-after-sales__field-head">
<span class="project-after-sales__label">附件上传</span>
<span class="project-after-sales__field-hint">支持图片/文档/压缩包,大小不超过1G</span>
<div class="project-after-sales__field-head-left">
<span class="project-after-sales__label">附件上传</span>
<span class="project-after-sales__field-hint">支持图片/文档/压缩包,大小不超过1G</span>
</div>
<!--设计稿 #28 只读态仍画了「上传文档」按钮,用 disabled 表达不可上传 -->
<button class="project-after-sales__upload" type="button" disabled>
上传文档
</button>
</div>
<FileList
v-if="afterSales.attachments.length > 0"
:files="afterSales.attachments"
show-badge
@preview="previewFile"
@download="downloadFile"
@delete="onDeleteFile"
@@ -204,9 +211,34 @@ function onDeleteFile(): void {
gap: 20px;
}
// 设计稿 #28:标签行「附件上传」左对齐 + hint,「上传文档」右对齐 x=1768~1837。
&__field-head {
.flex(row, space-between, center);
gap: 14px;
}
&__field-head-left {
.flex(row, flex-start, center);
gap: 14px; // 149 − (70+65) = 14
min-width: 0;
}
// 设计稿:按钮 x=1768~1837(w=69),右边界距行右 1849.5 = 12.5px。
&__upload {
flex-shrink: 0;
height: 24px;
padding: 0 10.5px;
border: none;
border-radius: 6px;
background: @project-field-bg;
color: @project-cell;
font-size: 12px;
line-height: 24px;
box-sizing: border-box;
cursor: not-allowed;
opacity: 1;
// 按钮右 1837,行右 1849.5。
margin-right: 12.5px;
}
&__field-hint {
@@ -295,19 +327,33 @@ function onDeleteFile(): void {
}
}
// .file-list 默认 column + flex-start 会让item 按内容宽度收缩并居中,
// 设计稿 #28附件行是满宽(x=70~1849.5),这里改stretch。
:deep(.file-list) {
.flex(column, flex-start, stretch);
gap: 10px;
}
// 设计稿 #28 附件行与 #29「项目文件」行同款:h=55、padding 0 18.5 / 0 22.5。
:deep(.file-list__item) {
height: 55px;
box-sizing: border-box;
padding: 0 5.7px 0 16px;
gap: 14px;
padding: 0 18.5px 0 22.5px;
border: none;
border-radius: 8px;
background: @finance-row-bg;
}
// flex:1 让info 紧贴徽标(否则 space-between 会把它推到中间)。
// FileList.__info 只传了两个参数给 .flex(),第三个 align 默认 center
// → 文件名被水平居中;这里显式改回 flex-start(左对齐,对齐设计稿 x=140)。
:deep(.file-list__info) {
.flex(column, flex-start, flex-start);
flex: 1;
min-width: 0;
}
:deep(.file-list__name) {
font-size: 13px;
line-height: 17px;
@@ -319,11 +365,17 @@ function onDeleteFile(): void {
color: @project-muted;
}
// 设计稿 #28:「预览」↔「下载」间距 47,「下载」↔「删除」间距 22。
:deep(.file-list__actions) {
gap: 14px;
gap: 22px;
}
:deep(.file-list__btn:first-child) {
margin-right: 15.5px;
}
:deep(.file-list__btn) {
padding: 0;
font-size: 12px;
line-height: 16px;
color: @project-label;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { FileInfo } from '@/types/api'
import { fileLabelOf, fileToneOf } from '@/utils/fileMeta'
import { formatFileSize } from '@/utils/format'
interface Props {
@@ -13,6 +14,12 @@ const emit = defineEmits<{
preview: [index: number]
download: [index: number]
}>()
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>
@@ -27,10 +34,16 @@ const emit = defineEmits<{
:key="`${file.name}-${index}`"
class="project-files__item"
>
<span
class="project-files__badge"
:style="{ color: fileToneOf(file), backgroundColor: `${fileToneOf(file)}1a` }"
>
{{ fileLabelOf(file) }}
</span>
<div class="project-files__info">
<span class="project-files__name" :title="file.name">{{ file.name }}</span>
<span class="project-files__meta">
{{ formatFileSize(file.size) }} · {{ file.uploadTime }}
{{ fileMeta(file) }}
</span>
</div>
<div class="project-files__actions">
@@ -40,12 +53,6 @@ const emit = defineEmits<{
<button class="project-files__btn" @click="emit('download', index)">
下载
</button>
<button
class="project-files__btn project-files__btn--danger"
@click="emit('delete', index)"
>
删除
</button>
</div>
</li>
<li v-if="files.length === 0" class="project-files__empty text-secondary">
@@ -74,21 +81,38 @@ const emit = defineEmits<{
&__list {
.flex(column, flex-start, stretch);
gap: 10px;
margin-top: 24px;
// tabs 盒底 305 → 设计稿首行顶 342,间距 37px。
margin-top: 37px;
}
// 设计稿 el(项目文件 tab):行盒 y=342 h=55,徽标 33.5×33.5 x=84,文件名 x=132。
&__item {
.flex(row, space-between, center);
gap: @spacing-base;
height: 55px;
box-sizing: border-box;
padding: 0 5.6px 0 17px;
// 行左 70 → 徽标左 84 = 14px;徽标右 117.5 → 文件名左 132 = 14.5px。
// 行右 1849.5 → 「下载」字形右 1837,padding 13.5px。
padding: 0 13.5px 0 14px;
border-radius: 8px;
background: @finance-row-bg;
}
&__badge {
.flex(row, center, center);
flex-shrink: 0;
width: 33.5px;
height: 33.5px;
border-radius: @radius-sm;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.2px;
}
// flex:1 让 info 紧贴徽标(否则 space-between 会把它推到中间)。
&__info {
.flex(column, flex-start, stretch);
flex: 1;
gap: 3px;
min-width: 0;
}
@@ -107,14 +131,16 @@ const emit = defineEmits<{
color: @project-muted;
}
// 设计稿:「预览」盒 1748.5~1779,「下载」盒 1814.5~1837 → 盒间距 35.5px。
&__actions {
.flex(row, flex-end, center);
gap: 14px;
gap: 35.5px;
flex-shrink: 0;
}
&__btn {
flex-shrink: 0;
padding: 0;
font-size: 12px;
line-height: 16px;
color: @project-label;

View File

@@ -152,9 +152,12 @@ function fileMeta(file: FileInfo): string {
.flex(column, flex-start, stretch);
padding: 0;
// 设计稿 #27 的整页基线比 #30 低约 14px(同人不同次导出),
// 所以按「距 tabs 分割线的距离」对齐:#27 分割线 317.5 → 标题文字 337.5 = 20px。
// 减掉 1px 边框 → margin-top 20px(tabs 盒底 305 + 20 = 325)。
&__title {
.section-title();
margin-top: 22px;
margin-top: 20px;
padding-left: 12px;
font-size: 15px;
font-weight: 700;
@@ -166,8 +169,9 @@ function fileMeta(file: FileInfo): string {
}
}
// 设计稿:标题底 355 → 卡片1 顶 380,间距 25px。
&__title:first-child + &__plans {
margin-top: 21px;
margin-top: 25px;
}
&__plans + &__title,
@@ -180,9 +184,10 @@ function fileMeta(file: FileInfo): string {
margin-top: 21px;
}
// 设计稿:卡片1 底 518 → 卡片2 顶 533,间距 15px。
&__plans {
.flex(column, flex-start, flex-start);
gap: 14px;
gap: 15px;
}
&__plan {
@@ -196,8 +201,9 @@ function fileMeta(file: FileInfo): string {
box-sizing: border-box;
gap: 0;
// 设计稿:橙条底 677 → 卡片底 691,padding-bottom 14px。
&:has(.project-finance__pay) {
padding-bottom: 0;
padding-bottom: 14px;
}
&:not(:has(.project-finance__field:nth-child(3))):not(:has(.project-finance__pay)) {
@@ -211,6 +217,7 @@ function fileMeta(file: FileInfo): string {
&.is-paid {
.project-finance__badge {
color: @finance-paid;
background: @finance-paid-bg;
}
.project-finance__amount {
@@ -221,6 +228,7 @@ function fileMeta(file: FileInfo): string {
&.is-pending {
.project-finance__badge {
color: @tag-warning-text;
background: @tag-warning-bg;
}
.project-finance__amount {
@@ -231,6 +239,7 @@ function fileMeta(file: FileInfo): string {
&.is-idle {
.project-finance__badge {
color: @project-label;
background: @project-field-bg;
}
.project-finance__amount {
@@ -252,10 +261,14 @@ function fileMeta(file: FileInfo): string {
.ellipsis();
}
// 设计稿:徽标盒 56.5×20.5,右边界 422.5(卡片右 442 − padding 19.5)。
&__badge {
.flex(row, center, center);
flex-shrink: 0;
margin-top: 2px;
height: 20.5px;
padding: 0 8px;
border-radius: 10.25px;
box-sizing: border-box;
font-size: 11px;
line-height: 15px;
white-space: nowrap;
@@ -268,11 +281,12 @@ function fileMeta(file: FileInfo): string {
gap: @spacing-base;
}
// 子元素全部 position:absolute(脱离流),若不显式给宽度,
// flex-basis auto 会让容器宽度塌成 0,.ellipsis() 把文字全压没了。
&__fields {
position: relative;
flex: 0 0 auto;
flex: 1;
min-width: 0;
height: 53px;
&:has(> :nth-child(3)) {
@@ -280,10 +294,11 @@ function fileMeta(file: FileInfo): string {
}
}
// 设计稿:金额左 345.5,字段右留到 339 → right: 76px(容器宽 415 − 339)。
&__field {
position: absolute;
left: 0;
right: 0;
right: 76px;
font-size: 11px;
line-height: 15px;
color: @project-label;
@@ -296,9 +311,11 @@ function fileMeta(file: FileInfo): string {
&:nth-child(3) { top: 61px; }
}
// 设计稿:金额顶 458.5,卡片顶 380 → 相对 78.5。
// 上游17(顶padding) + 19(标题) + 8(plan-body margin) = 44 → top 34.5。
&__amount {
position: absolute;
top: 29px;
top: 34.5px;
right: 0;
flex-shrink: 0;
font-size: 20px;
@@ -307,15 +324,16 @@ function fileMeta(file: FileInfo): string {
white-space: nowrap;
}
// 设计稿:橙条 y=642~677(h=35),底距卡片底 691 = 14px。
&__pay {
height: 34px;
margin: 11px -19px 0;
height: 35px;
margin: 10px -19px 0;
border: none;
border-radius: 0 0 13px 13px;
background: @finance-pending;
color: @white;
font-size: 12px;
line-height: 34px;
line-height: 35px;
cursor: pointer;
transition: background 0.2s;

View File

@@ -59,6 +59,7 @@ function businessTypeText(project: Project): string {
height: 67px;
box-sizing: border-box;
// 设计稿 el51 "</>" 文字 (76.9, 209);64px 盒内居中 → 盒 y=209+13−32≈195。
&__logo {
.flex(row, center, center);
flex: 0 0 64px;
@@ -82,8 +83,9 @@ function businessTypeText(project: Project): string {
min-width: 0;
}
// 设计稿 el50 项目名 y=198(19px/700,行高 25)→ 卡片 y=195 + 3。
&__name {
margin: 14px 0 0;
margin: 3px 0 0;
font-size: 19px;
font-weight: 700;
line-height: 25px;
@@ -91,10 +93,11 @@ function businessTypeText(project: Project): string {
.ellipsis();
}
// 设计稿 el48~el45 meta 文字 y=231 → 项目名底 223+8。
&__meta {
.flex(row, flex-start, center);
gap: 20px;
margin-top: 9px;
margin-top: 8px;
font-size: 11px;
font-weight: 500;
line-height: 15px;
@@ -112,9 +115,10 @@ function businessTypeText(project: Project): string {
font-weight: 400;
}
// 设计稿 el43 "进行中" (1802, 211.5) 16px/700,右边界 1850= 容器右边界。
&__status {
flex-shrink: 0;
margin: 27.5px -21.6px 0 0;
margin: 16.5px 0 0;
font-size: 16px;
font-weight: 700;
line-height: 21px;

View File

@@ -60,9 +60,11 @@ function linkDone(index: number): boolean {
.flex(column, flex-start, stretch);
padding: 0;
// 设计稿 el35 "项目进度(里程碑)" (82, 457) 15px/700。
// 上游时间行 y=406(底 421)→ 标题间距 36px。
&__title {
.section-title();
margin-top: 35px;
margin-top: 36px;
padding-left: 12px;
font-size: 15px;
font-weight: 700;

View File

@@ -45,6 +45,7 @@ function clampPercent(value: number): number {
.flex(column, flex-start, stretch);
padding: 0;
// 设计稿 el54 "开发进度" (82, 310) 15px/700,标题紧贴 tabs 分割线 y=303。
&__title {
.section-title();
margin-top: 4px;
@@ -59,9 +60,10 @@ function clampPercent(value: number): number {
}
}
// 设计稿 el53 "总体进度" y=358,标题底 330 → 28px。
&__head {
.flex(row, space-between, baseline);
margin-top: 29px;
margin-top: 28px;
}
&__label {
@@ -70,8 +72,9 @@ function clampPercent(value: number): number {
color: @project-label;
}
// 设计稿 el52 "65%" (1798.8, 350) 20px/700;head 顶 358 → 上移 8px。
&__percent {
margin-top: -9px;
margin-top: -8px;
font-size: 20px;
font-weight: 700;
line-height: 26px;

View File

@@ -55,8 +55,9 @@ defineProps<Props>()
.flex(row, flex-start, flex-start);
flex-wrap: wrap;
gap: 32px @team-gap;
margin-top: 33px;
margin-left: -3.5px;
// 设计稿 #30:项目团队标题文字 551~557.5 → 头像圆顶 641。
// 标题盒底 = 558 + 20 = 578,故间距 641 − 578 = 63px。
margin-top: 63px;
}
&__item {
@@ -76,8 +77,9 @@ defineProps<Props>()
line-height: 22px;
}
// 设计稿 #30:头像圆底 705 → 姓名文字顶 724,间距 19px。
&__name {
margin-top: 0;
margin-top: 19px;
max-width: 100%;
font-size: 16px;
font-weight: 600;
@@ -87,8 +89,9 @@ defineProps<Props>()
.ellipsis();
}
// 设计稿 #30:姓名文字底 738 → 角色文字顶 752,间距 14px。
&__role {
margin-top: 3px;
margin-top: 14px;
max-width: 100%;
font-size: 12px;
line-height: 19px;

View File

@@ -179,23 +179,27 @@ onMounted(load)
gap: 0;
background: @white;
// 设计稿 el55 面包屑 y=116,header 底 72 → padding-top 44px。
&__breadcrumb {
padding-top: 40px;
padding-top: 44px;
font-size: 24px;
font-weight: 500;
line-height: 27px;
color: @project-breadcrumb;
}
// 设计稿 el51 logo 盒 y=195(面包屑盒底 = 72+44+27 = 143)→ 间距 52px。
&__info {
margin-top: 56px;
margin-top: 52px;
}
// 设计稿 el42 tab 文字 y=264,meta 文字底 231+15=246。
&__tabs {
.flex(row, flex-start, center);
gap: 16.45px;
height: 41px;
margin-top: 14px;
// 信息卡底 195+67=262,tab 文字顶 264 → tabs 盒顶 264,margin 2px。
margin-top: 2px;
box-sizing: border-box;
border-bottom: 1px solid @project-divider;
}