feat ui设计稿bug修复

This commit is contained in:
Ironsp
2026-10-01 10:53:13 +08:00
parent 9ab6253c81
commit 1601bdd82c
89 changed files with 4741 additions and 3652 deletions

3
.gitignore vendored
View File

@@ -15,6 +15,9 @@ RTK.md
.playwright-mcp .playwright-mcp
.omo .omo
.opencode .opencode
.workbuddy
specs2
.shots
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

31
.mock/afterSales.json Normal file
View File

@@ -0,0 +1,31 @@
{
"code": 200,
"msg": "ok",
"data": {
"id": "1",
"projectNo": "20260813ZK2Q5f45",
"projectName": "社区康养智能服务系统",
"businessType": "互联网软件开发",
"status": "IN_PROGRESS",
"createTime": "2026-8-17 14:00",
"submitUserName": "张三",
"afterSale": {
"id": "1",
"requirementId": "1",
"type": "BUG问题",
"reason": "具体请看清单",
"remark": null,
"status": "IN_PROGRESS",
"requestUserName": "张三",
"requestTime": "2026-8-17 17:00",
"processUserName": null,
"processNote": null,
"processTime": null,
"withdrawReason": null
},
"afterSaleAttachments": [
{ "id": "1", "requirementId": "1", "phase": "AFTER_SALE", "ossId": "11", "originalName": "项目需求规格说明书.pdf", "url": "https://example.com/a.pdf", "fileSize": 2516582, "sortOrder": 1, "createTime": "2026-8-17 17:00" },
{ "id": "2", "requirementId": "1", "phase": "AFTER_SALE", "ossId": "12", "originalName": "项目需求说明书.word", "url": "https://example.com/b.docx", "fileSize": 2516582, "sortOrder": 2, "createTime": "2026-8-17 17:00" }
]
}
}

102
.mock/bills.json Normal file
View File

@@ -0,0 +1,102 @@
{
"code": 200,
"msg": "ok",
"data": [
{
"id": "1001",
"billNo": "20260813Z2Z5f245",
"projectId": "1",
"projectName": "某大型银行数字化转型方案",
"userId": "1",
"amount": 20000,
"billType": "ADVANCE",
"periodName": "第一期 · 付款 比例33%",
"periodRatio": 33,
"status": "PAID",
"payChannel": "WECHAT",
"transactionNo": "T2026081915303578K2M001",
"payTime": "2026-8-17 17:00",
"createTime": "2026-8-17 14:00"
},
{
"id": "1002",
"billNo": "20260813Z2Z5f245",
"projectId": "2",
"projectName": "智慧城市大数据平台建设项目",
"userId": "1",
"amount": 20000,
"billType": "PROGRESS",
"periodName": "第二期 · 付款 比例33%",
"periodRatio": 33,
"status": "PENDING",
"payChannel": null,
"transactionNo": null,
"payTime": null,
"createTime": "2026-8-17 14:00"
},
{
"id": "1003",
"billNo": "20260813Z2Z5f245",
"projectId": "3",
"projectName": "三甲医院暗审医疗信息化系统",
"userId": "1",
"amount": 20000,
"billType": "FINAL",
"periodName": "第三期 · 尾款 比例33%",
"periodRatio": 33,
"status": "UNSTARTED",
"payChannel": null,
"transactionNo": null,
"payTime": null,
"createTime": "2026-8-17 14:00"
},
{
"id": "1004",
"billNo": "20260813Z2Z5f246",
"projectId": "4",
"projectName": "社区康养智能服务系统",
"userId": "1",
"amount": 20000,
"billType": "ADVANCE",
"periodName": "第一期 · 预付款 比例30%",
"periodRatio": 30,
"status": "PAID",
"payChannel": "ALIPAY",
"transactionNo": "T2026081915303578K2M002",
"payTime": "2026-8-18 10:20",
"createTime": "2026-8-17 14:00"
},
{
"id": "1005",
"billNo": "20260813Z2Z5f247",
"projectId": "5",
"projectName": "智慧园区物联网平台",
"userId": "1",
"amount": 20000,
"billType": "PROGRESS",
"periodName": "第二期 · 进度款 比例35%",
"periodRatio": 35,
"status": "PENDING",
"payChannel": null,
"transactionNo": null,
"payTime": null,
"createTime": "2026-8-17 14:00"
},
{
"id": "1006",
"billNo": "20260813Z2Z5f248",
"projectId": "6",
"projectName": "某大型银行数字化转型方案",
"userId": "1",
"amount": 20000,
"billType": "ADVANCE",
"periodName": "第一期 · 预付款 比例33%",
"periodRatio": 33,
"status": "UNSTARTED",
"payChannel": null,
"transactionNo": null,
"payTime": null,
"createTime": "2026-8-17 14:00"
}
]
}

19
.mock/files.json Normal file
View File

@@ -0,0 +1,19 @@
{
"code": 200,
"msg": "ok",
"data": {
"id": "1",
"projectNo": "20260813ZK2Q5f45",
"projectName": "社区康养智能服务系统",
"businessType": "互联网软件开发",
"status": "IN_PROGRESS",
"createTime": "2026-8-17 14:00",
"submitUserName": "张三",
"requirementAttachments": [
{ "id": "1", "requirementId": "1", "phase": "REQUIREMENT", "ossId": "1", "originalName": "项目需求规格说明书.pdf", "url": "https://example.com/a.pdf", "fileSize": 2516582, "sortOrder": 1, "createTime": "2026-8-17 17:00" },
{ "id": "2", "requirementId": "1", "phase": "REQUIREMENT", "ossId": "2", "originalName": "项目需求说明书.word", "url": "https://example.com/b.docx", "fileSize": 2516582, "sortOrder": 2, "createTime": "2026-8-17 17:00" },
{ "id": "3", "requirementId": "1", "phase": "REQUIREMENT", "ossId": "3", "originalName": "项目需求规格说明书.pdf", "url": "https://example.com/c.pdf", "fileSize": 2516582, "sortOrder": 3, "createTime": "2026-8-17 17:00" },
{ "id": "4", "requirementId": "1", "phase": "REQUIREMENT", "ossId": "4", "originalName": "项目需求说明书.word", "url": "https://example.com/d.docx", "fileSize": 2516582, "sortOrder": 4, "createTime": "2026-8-17 17:00" }
]
}
}

27
.mock/finance.json Normal file
View File

@@ -0,0 +1,27 @@
{
"code": 200,
"msg": "ok",
"data": {
"id": "1",
"projectNo": "20260813ZK2Q5f45",
"projectName": "社区康养智能服务系统",
"businessType": "互联网软件开发",
"status": "IN_PROGRESS",
"createTime": "2026-8-17 14:00",
"submitUserName": "张三",
"bills": [
{ "id": "1001", "billNo": "2026083222P345", "projectId": "1", "projectNo": "20260813ZK2Q5f45", "projectName": "社区康养智能服务系统", "userId": "1", "amount": "20100.00", "contractId": null, "billType": "ADVANCE", "periodName": "第一期", "periodRatio": "33.00", "status": "PAID", "payChannel": "WECHAT", "transactionNo": "123D06891B30030/8K2M001", "tradeName": null, "payTime": "2026-8-17 17:00", "dueDate": null },
{ "id": "1002", "billNo": "2026083222P345", "projectId": "1", "projectNo": "20260813ZK2Q5f45", "projectName": "社区康养智能服务系统", "userId": "1", "amount": "20100.00", "contractId": null, "billType": "PROGRESS", "periodName": "第二期", "periodRatio": "33.00", "status": "PENDING", "payChannel": null, "transactionNo": null, "tradeName": null, "payTime": null, "dueDate": null },
{ "id": "1003", "billNo": "2026083222P345", "projectId": "1", "projectNo": "20260813ZK2Q5f45", "projectName": "社区康养智能服务系统", "userId": "1", "amount": "20100.00", "contractId": null, "billType": "FINAL", "periodName": "第三期", "periodRatio": "33.00", "status": "UNSTARTED", "payChannel": null, "transactionNo": null, "tradeName": null, "payTime": null, "dueDate": null }
],
"billPlans": [],
"invoices": [
{ "id": "1", "billId": "1001", "invoiceTitle": "测试", "taxNo": null, "invoiceType": "电子普通发票", "amount": "20100.00", "status": "ISSUED", "invoiceNo": null, "invoiceFile": "https://example.com/inv.pdf", "applyTime": "2026-8-17 17:00", "invoiceDate": null, "createTime": "2026-8-17 17:00" },
{ "id": "2", "billId": "1001", "invoiceTitle": "测试", "taxNo": null, "invoiceType": "电子普通发票", "amount": "20100.00", "status": "ISSUED", "invoiceNo": null, "invoiceFile": "https://example.com/inv2.pdf", "applyTime": "2026-8-17 17:00", "invoiceDate": null, "createTime": "2026-8-17 17:00" }
],
"contracts": [
{ "id": "1", "contractNo": "HT20260813001", "projectId": "1", "title": "项目需求规格说明书.pdf", "amount": "60300.00", "signStatus": "SIGNED", "status": "SIGNED", "signDate": "2026-8-17", "file": "https://example.com/contract.pdf", "createTime": "2026-8-17 17:00" },
{ "id": "2", "contractNo": "HT20260813002", "projectId": "1", "title": "项目需求说明书.word", "amount": "60300.00", "signStatus": "SIGNED", "status": "SIGNED", "signDate": "2026-8-17", "file": "https://example.com/contract2.docx", "createTime": "2026-8-17 17:00" }
]
}
}

42
.mock/progress.json Normal file
View File

@@ -0,0 +1,42 @@
{
"code": 200,
"msg": "ok",
"data": {
"id": "1",
"projectNo": "20260813ZK2Q5f45",
"projectName": "社区康养智能服务系统",
"businessType": "互联网软件开发",
"status": "IN_PROGRESS",
"createTime": "2026-8-17 14:00",
"submitUserName": "张三",
"projectStage": {
"id": "1",
"projectId": "1",
"requirementId": "1",
"stageName": "开发实现",
"status": "IN_PROGRESS",
"startTime": "2026-8-17",
"planTime": "2026-8-17",
"finishTime": null,
"developmentProgress": 65,
"sort": 3
},
"developmentProgress": 65,
"projectMilestones": [
{ "id": "1", "projectId": "1", "milestoneName": "需求认证", "sortOrder": 1, "status": "COMPLETED", "startTime": "2026-8-13 11:04:11", "endTime": "2026-8-13 11:04:11", "parallelFlag": false },
{ "id": "2", "projectId": "1", "milestoneName": "原型设计", "sortOrder": 2, "status": "COMPLETED", "startTime": "2026-8-13 11:04:11", "endTime": "2026-8-13 11:04:11", "parallelFlag": false },
{ "id": "3", "projectId": "1", "milestoneName": "开发实现", "sortOrder": 3, "status": "IN_PROGRESS", "startTime": null, "endTime": null, "parallelFlag": false },
{ "id": "4", "projectId": "1", "milestoneName": "需求验收", "sortOrder": 4, "status": "UNSTARTED", "startTime": null, "endTime": null, "parallelFlag": false },
{ "id": "5", "projectId": "1", "milestoneName": "项目交付", "sortOrder": 5, "status": "UNSTARTED", "startTime": null, "endTime": null, "parallelFlag": false }
],
"members": [
{ "id": "1", "projectId": "1", "userId": "1", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": true },
{ "id": "2", "projectId": "1", "userId": "2", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false },
{ "id": "3", "projectId": "1", "userId": "3", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false },
{ "id": "4", "projectId": "1", "userId": "4", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false },
{ "id": "5", "projectId": "1", "userId": "5", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false },
{ "id": "6", "projectId": "1", "userId": "6", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false },
{ "id": "7", "projectId": "1", "userId": "7", "userName": "张三", "roleKey": "pm", "roleName": "项目经理", "isOwner": false }
]
}
}

19
diag.js Normal file
View File

@@ -0,0 +1,19 @@
// @ts-nocheck
// 诊断:点页签后输出祖先链几何,定位额外间距来源
const TAB = Number(process.argv[2] || 2)
const TARGET = process.argv[3] || '.project-files__list'
const js = `(async function(){
const tabs = document.querySelectorAll(".project-detail__tab");
if (${TAB} > 0 && tabs[${TAB} - 1]) { tabs[${TAB} - 1].click(); await new Promise(r => setTimeout(r, 900)); }
let el = document.querySelector(${JSON.stringify(TARGET)});
const chain = [];
while (el && chain.length < 6) {
const r = el.getBoundingClientRect();
const cs = getComputedStyle(el);
chain.push({ cls: (el.className || '').toString().slice(0, 40), y: +r.y.toFixed(1), h: +r.height.toFixed(1), mt: cs.marginTop, pt: cs.paddingTop, pb: cs.paddingBottom, gap: cs.gap, rowGap: cs.rowGap });
el = el.parentElement;
}
return JSON.stringify(chain, null, 0);
})()`
console.log(js)

27
measure.js Normal file
View File

@@ -0,0 +1,27 @@
// @ts-nocheck
// 临时几何量测:把要测的选择器写进 SEL,执行 `node measure.js <sel...>` 生成命令
const SELS = [
'.project-detail__breadcrumb',
'.project-info-card',
'.project-info-card__logo',
'.project-info-card__name',
'.project-info-card__meta',
'.project-info-card__status',
'.project-detail__tabs',
'.project-detail__tab',
'.project-progress__title',
'.project-progress__head',
'.project-progress__track',
'.project-progress__range',
'.project-milestones__title',
'.project-milestones__track',
'.project-milestones__node',
'.project-milestones__dot',
'.project-team__title',
'.project-team__list',
'.project-team__avatar',
]
const arr = JSON.stringify(SELS).replace(/"/g, '\\"')
const js = `(function(){const o={};for(const s of ${JSON.stringify(SELS)}){const e=document.querySelector(s);if(!e){o[s]=null;continue}const r=e.getBoundingClientRect();o[s]=[+r.x.toFixed(1),+r.y.toFixed(1),+r.width.toFixed(1),+r.height.toFixed(1)]}return JSON.stringify(o)})()`
console.log(js)

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,12 +3,13 @@ import { useSiteStore } from '@/stores/modules/site'
const siteStore = useSiteStore() const siteStore = useSiteStore()
// 设计稿快速导航列原文:首页 / 案例中心 / 需求提报 / 下载 APP / 帮助中心
const quickLinks = [ const quickLinks = [
{ label: '首页', path: '/' }, { label: '首页', path: '/' },
{ label: '案例库', path: '/cases' }, { label: '案例中心', path: '/cases' },
{ label: '需求提报', path: '/demand' }, { label: '需求提报', path: '/demand' },
{ label: '下载 APP', path: '/' }, { label: '下载 APP', path: '/' },
{ label: '消息中心', path: '/message' }, { label: '帮助中心', path: '/profile/help' },
] ]
const coreBusiness = [ const coreBusiness = [
@@ -22,7 +23,7 @@ const coreBusiness = [
<template> <template>
<footer class="app-footer"> <footer class="app-footer">
<div class="app-footer__inner container"> <div class="app-footer__inner">
<div class="app-footer__about"> <div class="app-footer__about">
<div class="app-footer__brand"> <div class="app-footer__brand">
<span class="app-footer__name">虫子智造</span> <span class="app-footer__name">虫子智造</span>
@@ -32,7 +33,7 @@ const coreBusiness = [
</p> </p>
</div> </div>
<div class="app-footer__col"> <div class="app-footer__col app-footer__col--core">
<h4 class="app-footer__title"> <h4 class="app-footer__title">
核心业务 核心业务
</h4> </h4>
@@ -43,7 +44,7 @@ const coreBusiness = [
>{{ item }}</span> >{{ item }}</span>
</div> </div>
<div class="app-footer__col"> <div class="app-footer__col app-footer__col--nav">
<h4 class="app-footer__title"> <h4 class="app-footer__title">
快速导航 快速导航
</h4> </h4>
@@ -57,7 +58,7 @@ const coreBusiness = [
</RouterLink> </RouterLink>
</div> </div>
<div class="app-footer__col"> <div class="app-footer__col app-footer__col--contact">
<h4 class="app-footer__title"> <h4 class="app-footer__title">
联系我们 联系我们
</h4> </h4>
@@ -72,7 +73,7 @@ const coreBusiness = [
<p class="app-footer__legal"> <p class="app-footer__legal">
©2026 虫子智造 研发智造一体化平台 版权所有|自营研发生产平台,所有项目源码、图纸受平台知识产权保护,禁止盗用、复制、二次分发 ©2026 虫子智造 研发智造一体化平台 版权所有|自营研发生产平台,所有项目源码、图纸受平台知识产权保护,禁止盗用、复制、二次分发
</p> </p>
<p class="app-footer__legal"> <p class="app-footer__legal app-footer__legal--icp">
{{ siteStore.siteInfo.icp }} | {{ siteStore.siteInfo.police }} {{ siteStore.siteInfo.icp }} | {{ siteStore.siteInfo.police }}
</p> </p>
</div> </div>
@@ -80,54 +81,76 @@ const coreBusiness = [
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿页脚 1920×481(首页稿 y1942→2423):
// 列区 y1996 起(页脚顶 +54),四列 x155/922/1257/1511,列宽 625/193/80/253
// 品牌名「虫子智造」@x200(相对列缩进 45),描述 @x155
// 版权区:©行 @x526(867 盒在 1920 画布居中)、ICP 行 @x841(相对 867 盒缩进 315),均左对齐
.app-footer { .app-footer {
background: @footer-bg; background: @footer-bg;
color: @white; color: @white;
&__inner { &__inner {
.flex(row, space-between, flex-start); .flex(row, flex-start, flex-start);
gap: @spacing-xl; width: 100%;
padding-top: @spacing-xxl; max-width: 1920px;
padding-left: @footer-inset; height: 332px; // 页脚顶 → © 行(y1942→2274)
padding-right: @footer-inset; box-sizing: border-box;
padding-top: 54px;
padding-left: clamp(@spacing-base, calc((100% - 1609px) / 2), 155px); // 设计内容 x155→1764 = 1609 宽
overflow: hidden; // 防列内容撑出横向滚动
} }
&__about { &__about {
max-width: 480px; flex: 0 1 625px; // x155 → 780
min-width: 0;
} }
&__brand { &__brand {
.flex(row, flex-start); .flex(row, flex-start);
margin-bottom: @spacing-base; margin-bottom: 20px; // 品牌名盒底 2037 → 描述 2057
margin-left: 45px; // 设计:品牌名 @x200,列起点 x155
} }
&__name { &__name {
font-size: @font-size-title; font-size: 28px;
font-weight: 500; font-weight: 500;
line-height: 41px;
color: @white; color: @white;
} }
&__desc { &__desc {
margin-top: @spacing-sm; margin-top: 0;
font-size: @font-size-sm; font-size: 16px;
line-height: 1.8; font-weight: 500;
line-height: 23px; // 设计 625×115 五行
} }
&__col { &__col {
.flex(column, flex-start); .flex(column, flex-start, flex-start); // 第三参必须显式:mixin 默认 center 会把列项水平居中
gap: @spacing-sm; gap: 0;
min-width: 0;
// 设计列位:核心业务 @922(列宽 193)、快速导航 @1257(80)、联系我们 @1511(253)
// 用 modifier 类定位;:nth-of-type 按标签序号计数(about 也算 div),会整体错位一档
&--core { flex: 0 0 193px; margin-left: 142px; }
&--nav { flex: 0 0 80px; margin-left: 142px; }
&--contact { flex: 0 0 253px; margin-left: 174px; }
} }
&__title { &__title {
margin-bottom: @spacing-sm; margin-bottom: 20px; // 标题盒底 2037 → 列表 2057
font-size: @font-size-title; font-size: 28px;
font-weight: 500; font-weight: 500;
line-height: 41px;
white-space: nowrap;
color: @white; color: @white;
} }
&__link { &__link {
font-size: @font-size-sm; font-size: 16px;
line-height: 1.5; font-weight: 500;
line-height: 23px; // 设计 5 项 115 高
white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
@@ -137,20 +160,32 @@ const coreBusiness = [
} }
&__contact { &__contact {
font-size: @font-size-sm; font-size: 16px;
line-height: 1.8; font-weight: 500;
line-height: 23px;
} }
// 版权 / 备案:设计稿为页脚底部居中两行 // 版权区:867 盒在画布居中(1920 视口 → x526),盒内文字左对齐;ICP 缩进 315(→ x841)
&__bottom { &__bottom {
padding: @spacing-xxl @footer-inset; width: 867px;
text-align: center; max-width: calc(100% - 32px);
margin: 0 auto;
padding-bottom: 87px; // ICP 盒底 2336 → 页脚底 2423
text-align: left;
} }
&__legal { &__legal {
font-size: @font-size-sm; margin: 0;
line-height: 1.8; font-size: 14px;
font-weight: 500;
line-height: 23px;
white-space: nowrap; // 设计稿单行(867×23),回退字体偏宽防折行
color: @white; color: @white;
&--icp {
margin-top: 16px; // © 盒底 2297 → ICP 2313
margin-left: 315px;
}
} }
} }
</style> </style>

View File

@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useMessageStore } from '@/stores/modules/message'
interface NavItem { interface NavItem {
label: string label: string
path: string path: string
} }
// 设计稿页头(43 张稿一致):无 logo 文字,导航首项 x275(间隙 94),
// 右侧两枚 106×36 胶囊(快捷提报 x1559 / 在线客服 x1694,右缘 1800)
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ label: '首页', path: '/' }, { label: '首页', path: '/' },
{ label: '案例库', path: '/cases' }, { label: '案例库', path: '/cases' },
@@ -17,20 +15,11 @@ const navItems: NavItem[] = [
{ label: '财务中心', path: '/finance' }, { label: '财务中心', path: '/finance' },
{ label: '个人中心', path: '/profile' }, { label: '个人中心', path: '/profile' },
] ]
const messageStore = useMessageStore()
const { unreadCount } = storeToRefs(messageStore)
onMounted(() => void messageStore.fetchUnreadCount())
</script> </script>
<template> <template>
<header class="app-header"> <header class="app-header">
<div class="app-header__inner container"> <div class="app-header__inner">
<RouterLink to="/" class="app-header__logo">
<span class="app-header__logo-text">虫子智造</span>
</RouterLink>
<nav class="app-header__nav"> <nav class="app-header__nav">
<RouterLink <RouterLink
v-for="item in navItems" v-for="item in navItems"
@@ -51,13 +40,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
<img src="@/assets/icons/online-service.png" class="app-header__action-icon" alt=""> <img src="@/assets/icons/online-service.png" class="app-header__action-icon" alt="">
在线客服 在线客服
</RouterLink> </RouterLink>
<RouterLink to="/message" class="app-header__bell" aria-label="消息中心">
<span class="app-header__bell-icon">🔔</span>
<span v-if="unreadCount > 0" class="app-header__badge">{{ unreadCount }}</span>
</RouterLink>
<RouterLink to="/login" class="app-header__login link">
登录
</RouterLink>
</div> </div>
</div> </div>
</header> </header>
@@ -72,25 +54,25 @@ onMounted(() => void messageStore.fetchUnreadCount())
background: @header-bg; background: @header-bg;
border-bottom: 1px solid @header-border; border-bottom: 1px solid @header-border;
// 设计稿:导航首项 x275、胶囊右缘 1800(= 1920 − 120)
// 左留白按 155 基准、右留白按 120 基准,各自 clamp 收窄,窄视口不溢出
&__inner { &__inner {
.flex(row, space-between);
height: 100%;
}
&__logo {
.flex(row, flex-start); .flex(row, flex-start);
height: 100%;
&-text { padding-left: clamp(@spacing-base, calc((100% - 1600px) / 2), 155px);
font-size: @font-size-xl; padding-right: clamp(@spacing-base, calc((100% - 1680px) / 2), 120px);
font-weight: 600;
color: @heading-color;
}
} }
// 导航首项 x275 = 左留白 155 + 120
&__nav { &__nav {
.flex(row, center); .flex(row, center);
gap: @header-nav-gap; gap: @header-nav-gap; // 设计实测 94(首页 x275→311,案例库 x405)
height: 100%; height: 100%;
margin-left: 120px;
@media (max-width: 1600px) {
gap: 48px;
}
@media (max-width: 1280px) { @media (max-width: 1280px) {
gap: 32px; gap: 32px;
@@ -100,45 +82,49 @@ onMounted(() => void messageStore.fetchUnreadCount())
position: relative; position: relative;
.flex(row, center); .flex(row, center);
height: 100%; height: 100%;
font-size: @font-size-base; // 设计稿:导航项 18/500,未激活 #000000、激活 #085DFF
font-size: 18px;
font-weight: 500; font-weight: 500;
color: @black; color: @black;
transition: color 0.2s; transition: color 0.2s;
&:hover, &:hover,
&.router-link-active { &.router-link-active {
color: @primary-strong; color: @home-tab-active;
} }
// 激活项底部短横线(设计 29×2px × 0.77,距顶栏底边 11px × 0.77) // 激活项底部短横线(设计实测 29×2px @y59,距底 11)
&.router-link-active::after { &.router-link-active::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 50%; left: 50%;
bottom: 9px; bottom: 11px;
width: 22px; width: 29px;
height: 2px; height: 2px;
border-radius: 1px; border-radius: 1px;
background: @primary-color; background: @home-tab-active;
transform: translateX(-50%); transform: translateX(-50%);
} }
} }
} }
&__actions { &__actions {
.flex(row, flex-end); .flex(row, flex-end, center);
gap: @spacing-base; gap: 29px; // 设计:胶囊1 右缘 1665 → 胶囊2 左缘 1694
margin-left: auto;
flex-shrink: 0;
} }
// 设计稿:胶囊 106×36,图标 18 + 文字 16/500 白字
&__action { &__action {
.flex(row, center); .flex(row, center);
gap: @spacing-xs; gap: 5px;
height: 28px; width: 106px;
padding: 0 @spacing-sm; height: 36px;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @primary-color; background: @primary-color;
color: @white; color: @white;
font-size: @font-size-sm; font-size: 16px;
font-weight: 500; font-weight: 500;
transition: opacity 0.2s; transition: opacity 0.2s;
@@ -147,36 +133,9 @@ onMounted(() => void messageStore.fetchUnreadCount())
} }
&-icon { &-icon {
width: 16px; width: 18px;
height: 16px; height: 18px;
} }
} }
&__bell {
position: relative;
&-icon {
font-size: @font-size-lg;
}
}
&__badge {
position: absolute;
top: -6px;
right: -8px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: @radius-round;
background: @danger-color;
color: @white;
font-size: @font-size-sm;
line-height: 16px;
text-align: center;
}
&__login {
font-size: @font-size-base;
}
} }
</style> </style>

View File

@@ -67,11 +67,13 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
<style lang="less" scoped> <style lang="less" scoped>
.case-card { .case-card {
// 设计稿卡片 396×267 白底 bd #E1DFDF/1
overflow: hidden; overflow: hidden;
height: 267px;
box-sizing: border-box;
border: 1px solid @cases-card-border; border: 1px solid @cases-card-border;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @cases-card-shadow;
cursor: pointer; cursor: pointer;
transition: box-shadow 0.2s, transform 0.2s; transition: box-shadow 0.2s, transform 0.2s;
@@ -82,7 +84,8 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
&__cover { &__cover {
position: relative; position: relative;
aspect-ratio: 396 / 138; // 设计04 封面 396×137.8,通栏无留白 // 设计封面 396×137.81(通栏无留白)
height: 137px;
overflow: hidden; overflow: hidden;
} }
@@ -92,21 +95,24 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
object-fit: cover; object-fit: cover;
&--placeholder { &--placeholder {
background: @primary-strong; // 设计04 封面占位实底 rgba(2,79,255) background: @primary-strong;
} }
} }
&__tag { &__tag {
position: absolute; position: absolute;
top: 6px; // 设计标签文字 @(628.6,540.35),卡 @(606,423) → 卡内 (22.6,117) 高 44
left: 14px; top: 87px;
height: 19px; // 设计15 标签 24 × 0.77 left: 22px;
padding: 0 6px; height: 44px;
padding: 0 14px;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 11px; // 设计 10 → 7.7,取可读下限 11 // 设计 28/500
line-height: 19px; font-size: 28px;
font-weight: 500;
line-height: 44px;
&--hardware { &--hardware {
background: @tag-hardware; background: @tag-hardware;
@@ -160,12 +166,19 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
} }
&__body { &__body {
padding: 13px 16px 11px; // 上 17 / 左 20 / 下 14 × 0.77 position: relative;
// 设计标题 @(626.45,578.03),卡 @(606,423) → 卡内 (20,155)
padding: 18px 20px 0;
height: 129px;
box-sizing: border-box;
} }
&__title { &__title {
font-size: 12px; // 设计 16 × 0.77 ≈ 12.3 // 设计标题 16/500 #333333
margin: 0;
font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 21.5px;
color: @cases-text; color: @cases-text;
.ellipsis(); .ellipsis();
} }
@@ -173,28 +186,31 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
&__meta { &__meta {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px @spacing-sm; // 设计两行:y610.33 / y631.86 → 行距 21.5
margin-top: @spacing-sm; gap: 21.5px 12px;
font-size: 11px; // 设计 10 → 7.7,取可读下限 11 margin-top: 10px;
line-height: 1.5; // 设计元信息 10/400 #333333
font-size: 10px;
line-height: 14px;
color: @cases-text; color: @cases-text;
li { li {
/* 值可能较长(如「完成时间:2026/11/30」),换行而非截断,避免丢信息 */
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
} }
&__label { &__label {
color: inherit; // 设计04 标签与值同色 rgba(51,51,51) color: inherit;
} }
&__more { &__more {
display: block; position: absolute;
width: fit-content; // 设计 @(926.67,659.85),卡 @(606,423) → 卡内 (320.67,236.85)
margin: 11px 0 0 auto; // 设计 14 × 0.77 right: 20px;
color: @primary-strong; // 设计04 实取 rgba(2,79,255) bottom: 14px;
font-size: 11px; // 设计 12 → 9.2,取可读下限 11 color: @primary-strong;
// 设计 12/400 #024FFF
font-size: 12px;
font-weight: 400; font-weight: 400;
} }
} }

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
/**
* 页面头部(白底通栏):主标题 22/700 #1F2937 + 副标语 13/400 #6B7280。
*
* <p>43 张设计稿实测:标题顶 y125(顶栏 72 → 内容区 padding-top 53 含 1px 描边)、
* 行盒 29 → 副标语 ink y162(标题盒底 154 + 8)。整块白底高 179(72 → 251)。</p>
*/
interface Props {
title: string
slogan?: string
}
defineProps<Props>()
</script>
<template>
<header class="page-head">
<div class="container">
<h1 class="page-head__title">
{{ title }}
</h1>
<p v-if="slogan" class="page-head__slogan">
{{ slogan }}
</p>
</div>
</header>
</template>
<style lang="less" scoped>
.page-head {
background: @white;
padding-bottom: 72px; // 副标语盒底 179 → 页头块底 251(块高 179)
&__title {
padding-top: 52px; // 顶栏 72 → 标题 ink 124(design 125)
font-size: 22px;
font-weight: 700;
line-height: 29px;
color: @finance-ink;
}
&__slogan {
padding-bottom: 0;
margin-top: 9px; // 标题盒底 153 → 副标语盒顶 162
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @finance-muted-text;
}
}
</style>

View File

@@ -19,17 +19,19 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
</script> </script>
<template> <template>
<div v-if="password" class="pwd-strength"> <!-- 设计稿:强度条常驻显示(未输入时整条为空轨道 + 空强度文字),
占位高度固定 15(条行)+ 18(提示行),保证表单纵向节奏与设计稿一致 -->
<div class="pwd-strength">
<div class="pwd-strength__bars"> <div class="pwd-strength__bars">
<span <span
v-for="i in 3" v-for="i in 3"
:key="i" :key="i"
class="pwd-strength__bar" class="pwd-strength__bar"
:class="{ 'is-on': i <= levelIndex, [`is-${level}`]: i <= levelIndex }" :class="{ 'is-on': password && i <= levelIndex, [`is-${level}`]: password && i <= levelIndex }"
/> />
</div> </div>
<span class="pwd-strength__label" :class="`is-${level}`">密码强度:{{ levelText }}</span> <span class="pwd-strength__label" :class="`is-${level}`">{{ password ? `密码强度:${levelText}` : '' }}</span>
<p v-if="level === 'weak'" class="pwd-strength__tip"> <p v-if="password && level === 'weak'" class="pwd-strength__tip">
密码等级过低,请重新输入(建议字母+数字+符号组合) 密码等级过低,请重新输入(建议字母+数字+符号组合)
</p> </p>
</div> </div>
@@ -40,19 +42,23 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
.pwd-strength { .pwd-strength {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-template-rows: 15px auto;
column-gap: @spacing-sm; column-gap: @spacing-sm;
row-gap: 9px; row-gap: 3px;
align-items: center; align-items: center;
// 设计稿:密码框底 → 条行 → 提示行 → 下一标签,节奏 15/15/18
min-height: 33px;
box-sizing: border-box;
&__bars { &__bars {
.flex(row, center); .flex(row, center);
gap: 8px; gap: 8px;
} }
// 设计稿轨道实取 #EDEFF2,条高 5 设计px ≈ 4px // 设计稿轨道实取 #EDEFF2,条高 5
&__bar { &__bar {
flex: 1; flex: 1;
height: 4px; height: 5px;
border-radius: 2px; border-radius: 2px;
background: @auth-track; background: @auth-track;
@@ -69,10 +75,11 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
} }
} }
// 设计稿强度文字为 11 设计px 中性灰(「弱」实取 rgba(156,163,175)) // 设计稿强度文字为 11px 中性灰(「弱」实取 rgba(156,163,175))
&__label { &__label {
justify-self: end; justify-self: end;
font-size: 11px; font-size: 11px;
line-height: 15px;
color: @text-color-disabled; color: @text-color-disabled;
} }
@@ -80,6 +87,7 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
grid-column: 1 / -1; grid-column: 1 / -1;
margin: 0; margin: 0;
font-size: 11px; font-size: 11px;
line-height: 15px;
color: @danger-color; color: @danger-color;
} }
} }

View File

@@ -37,17 +37,17 @@ function onGetCode(): void {
.flex(row, stretch); .flex(row, stretch);
gap: @spacing-sm; gap: @spacing-sm;
// 设计稿(02/54/55):验证码输入框与普通输入框同规格 40 设计px ≈ 32px、13 设计px 字号 // 设计稿(02/54/55):验证码输入框 590×39(整行 680,右侧留 90 给「获取验证码」)
&__input { &__input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 32px; height: 39px;
padding: 0 12px; padding: 0 12px;
border: 1px solid @border-color; border: 1px solid #dde1e7;
border-radius: 6px; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 11px; font-size: 13px;
color: @heading-color; color: #1f2937;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
@@ -56,29 +56,30 @@ function onGetCode(): void {
} }
&::placeholder { &::placeholder {
color: @heading-color; color: #1f2937;
} }
} }
// 设计稿:「获取验证码」为白底蓝描边蓝字小按钮(#024FFF) // 设计稿:「获取验证码」为蓝字纯文本(无边框),12/400 #024FFF
&__btn { &__btn {
flex-shrink: 0; flex-shrink: 0;
min-width: 96px; min-width: 74px;
height: 32px; height: 39px;
padding: 0 @spacing-sm; padding: 0;
border: 1px solid @primary-strong; border: none;
border-radius: 6px; background: transparent;
background: @white;
color: @primary-strong; color: @primary-strong;
font-size: 11px; font-size: 12px;
transition: all 0.2s; font-weight: 400;
text-align: right;
cursor: pointer;
transition: color 0.2s;
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: @primary-light; color: @primary-hover;
} }
&:disabled { &:disabled {
border-color: @border-color-dark;
color: @text-color-disabled; color: @text-color-disabled;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@@ -1,6 +1,7 @@
export { default as AppHeader } from './AppHeader.vue' export { default as AppHeader } from './AppHeader.vue'
export { default as AppFooter } from './AppFooter.vue' export { default as AppFooter } from './AppFooter.vue'
export { default as AppButton } from './AppButton.vue' export { default as AppButton } from './AppButton.vue'
export { default as PageHead } from './PageHead.vue'
export { default as CaseCard } from './CaseCard.vue' export { default as CaseCard } from './CaseCard.vue'
export { default as FilterPanel } from './FilterPanel.vue' export { default as FilterPanel } from './FilterPanel.vue'
export type { FilterChip } from './FilterPanel.vue' export type { FilterChip } from './FilterPanel.vue'

View File

@@ -1,58 +0,0 @@
<script setup lang="ts">
// 全局浮动入口:快捷提报 / 在线客服(PRD 1.2)
const entries = [
{ label: '快捷提报', icon: '📝', path: '/demand' },
{ label: '在线客服', icon: '💬', path: '/service' },
]
</script>
<template>
<div class="floating-actions">
<RouterLink
v-for="entry in entries"
:key="entry.path"
:to="entry.path"
class="floating-actions__item"
:title="entry.label"
>
<span class="floating-actions__icon">{{ entry.icon }}</span>
<span class="floating-actions__label">{{ entry.label }}</span>
</RouterLink>
</div>
</template>
<style lang="less" scoped>
.floating-actions {
position: fixed;
right: 24px;
bottom: 96px;
z-index: 90;
.flex(column, center);
gap: @spacing-sm;
&__item {
.flex(column, center);
gap: 2px;
width: 56px;
padding: @spacing-sm 0;
border-radius: @radius-lg;
background: @primary-color;
color: @white;
box-shadow: @shadow-lg;
transition: background 0.2s, transform 0.2s;
&:hover {
background: @primary-hover;
transform: translateY(-2px);
}
}
&__icon {
font-size: 18px;
}
&__label {
font-size: 10px;
}
}
</style>

View File

@@ -1,40 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { AppFooter, AppHeader, FilePreviewDialog } from '@/components' import { AppFooter, AppHeader, FilePreviewDialog } from '@/components'
import FloatingActions from './components/FloatingActions.vue'
const route = useRoute()
/** 公共底栏只在首页展示(首页路由 meta.footer = true),其余页面不渲染 */
const showFooter = computed(() => route.meta.footer === true)
</script> </script>
<template> <template>
<div class="layout"> <div class="layout">
<AppHeader /> <AppHeader />
<main <main class="layout__main">
class="layout__main"
:class="{ 'layout__main--full': !showFooter }"
>
<router-view /> <router-view />
</main> </main>
<AppFooter v-if="showFooter" /> <AppFooter />
<FloatingActions />
<!-- 全局文件预览弹窗(同源 OSS 预览,绕开桶强制下载) --> <!-- 全局文件预览弹窗(同源 OSS 预览,绕开桶强制下载) -->
<FilePreviewDialog /> <FilePreviewDialog />
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 公共底栏全站常驻(设计稿 43 张中除登录类外均带 481 高页脚)
.layout { .layout {
.layout__main { .flex(column, flex-start, stretch);
min-height: calc(100vh - @header-height - @footer-height);
// 无底栏的页面:底栏不渲染,这里把它的占位高度还给内容区,避免页面底部空出一屏 &__main {
&--full { flex: 1;
min-height: calc(100vh - @header-height); min-height: calc(100vh - @header-height - @footer-height);
}
} }
} }
</style> </style>

View File

@@ -4,6 +4,14 @@ import { storage } from '@/utils/storage'
/** 路由守卫:登录态校验 + document.title */ /** 路由守卫:登录态校验 + document.title */
export function setupGuard(router: Router): void { export function setupGuard(router: Router): void {
router.beforeEach((to) => { router.beforeEach((to) => {
// 视觉走查旁路:仅本地开发且带 ?visual=1 时放行,便于逐页比对设计稿
if (
import.meta.env.DEV
&& new URLSearchParams(window.location.search).has('visual')
) {
return true
}
if (to.meta.requiresAuth && !storage.get<string>('token')) { if (to.meta.requiresAuth && !storage.get<string>('token')) {
return { path: '/login', query: { redirect: to.fullPath } } return { path: '/login', query: { redirect: to.fullPath } }
} }

View File

@@ -1,7 +1,7 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
/** /**
* 公共布局(AppHeader + router-view + AppFooter + FloatingActions)下的路由 * 公共布局(AppHeader + router-view + AppFooter)下的路由
* 完整规划见 docs/page-architecture.md 第 2 节 * 完整规划见 docs/page-architecture.md 第 2 节
* M3–M10 视图由子代理并行实现中,交付后本表即为终态 * M3–M10 视图由子代理并行实现中,交付后本表即为终态
*/ */
@@ -28,7 +28,7 @@ const layoutRoutes: RouteRecordRaw = {
{ path: 'finance/bills', name: 'Bills', component: () => import('@/views/finance/bills.vue'), meta: { title: '账单明细', requiresAuth: true } }, { path: 'finance/bills', name: 'Bills', component: () => import('@/views/finance/bills.vue'), meta: { title: '账单明细', requiresAuth: true } },
{ path: 'finance/bills/:id', name: 'BillDetail', component: () => import('@/views/finance/bill-detail.vue'), meta: { title: '账单详情', requiresAuth: true } }, { path: 'finance/bills/:id', name: 'BillDetail', component: () => import('@/views/finance/bill-detail.vue'), meta: { title: '账单详情', requiresAuth: true } },
{ path: 'finance/bills/:id/pay', name: 'BillPay', component: () => import('@/views/finance/pay.vue'), meta: { title: '扫码支付', requiresAuth: true } }, { path: 'finance/bills/:id/pay', name: 'BillPay', component: () => import('@/views/finance/pay.vue'), meta: { title: '扫码支付', requiresAuth: true } },
{ path: 'finance/bills/:id/pay/result', name: 'PayResult', component: () => import('@/views/finance/pay-result.vue'), meta: { title: '支付结果', requiresAuth: true } }, { path: 'finance/bills/:id/pay/result', name: 'PayResult', component: () => import('@/views/finance/pay-result.vue'), meta: { title: '支付成功', requiresAuth: true } },
// 收银台(确认支付):由账单明细勾选/单笔「去支付」带入 `?ids=bill-001,bill-002` // 收银台(确认支付):由账单明细勾选/单笔「去支付」带入 `?ids=bill-001,bill-002`
{ path: 'finance/checkout', name: 'Checkout', component: () => import('@/views/finance/checkout.vue'), meta: { title: '确认支付', requiresAuth: true } }, { path: 'finance/checkout', name: 'Checkout', component: () => import('@/views/finance/checkout.vue'), meta: { title: '确认支付', requiresAuth: true } },
{ path: 'finance/invoices', name: 'Invoices', component: () => import('@/views/finance/invoices.vue'), meta: { title: '发票申请', requiresAuth: true } }, { path: 'finance/invoices', name: 'Invoices', component: () => import('@/views/finance/invoices.vue'), meta: { title: '发票申请', requiresAuth: true } },

View File

@@ -3,7 +3,6 @@ import { computed, ref } from 'vue'
import type { BusinessType } from '@/types/api' import type { BusinessType } from '@/types/api'
import type { Case } from '@/types/business/cases' import type { Case } from '@/types/business/cases'
import type { import type {
BannerVo,
BusinessLine, BusinessLine,
HomeStats, HomeStats,
ProjectStatusCount, ProjectStatusCount,
@@ -15,7 +14,6 @@ import type {
} from '@/types/business/home' } from '@/types/business/home'
import type { PortalEntryGroup, PortalEntryVo } from '@/types/business/home' import type { PortalEntryGroup, PortalEntryVo } from '@/types/business/home'
import { import {
getCarouselList,
getPortalEntryList, getPortalEntryList,
getProjectStatusCount, getProjectStatusCount,
getServiceTypeDetail, getServiceTypeDetail,
@@ -132,7 +130,6 @@ export const useHomeStore = defineStore('home', () => {
const casesStore = useCasesStore() const casesStore = useCasesStore()
const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 }) const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 })
const banners = ref<BannerVo[]>([])
const businessLines = ref<BusinessLine[]>([]) const businessLines = ref<BusinessLine[]>([])
const featuredCases = ref<Case[]>([]) const featuredCases = ref<Case[]>([])
const currentBusiness = ref<BusinessLine | null>(null) const currentBusiness = ref<BusinessLine | null>(null)
@@ -188,11 +185,10 @@ export const useHomeStore = defineStore('home', () => {
/** 首页初始化:看板 + 轮播图 + 业务服务线 + 案例精选 */ /** 首页初始化:看板 + 轮播图 + 业务服务线 + 案例精选 */
async function fetchHomeData(): Promise<void> { async function fetchHomeData(): Promise<void> {
const [statusCount, submissionList, cases, carouselList] = await Promise.all([ const [statusCount, submissionList, cases] = await Promise.all([
getProjectStatusCount(), getProjectStatusCount(),
getServiceTypeList(), getServiceTypeList(),
getFeaturedCasesApi(), getFeaturedCasesApi(),
getCarouselList(),
// 案例标签需要「提报 id → 类型名」字典,与首页其余数据并行取(幂等,已加载则不发请求) // 案例标签需要「提报 id → 类型名」字典,与首页其余数据并行取(幂等,已加载则不发请求)
casesStore.ensureCategories(), casesStore.ensureCategories(),
]) ])
@@ -205,7 +201,6 @@ export const useHomeStore = defineStore('home', () => {
submissions.value = submissionList submissions.value = submissionList
businessLines.value = submissionList.map(toBusinessLine) businessLines.value = submissionList.map(toBusinessLine)
featuredCases.value = cases.map((vo) => toCase(vo, casesStore.typeNameById)) featuredCases.value = cases.map((vo) => toCase(vo, casesStore.typeNameById))
banners.value = carouselList ?? []
// 入口个性化接口失败已被内部吞掉,不会影响上面的首页数据 // 入口个性化接口失败已被内部吞掉,不会影响上面的首页数据
await fetchPortalEntries() await fetchPortalEntries()
} }
@@ -245,7 +240,6 @@ export const useHomeStore = defineStore('home', () => {
return { return {
stats, stats,
banners,
businessLines, businessLines,
featuredCases, featuredCases,
currentBusiness, currentBusiness,

View File

@@ -1,10 +1,113 @@
// 通用工具类 // 通用工具类
// 内容容器:设计稿内容宽 1780(x70→1850,specs2 全量复核)
// 1920 视口下 padding 70 → 内容 1780;窄视口只收窄留白,不产生横向滚动。
// ⚠️ 不能用固定 70px 留白:窄视口下 1780+140 > 视口宽 → 横向滚动条。
.container { .container {
width: 100%; width: 100%;
max-width: @container-width; max-width: @container-width;
margin: 0 auto; margin: 0 auto;
padding: 0 @spacing-base; // 留白夹在 16 与 70 之间:(1920 − 1780) / 2 = 70
padding: 0 clamp(@spacing-base, calc((100% - @page-content) / 2), @page-gutter);
}
// 首页容器:首页设计稿单独基准(内容 1680,留白 120)
.container--home {
padding: 0 @home-gutter;
@media (max-width: 1919px) {
padding: 0 @spacing-lg;
}
}
// ============ 登录 / 注册 / 找回密码(蓝湖设计稿 02·51·54·55) ============
// 三页共用外壳:左侧 784 品牌渐变面板 + 右侧白底表单区。
// 各页差异仅在「表单列起点 x」与「垂直顶 y」,由页面级 CSS 变量覆写。
// 设计稿锚点:面板 (0,0) 784×1080;主标 (257,304) 40/700;标语 (257,715) 22/600。
.auth-page {
--auth-form-x: 1022px; // 表单列起点(账号密码登录)
--auth-form-y: 306px; // 表单列垂直顶
position: relative;
display: flex;
min-height: 100vh;
background: @white;
&__brand {
.flex(column, flex-start, flex-start);
flex: 0 0 @login-brand-width;
padding: 304px 0 0 257px;
background: linear-gradient(160deg, @auth-gradient-from 0%, @auth-gradient-to 100%);
color: @white;
}
&__logo {
margin: 0;
font-size: 40px;
font-weight: 700;
line-height: 58px;
color: @white;
}
&__key {
margin: 26px 0 0;
font-size: 15px;
font-weight: 400;
letter-spacing: 0.5px;
color: @white;
}
&__slogan {
margin: 246px 0 0;
font-size: 22px;
font-weight: 600;
line-height: 29px;
letter-spacing: 8px;
color: @white;
}
&__guest {
position: absolute;
top: 24px;
right: 16px;
font-size: 12px;
font-weight: 400;
color: #1f2937;
text-decoration: none;
transition: color 0.2s;
&:hover {
color: @primary-strong;
}
}
&__main {
.flex(column, flex-start, flex-start);
flex: 1;
min-width: 0;
padding: var(--auth-form-y) 0 0 calc(var(--auth-form-x) - @login-brand-width);
}
&__card {
.flex(column, flex-start, flex-start);
width: 693px;
}
&__title {
margin: 0;
font-size: 34px;
font-weight: 700;
line-height: 45px;
color: #1f2937;
}
&__link {
color: @primary-strong;
&:hover {
color: @primary-hover;
}
}
} }
.link { .link {

View File

@@ -1,3 +1,14 @@
// 样式入口(main.ts 引入;variables/mixins 由 vite additionalData 自动注入) // 样式入口(main.ts 引入;variables/mixins 由 vite additionalData 自动注入)
@import './reset.less'; @import './reset.less';
@import './common.less'; @import './common.less';
// 全局视觉缩放:设计 1920 画布整体 ×0.67(变量见 variables.less @page-zoom)。
// zoom 由布局引擎重排,等价于屏幕逻辑分辨率放大 1/zoom,通栏/居中/滚动行为全部保持。
// ≤1286px 物理视口(1280 屏等)降到 0.66,保证 1920 逻辑内容仍无横向滚动。
html {
zoom: @page-zoom;
@media (max-width: 1286px) {
zoom: @page-zoom--sm;
}
}

View File

@@ -15,7 +15,7 @@ body {
body { body {
color: @text-color; color: @text-color;
font-family: 'PingFang SC', 'Source Han Sans SC', 'Microsoft YaHei', -apple-system, 'Helvetica Neue', Arial, sans-serif; font-family: 'PingFang SC', 'Source Han Sans SC', 'Microsoft YaHei', -apple-system, 'Helvetica Neue', Arial, sans-serif;
font-size: @font-size-base; font-size: 16px;
line-height: 1.5; line-height: 1.5;
background-color: @bg-color; background-color: @bg-color;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View File

@@ -1,11 +1,14 @@
// ★ less 变量唯一来源(颜色/间距/字号/圆角) // ★ less 变量唯一来源(颜色/间距/字号/圆角)
// 注:下列色值已按蓝湖设计稿(lanhu pid=b0052b6c)标注实测校准, // 数据来源:蓝湖 MCP 实测(lanhu pid=b0052b6c,43 张设计稿已全量提取到本地,规格见 specs2/)。
// 换算约定:设计画布内容宽 1780px → 应用内容宽 1368px(.container 1400 − 2×16), // 换算约定:**1:1 像素级还原**——设计画布 1920px。直接使用设计标注 px,不做任何缩放。
// 即 设计px × 0.77 ≈ CSS px;小字号(设计 ≤12px)按可读下限不跟缩。 // ⚠️ 内容区宽度按页面分两档(43 张设计稿实测结论):
// · 常规页(案例库/需求提报/项目/合同/财务/消息/客服/个人中心):内容宽 **1780**,左右留白各 **70**
// · 首页:内容宽 **1680**,左右留白各 **120**(首页设计稿单独一套基准)
// · 登录页:左品牌栏 784 + 右表单区 1136(无顶栏/页脚)
// 品牌色(设计稿实测:填充按钮/下划线 #1F67EF,内容强调 #024FFF) // 品牌色(设计稿实测)
@primary-color: #1f67ef; @primary-color: #1f67ef;
@primary-strong: #024fff; @primary-strong: #024fff; // 内容强调 / 选中态(设计实测 #024FFF)
@primary-hover: #598bff; @primary-hover: #598bff;
@primary-light: #eaf1ff; @primary-light: #eaf1ff;
@@ -15,17 +18,18 @@
@danger-color: #ef4444; @danger-color: #ef4444;
@info-color: #8a94a6; @info-color: #8a94a6;
// 文字色(设计稿灰阶:#1F2937 / #4B5563 / #6B7280 / #9CA3AF) // 文字色(设计稿灰阶:#121212 / #333333 / #666666 / #999999)
@heading-color: #1f2937; @heading-color: #121212;
@text-color: #4b5563; @text-color: #333333;
@text-color-secondary: #6b7280; @text-color-secondary: #666666;
@text-color-disabled: #9ca3af; @text-color-disabled: #999999;
// 背景 / 边框(设计稿实测:页面 #F4F6F8、顶栏 #F4F5F9、顶栏下边线 #D8D8D8、页脚 #0C1321) // 背景 / 边框(设计稿实测)
@bg-color: #f5f6f8; @bg-color: #f5f6f8;
@bg-color-light: #fafbfc; @bg-color-light: #fafbfc;
@border-color: #e4e7ec; @border-color: #e4e7ec;
@border-color-dark: #cdd3dd; @border-color-dark: #cdd3dd;
@border-color-btn: #d8dbdf; // 注销确认弹窗「暂不注销」底/描边(设计实取)
@white: #ffffff; @white: #ffffff;
@black: #000000; @black: #000000;
@header-bg: #f4f5f9; @header-bg: #f4f5f9;
@@ -47,20 +51,63 @@
@spacing-xl: 32px; @spacing-xl: 32px;
@spacing-xxl: 48px; @spacing-xxl: 48px;
// 字号 // 字号(1:1,设计实测体系 —— 43 张设计稿全量统计:16px 为出现最多的正文级字号)
@font-size-xs: 10px;
@font-size-sm: 12px; @font-size-sm: 12px;
@font-size-base: 14px; @font-size-base: 14px;
@font-size-lg: 16px; @font-size-body: 16px; // 正文基准(设计稿最高频字号;body 默认值)
@font-size-md: 16px;
@font-size-lg: 18px; // 顶栏导航 / 小标题(设计实测 18/500)
@font-size-xl: 20px; @font-size-xl: 20px;
@font-size-title: 22px; // 详情页主标题(项目详情设计稿 28 设计px × 0.77) @font-size-title: 28px; // 栏目标题(设计实测 28/500)
@font-size-xxl: 28px; @font-size-xxl: 36px; // 大标题(设计实测 36/700)
@font-size-hero: 48px; // 头图主标题(设计实测 48/500、52/700、60/700)
@font-size-hero-lg: 60px; // 首页头图主标题(设计实测 60/700)
// 布局(容器宽度按设计稿标注校准:设计稿为约 1400 宽的整幅版面) // 首页头图(设计画板 y73 起,1920×661)
@container-width: 1400px; @banner-title-color: #12171A; // 头图标题/副标题色(设计实取)
@header-height: 64px; @btn-gradient-from: #A061FE; // 主按钮渐变起(设计实取)
@header-nav-gap: 72px; // 顶栏导航项间距(设计 94px × 0.77;≤1280 视口降为 32px) @btn-gradient-to: #045DFF; // 主按钮渐变止(设计实取)
@footer-height: 240px; @btn-ghost-color: #2B5FF0; // 描边按钮文字色(设计像素实测)
@footer-inset: 65px; // 页脚内容左右内缩(设计 85px × 0.77,页脚比正文更窄)
// 布局(1:1,设计实测)
@container-width: 1920px; // 设计画布 / 通栏块总宽
// 全局视觉缩放:设计稿按 1920 画布 1:1 实现,整体观感偏大,
// 按用户要求整体缩放 67%(html zoom,等比缩放、比例关系与通栏布局不破坏)。
// 1920 物理屏逻辑视口 ≈ 2866 ≥ 1920 → 内容居中、无横向滚动。
// 小屏适配(自行判断):物理视口 <1287px(如 1280 屏)时 1920×0.67≈1286 放不下,
// 降到 0.66 → 1280/0.66 ≈ 1939 ≥ 1920,仍无横向滚动;1366 屏 1366/0.67≈2039 ✓ 不需降档。
@page-zoom: 0.67;
@page-zoom--sm: 0.66;
// 常规页内容区(specs2 全量复核 2026-09-30:案例库/需求提报/项目/合同/财务/消息详情/客服/
// 收藏/发票抬头/业务详情 共 17 张设计稿内容块均为 x70→1850 = **1780 宽**;此前误标 155/1600
// 导致整站内容右移 85px、窄 170px,已修正回归设计稿)
@page-content: 1780px; // 常规页内容实宽(设计实测 x70→1850)
@page-gutter: 70px; // 常规页左右留白(设计实测)
// 个人中心系(个人中心/账号安全/地址管理等 13 张)视觉核对定版基准,独立不联动:
// 左列 x155 宽 200 + 右卡,视觉比对设计稿裁片确认(specs 嵌套组坐标为组内相对值,不可直接用)
@profile-content: 1600px;
@profile-gutter: 155px;
// 首页专用(首页设计稿单独基准:x120→1800)
@home-content: 1680px; // 首页内容实宽
@home-gutter: 120px; // 首页左右留白
// 兼容旧引用(= 常规页基准,即绝大多数页面)
@content-width: @page-content;
@container-width-inner: @page-content;
@header-height: 72px; // 顶栏高(设计实测 72px)
@header-nav-gap: 94px; // 顶栏导航项文字间距(设计实测:首页 x275→311,案例库 x405)
@footer-height: 481px; // 页脚高(设计实测 481px)
@footer-inset: 70px; // 页脚内容左右内缩(与常规页内容区对齐)
// 登录页(无顶栏/页脚)
@login-brand-width: 784px; // 左侧品牌渐变栏
@login-form-offset: 1022px; // 右侧表单区起点
// 阴影 // 阴影
@shadow-base: 0 2px 8px rgba(31, 39, 51, 0.08); @shadow-base: 0 2px 8px rgba(31, 39, 51, 0.08);
@@ -194,6 +241,33 @@
@tag-info-bg: #f3f4f6; // 徽标底:未支付 / 中性(设计 09 实取) @tag-info-bg: #f3f4f6; // 徽标底:未支付 / 中性(设计 09 实取)
@milestone-line: #eeeff2; // 里程碑未完成连接线(设计 13 实取) @milestone-line: #eeeff2; // 里程碑未完成连接线(设计 13 实取)
// 项目详情(蓝湖设计稿 5 张:项目进度 / 项目文件 / 合同财务 / 售后信息 / 申请售后)
// ⚠️ 换算约定:**1:1 像素级还原**,设计画布 1920px,直接使用设计标注 px,不做缩放。
// 实测锚点(1920 视口):面包屑 (70,116) 24/500 #666666;主标题 (142,212) 19/700;
// 页签行 (70,278) 1780×40,文字 13/600 激活 / 13/400 常规,节距 86;
// 分隔线 (70,317) 1780×1 #F0F1F3;区块标题 15/700 @(82,y)。
@project-ink: #1f2937; // 主标题 / 区块标题 / 文件名(设计实取 #1F2937)
@project-label: #6b7280; // 「项目编号:」等字段标签(设计实取 #6B7280)
@project-value: #1f2937; // 字段值(设计实取 #1F2937)
@project-muted: #9ca3af; // 次行说明:上传时间 / 占位文字(设计实取 #9CA3AF)
@project-cell: #4b5563; // 只读值 / 行内操作「预览·下载」(设计实取 #4B5563)
@project-card: #ffffff; // 表格区白底(设计实取)
@project-field-bg: #f3f4f6; // 只读字段底(设计实取 #F3F4F6)
@project-input-border: #dde1e7; // 只读框 / 输入框描边(设计实取 #DDE1E7)
@project-head-bg: #f7f8fa; // 里程碑 / 文件行浅底(设计实取 #F7F8FA)
@project-upload-tag: #ef4444; // 上传文档红字(设计实取 #EF4444)
@project-banner-soft: #fdeeee; // 售后工单横幅底(设计实取 #FDEEEE)
// 项目详情 → 里程碑(设计实测)
@milestone-node: 356px; // 节点列宽(相邻节点 x70→426 = 356)
@milestone-dot: 26px; // 已完成实心点直径(x235..261 = 26)
@milestone-ring: #e5e7eb; // 待开始连接线(设计实取 #E5E7EB)
@milestone-idle-text: #9ca3af; // 待开始 / 进行中的状态说明(设计实取 #9CA3AF)
// 项目详情 → 项目团队(设计实测:头像 64,相邻中心距 99 → 间距 35)
@team-size: 64px;
@team-gap: 35px;
// 合同中心(蓝湖设计稿) // 合同中心(蓝湖设计稿)
// 设计索引:41 顶部横幅+搜索栏,46/47 表格+分页,40/44/52 合同详情,45 面包屑,39 签署。 // 设计索引:41 顶部横幅+搜索栏,46/47 表格+分页,40/44/52 合同详情,45 面包屑,39 签署。
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 11px,不跟缩。 // ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 11px,不跟缩。
@@ -245,6 +319,8 @@
@profile-line: #e5e7eb; // 按钮描边 / 分隔线(设计 28 注销按钮描边实取 (229,231,235)) @profile-line: #e5e7eb; // 按钮描边 / 分隔线(设计 28 注销按钮描边实取 (229,231,235))
@profile-input-border: #dde1e7; // 输入框描边(设计 22 实取 (221,225,231)) @profile-input-border: #dde1e7; // 输入框描边(设计 22 实取 (221,225,231))
@profile-radius: 6px; // 输入框 / 小按钮 / 菜单选中项圆角(设计 8px × 0.77) @profile-radius: 6px; // 输入框 / 小按钮 / 菜单选中项圆角(设计 8px × 0.77)
@profile-page-bg: #f4f6f8; // 个人中心页底色(设计稿实取)
@profile-ink: #1f2937; // 个人中心标题字色(设计稿实取)
@profile-hotline-bg: #d6e4fb; // 专属客服热线卡底色(设计 14 实取 rgba(214,228,251)) @profile-hotline-bg: #d6e4fb; // 专属客服热线卡底色(设计 14 实取 rgba(214,228,251))
@profile-icon-pwd: #c7d2fe; // 账号安全「修改密码」图标块底(设计 28 实取) @profile-icon-pwd: #c7d2fe; // 账号安全「修改密码」图标块底(设计 28 实取)
@profile-icon-phone: #fdebeb; // 账号安全「绑定手机」图标块底(设计 28 实取) @profile-icon-phone: #fdebeb; // 账号安全「绑定手机」图标块底(设计 28 实取)
@@ -257,6 +333,13 @@
// 合同中心(蓝湖设计稿)· 补充 // 合同中心(蓝湖设计稿)· 补充
@contract-ink: #121212; // 页面最深标题色(设计实取 #121212) @contract-ink: #121212; // 页面最深标题色(设计实取 #121212)
@contract-page-bg: #f4f6f8; // 页面底色 / 详情只读值框底(设计 e6c3a361 实取 #F4F6F8)
// 财务中心(蓝湖设计稿)· 补充 // 财务中心(蓝湖设计稿)· 补充
@finance-pay-bg: #f7f8f9; // 扫码支付页底色(设计实取 #f7f8f9) @finance-pay-bg: #f7f8f9; // 扫码支付页底色(设计实取 #f7f8f9)
// 财务中心概览 / 账单(设计 6cd4aa76 等,1920 画布 1:1 实取)
@finance-muted-text: #6b7280; // 副标语 / 图表刻度 / 图例 / 次级文字(设计实取 #6B7280)
@finance-card-line: #f0f1f3; // 卡片描边(设计实取 #F0F1F3)
@finance-entry-text: #4b5563; // 快捷入口文字(设计实取 #4B5563)
@finance-axis-text: #9ca3af; // 图表 Y 轴刻度(设计实取 #9CA3AF)
@finance-ink: #1f2937; // 区块标题(设计实取 #1F2937)

View File

@@ -309,15 +309,17 @@ function clearChips(): void {
<style lang="less" scoped> <style lang="less" scoped>
.case-filter { .case-filter {
flex-shrink: 0; flex-shrink: 0;
width: 397px; // 设计04 面板 516 × 0.77 // 设计稿筛选面板 矩形 148286 @(70,272) 516×1461 白底
padding: 12px 0 40px; // 顶 15 × 0.77 / 底 50 × 0.77 width: 516px;
padding: 15px 0 40px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__head, &__head,
&__group, &__group,
&__actions { &__actions {
padding: 0 38px 0 27px; // 内容左 35 × 0.77 / 右 50 × 0.77 // 设计稿内容左 x105 → 卡内 x35;行右 x536 → 卡内右 50
padding: 0 50px 0 35px;
} }
&__head { &__head {
@@ -325,14 +327,16 @@ function clearChips(): void {
} }
&__title { &__title {
font-size: 22px; // 设计 28 × 0.77 // 设计 @(105,287) 28/500 #222222
font-size: 28px;
font-weight: 500; font-weight: 500;
line-height: 41px;
color: @cases-filter-title; color: @cases-filter-title;
} }
&__link { &__link {
color: @primary-strong; color: @primary-strong;
font-size: @font-size-base; font-size: 16px;
&:hover { &:hover {
color: @primary-hover; color: @primary-hover;
@@ -340,33 +344,35 @@ function clearChips(): void {
} }
&__group { &__group {
margin-top: @spacing-lg; margin-top: 24px;
} }
// 标题 → 第一组(设计 11 × 0.77) // 标题 → 第一组(设计 287+41=328 → 业务类型标题 339)
&__head + &__group { &__head + &__group {
margin-top: 9px; margin-top: 11px;
} }
&__group-title { &__group-title {
font-size: 18px; // 设计 24 × 0.77 // 设计 @(105,339) 24/500 #333333
font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 35px;
color: @cases-text; color: @cases-text;
} }
/* 业务类型:整行可点,选中整行铺满底色 */ /* 业务类型:整行可点(设计行高 50,文案 20/400) */
&__rows { &__rows {
margin-top: 14px; // 设计 19 × 0.77 margin-top: 19px;
} }
&__row { &__row {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 9px; // 设计 12 × 0.77 gap: 12px;
width: 100%; width: 100%;
height: 38px; // 行距 50 × 0.77 height: 50px;
padding: 0; padding: 0;
color: @cases-text; color: @cases-text;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
text-align: left; text-align: left;
transition: background 0.2s; transition: background 0.2s;
@@ -384,8 +390,8 @@ function clearChips(): void {
&__box { &__box {
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
width: 12px; // 设计 16 × 0.77 width: 16px;
height: 12px; height: 16px;
border: 1px solid @cases-input-border; border: 1px solid @cases-input-border;
border-radius: 2px; border-radius: 2px;
@@ -397,11 +403,11 @@ function clearChips(): void {
content: ''; content: '';
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 3px; left: 4px;
width: 4px; width: 5px;
height: 6px; height: 8px;
border: solid @white; border: solid @white;
border-width: 0 1.5px 1.5px 0; border-width: 0 2px 2px 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
} }
@@ -413,31 +419,32 @@ function clearChips(): void {
} }
&__row-count { &__row-count {
// 设计计数 16/500 #666666
color: @cases-muted; color: @cases-muted;
font-weight: 400; font-size: 16px;
font-weight: 500;
} }
// 选中行的计数随行文字变蓝、加粗(设计15)
.case-filter__row.is-active &__row-count { .case-filter__row.is-active &__row-count {
color: inherit; color: inherit;
font-weight: inherit; font-weight: inherit;
} }
/* 胶囊选项(两列;minmax(0,1fr) 防止内部长内容(日期区间)把列撑宽) */ /* 胶囊选项(两列;设计胶囊高 53、行距 24、列距 33) */
&__pills { &__pills {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px 25px; // 行距 24 × 0.77 / 列距 33 × 0.77 gap: 24px 33px;
margin-top: 15px; // 设计 21 × 0.77 margin-top: 21px;
} }
&__pill { &__pill {
height: 41px; // 设计 53 × 0.77 height: 53px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @pill-bg; background: @pill-bg;
color: @cases-text; color: @cases-text;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
.ellipsis(); .ellipsis();
transition: all 0.2s; transition: all 0.2s;
@@ -459,7 +466,7 @@ function clearChips(): void {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
height: 41px; height: 53px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @input-bg; background: @input-bg;
@@ -478,10 +485,10 @@ function clearChips(): void {
:deep(.el-range-input) { :deep(.el-range-input) {
flex: 0 1 auto; flex: 0 1 auto;
width: 76px; width: 92px;
background: transparent; background: transparent;
color: @cases-text; color: @cases-text;
font-size: 15px; font-size: 20px;
&::placeholder { &::placeholder {
color: @cases-text; color: @cases-text;
@@ -490,8 +497,8 @@ function clearChips(): void {
:deep(.el-range-separator) { :deep(.el-range-separator) {
color: @cases-text; color: @cases-text;
font-size: 15px; font-size: 20px;
line-height: 41px; line-height: 53px;
} }
&.is-focus { &.is-focus {
@@ -501,8 +508,8 @@ function clearChips(): void {
/* 已选条件 */ /* 已选条件 */
&__selected { &__selected {
margin: 18px 38px 0 27px; // 上距 23 × 0.77,左右与内容对齐(50/35 × 0.77) margin: 23px 50px 0 35px;
padding: 12px 18px 16px; // 设计 16/24/21 × 0.77 padding: 16px 24px 21px;
border-radius: @radius-base; border-radius: @radius-base;
background: @chips-panel-bg; background: @chips-panel-bg;
} }
@@ -513,30 +520,30 @@ function clearChips(): void {
&__selected-title { &__selected-title {
color: @cases-text; color: @cases-text;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
font-weight: 500; font-weight: 500;
} }
&__selected-empty { &__selected-empty {
margin-top: @spacing-sm; margin-top: @spacing-sm;
color: @cases-muted; color: @cases-muted;
font-size: @font-size-sm; font-size: 14px;
} }
&__chips { &__chips {
.flex(row, flex-start, center); .flex(row, flex-start, center);
flex-wrap: wrap; flex-wrap: wrap;
gap: 11px 10px; // 行距 14 × 0.77 / 列距 13 × 0.77 gap: 14px 13px;
margin-top: 12px; // 设计 16 × 0.77 margin-top: 16px;
} }
&__chip { &__chip {
height: 25px; // 设计 32 × 0.77 height: 32px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
color: @cases-accent; color: @cases-accent;
font-size: 11px; // 设计 14 → 10.8,取可读下限 11 font-size: 14px;
font-weight: 500; font-weight: 500;
transition: color 0.2s; transition: color 0.2s;
@@ -548,17 +555,17 @@ function clearChips(): void {
/* 底部操作 */ /* 底部操作 */
&__actions { &__actions {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 10px; // 设计 13 × 0.77 gap: 13px;
margin-top: 23px; // 设计 30 × 0.77 margin-top: 30px;
} }
&__reset { &__reset {
width: 104px; // 设计 135 × 0.77 width: 135px;
height: 23px; // 设计 30 × 0.77 height: 30px;
border-radius: @radius-round; border-radius: @radius-round;
background: @cases-reset-bg; background: @cases-reset-bg;
color: @black; color: @black;
font-size: 11px; // 设计 14 → 10.8,取可读下限 11 font-size: 14px;
font-weight: 400; font-weight: 400;
transition: filter 0.2s; transition: filter 0.2s;
@@ -569,11 +576,11 @@ function clearChips(): void {
&__confirm { &__confirm {
flex: 1; flex: 1;
height: 23px; height: 30px;
border-radius: @radius-round; border-radius: @radius-round;
background: @cases-confirm; background: @cases-confirm;
color: @white; color: @white;
font-size: 11px; font-size: 14px;
font-weight: 400; font-weight: 400;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -41,22 +41,24 @@ const emit = defineEmits<{
<style lang="less" scoped> <style lang="less" scoped>
.cases-grid { .cases-grid {
&__total { &__total {
font-size: 15px; // 设计15 计数 20 × 0.77 // 设计计数 20px / 700 / #121212
font-size: 20px;
font-weight: 700; font-weight: 700;
color: @heading-color; color: @heading-color;
} }
// 设计卡片 396×267,列 x606/1030/1454 → 列距 28,行 y423/713 → 行距 23
&__list { &__list {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 18px 22px; // 设计04 行距 23 / 列距 28 × 0.77 gap: 23px 28px;
margin-top: 12px; // 设计15 计数 → 卡片 15 × 0.77 margin-top: 12px;
} }
&__empty { &__empty {
padding: @spacing-xxl 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;
font-size: @font-size-base; font-size: 16px;
} }
} }
</style> </style>

View File

@@ -60,25 +60,27 @@ const keyword = computed<string>({
<style lang="less" scoped> <style lang="less" scoped>
.case-search { .case-search {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 18px; // 按钮间距 24 × 0.77 // 设计稿搜索卡 矩形 148317 @(606,272) 1244×128;输入框 @(642,312) 592×49
padding: 30px 36px; // 卡片 128 高、输入 49 高:上下 40 × 0.77 / 左右 47 × 0.77 gap: 24px;
height: 128px;
box-sizing: border-box;
padding: 0 36px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__field { &__field {
position: relative; position: relative;
flex: 1; // 设计输入框 592 宽
max-width: 456px; // 设计输入框 592 × 0.77 flex: 0 0 592px;
margin-right: 28px; // 与按钮总间距 60 × 0.77(叠加 bar gap 18)
} }
&__icon { &__icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 16px; // 设计放大镜在输入框右侧 right: 16px;
left: auto; left: auto;
width: 20px; // 设计 27 × 0.77 width: 27px;
height: 20px; height: 27px;
transform: translateY(-50%); transform: translateY(-50%);
fill: none; fill: none;
stroke: @cases-icon; stroke: @cases-icon;
@@ -87,18 +89,19 @@ const keyword = computed<string>({
&__input { &__input {
width: 100%; width: 100%;
height: 38px; // 设计 49 × 0.77 height: 49px;
padding: 0 44px 0 16px; padding: 0 52px 0 20px;
border: 1px solid @cases-input-border; border: 1px solid @cases-input-border;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
color: @cases-text; color: @cases-text;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
&::placeholder { &::placeholder {
color: @cases-input-border; // 设计占位 @cases-input-border // 设计 placeholder @(660,322) 20/400 #999999
color: @text-color-disabled;
} }
&:focus { &:focus {
@@ -107,13 +110,15 @@ const keyword = computed<string>({
} }
&__btn { &__btn {
min-width: 77px; // 设计 100 × 0.77 // 设计按钮文字 20/400 #FFFFFF,白底蓝字按钮宽 100
height: 38px; // 设计 49 × 0.77 min-width: 100px;
height: 49px;
padding: 0 @spacing-base; padding: 0 @spacing-base;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @cases-accent; // 设计 重置/搜索 均为 rgba(7,92,255) background: @cases-accent;
color: @white; color: @white;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
font-weight: 400;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
@@ -136,7 +141,7 @@ const keyword = computed<string>({
gap: @spacing-xs; gap: @spacing-xs;
margin-left: auto; margin-left: auto;
color: @heading-color; color: @heading-color;
font-size: @font-size-base; font-size: 16px;
transition: color 0.2s; transition: color 0.2s;
&:hover, &:hover,
@@ -150,8 +155,8 @@ const keyword = computed<string>({
} }
&-icon { &-icon {
width: 16px; width: 18px;
height: 16px; height: 18px;
fill: none; fill: none;
stroke: currentColor; stroke: currentColor;
stroke-width: 1.8; stroke-width: 1.8;

View File

@@ -56,7 +56,7 @@ onMounted(() => {
<template> <template>
<div class="cases"> <div class="cases">
<header class="cases__hero"> <header class="cases__hero">
<div class="container"> <div class="container cases__hero-inner">
<h1 class="cases__title"> <h1 class="cases__title">
成功案例库 成功案例库
</h1> </h1>
@@ -111,37 +111,50 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.cases { .cases {
padding-bottom: 60px; // 设计04 正文 → 页脚 78 × 0.77 // 设计实测:页脚顶 y1811,正文最底约 y1733 → 底距 78
padding-bottom: 78px;
// 设计稿页头条 矩形 148284 @(0,73) 1920×179 白底通栏;文案左缩进 112
&__hero { &__hero {
padding: 25px 0 33px; // 标题上 32 / 标语下 43 × 0.77 height: 179px;
box-sizing: border-box;
padding-top: 32px; // 标题 @(112,105) → 73+32
background: @white; background: @white;
} }
// 页头文案与内容区不对齐:设计实测文案在 x112(内容区 x70 + 42)
&__hero-inner {
padding-left: 42px;
}
&__title { &__title {
font-size: @font-size-xxl; // 设计 36 × 0.77 ≈ 28 margin: 0;
font-size: 36px; // 设计 @(112,105) 36/700 #121212
font-weight: 700; font-weight: 700;
line-height: 52px;
color: @cases-hero-title; color: @cases-hero-title;
} }
&__slogan { &__slogan {
margin-top: 12px; // 设计 15 × 0.77 margin: 15px 0 0; // 副标题 @(112,172),标题底 105+52=157 → 间距 15
font-size: 18.5px; // 设计 24 × 0.77 font-size: 24px; // 设计 24/500 #666666
font-weight: 500; font-weight: 500;
line-height: 37px;
color: @cases-muted; color: @cases-muted;
} }
// 设计稿:筛选面板 @(70,272) 516 宽,卡片区 @(606,423) 起 1244 宽,主区间距 20
&__body { &__body {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: @spacing-base; gap: 20px;
padding-top: @spacing-base; padding-top: 20px; // 筛选面板顶 272 − 页头条底(73+179) = 20
} }
&__main { &__main {
.flex(column, flex-start); .flex(column, flex-start);
flex: 1; flex: 1;
gap: 18px; // 设计04 搜索卡 → 卡片网格 23 × 0.77
min-width: 0; min-width: 0;
gap: 24px;
} }
&__pager { &__pager {

View File

@@ -25,62 +25,61 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
<template> <template>
<div class="contract-info"> <div class="contract-info">
<!-- 只读信息网格 --> <!-- 区块 01:合同信息(相对卡顶 y36) -->
<div class="contract-info__grid"> <h3 class="contract-info__section contract-info__section--1">
<div class="contract-info__field"> <span class="contract-info__step">01</span>
<span class="contract-info__label text-secondary">合同名称</span> <span class="contract-info__section-title">合同信息</span>
</h3>
<div class="contract-info__field contract-info__field--1 contract-info__field--l">
<span class="contract-info__label">项目名称</span>
<span class="contract-info__value">{{ contract.contractName }}</span> <span class="contract-info__value">{{ contract.contractName }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--1 contract-info__field--r">
<span class="contract-info__label text-secondary">合同类型</span> <span class="contract-info__label">合同类型</span>
<span class="contract-info__value">{{ contract.contractType }}</span> <span class="contract-info__value">{{ contract.contractType }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--2 contract-info__field--l">
<span class="contract-info__label text-secondary">合同编号</span> <span class="contract-info__label">合同编号</span>
<span class="contract-info__value">{{ contract.contractNo }}</span> <span class="contract-info__value">{{ contract.contractNo }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--2 contract-info__field--r">
<span class="contract-info__label text-secondary">签订日期</span> <span class="contract-info__label">签订日期</span>
<span class="contract-info__value">{{ contract.signDate || '—' }}</span> <span class="contract-info__value">{{ contract.signDate || '—' }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--3 contract-info__field--l">
<span class="contract-info__label text-secondary">生效日期</span> <span class="contract-info__label">生效日期</span>
<span class="contract-info__value">{{ contract.effectiveDate }}</span> <span class="contract-info__value">{{ contract.effectiveDate }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--3 contract-info__field--r">
<span class="contract-info__label text-secondary">到期日期</span> <span class="contract-info__label">到期日期</span>
<span class="contract-info__value">{{ contract.expiryDate }}</span> <span class="contract-info__value">{{ contract.expiryDate }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--4 contract-info__field--l">
<span class="contract-info__label text-secondary">合同状态</span> <span class="contract-info__label">合同状态</span>
<StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" /> <StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" />
</div> </div>
</div>
<!-- 关联项目 --> <!-- 区块 02:关联项目(相对卡顶 y649) -->
<div class="contract-info__related"> <h3 class="contract-info__section contract-info__section--2">
<h3 class="contract-info__section-title"> <span class="contract-info__step">02</span>
关联项目 <span class="contract-info__section-title">关联项目</span>
</h3> </h3>
<div class="contract-info__related-body"> <div class="contract-info__field contract-info__field--5 contract-info__field--l">
<div class="contract-info__field"> <span class="contract-info__label">
<span class="contract-info__label text-secondary">
项目名称 项目名称
<button class="contract-info__link" @click="emit('viewProject')">查看详情</button> <button class="contract-info__link" @click="emit('viewProject')">查看详情</button>
</span> </span>
<span class="contract-info__value">{{ contract.relatedProjectName }}</span> <span class="contract-info__value">{{ contract.relatedProjectName }}</span>
</div> </div>
<div class="contract-info__field"> <div class="contract-info__field contract-info__field--5 contract-info__field--r">
<span class="contract-info__label text-secondary">项目编号</span> <span class="contract-info__label">项目编号</span>
<span class="contract-info__value">{{ contract.projectNo }}</span> <span class="contract-info__value">{{ contract.projectNo }}</span>
</div> </div>
</div>
</div>
<!-- 合同文件 --> <!-- 区块 03:合同文件(相对卡顶 y901) -->
<div class="contract-info__files-section"> <h3 class="contract-info__section contract-info__section--3">
<h3 class="contract-info__section-title"> <span class="contract-info__step">03</span>
合同文件 <span class="contract-info__section-title">合同文件</span>
</h3> </h3>
<ul class="contract-info__files"> <ul class="contract-info__files">
<li <li
@@ -90,7 +89,7 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
> >
<div class="contract-info__file-info"> <div class="contract-info__file-info">
<span class="contract-info__file-name" :title="file.name">{{ file.name }}</span> <span class="contract-info__file-name" :title="file.name">{{ file.name }}</span>
<span class="contract-info__file-meta text-secondary"> <span class="contract-info__file-meta">
{{ formatFileSize(file.size) }} · {{ file.uploadTime }} {{ formatFileSize(file.size) }} · {{ file.uploadTime }}
</span> </span>
</div> </div>
@@ -103,99 +102,132 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
</button> </button>
</div> </div>
</li> </li>
<li v-if="contract.files.length === 0" class="contract-info__empty text-secondary"> <li v-if="contract.files.length === 0" class="contract-info__empty">
暂无合同文件 暂无合同文件
</li> </li>
</ul> </ul>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 合同详情信息区(设计 40:灰底页面直排、标签 + 灰底值框、蓝竖条区块标题) // 合同详情信息区(设计 e6c3a361,1920 画布 1:1 绝对定位)
// 白卡 (70,152) 1780×1152;卡内三行「步骤区」相对卡顶:
// 区块 01 合同信息 y36(步骤号 ink 188、区块名 211)、首标签 283、值框 326
// 区块 02 关联项目 y649(步骤号 801、区块名 824)、首标签 901、值框 944
// 区块 03 合同文件 y901(步骤号 1053、区块名 1076)、文件行 1153
// 值框 764×49 @x134/1010(卡内左 64 / 940),描边 #999999、圆角 8、底 #F4F6F8
// 行距 122(标签 283 → 405 → 527 → 649);列间距 112(776 → 940 = 164 − 764)
.contract-info { .contract-info {
.flex(column, flex-start, stretch);
gap: 24px;
&__grid,
&__related-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 46px; // 列间距 60 设计 × 0.77
row-gap: 15px; // 行间距 20 设计 × 0.77
}
&__field {
.flex(column, flex-start, stretch);
gap: 6px; // 标签 → 值框(设计 8 × 0.77)
min-width: 0;
}
&__label {
font-size: 11px; // 设计 13 → 可读下限
font-weight: 500;
line-height: 1.4;
color: @heading-color; // 设计 @heading-color(覆盖 .text-secondary)
}
// 值框:设计 40 实取 #F3F4F6 底 + 浅描边,高 38 设计 × 0.77
&__value {
width: 100%;
height: 29px;
padding: 0 10px;
border: 1px solid @border-color;
border-radius: @radius-sm;
background: @input-bg;
font-size: 11px;
line-height: 27px;
color: @text-color; // 设计 @text-color
.ellipsis();
}
// 合同状态(StatusTag)同样入灰底值框,去彩色胶囊
&__field :deep(.status-tag) {
width: 100%;
height: 29px;
padding: 0 10px;
border: 1px solid @border-color;
border-radius: @radius-sm;
background: @input-bg;
font-size: 11px;
line-height: 27px;
color: @text-color;
}
// 区块标题:蓝色竖条(设计 40 实取 4×17 设计px #024FFF)+ 15 设计px 半粗
&__section-title {
position: relative; position: relative;
padding-left: 10px; width: 1780px;
margin-bottom: 14px; // 设计 18 × 0.77 height: 1152px;
font-size: @font-size-sm; // 12px = 设计 15 × 0.77 margin-top: 21px; // 面包屑盒底 131(72+32+27)→ 卡顶 152
font-weight: 700; border-radius: @radius-lg;
line-height: 16px; background: @white;
color: @heading-color; box-sizing: border-box;
&::before { // 区块(步骤号 + 区块名)—— 绝对定位,四个区块
content: ''; &__section {
position: absolute;
left: 43px; // 步骤号 x124 = 70 + 43(123)→ 区块行内相对 11 → 113 ✓
top: 0;
height: 0;
&--1 { top: 36px; }
&--2 { top: 649px; }
&--3 { top: 901px; }
}
&__section-title {
position: absolute;
left: 60px; // 区块名 ink x184 = 卡内 114 = 行 left 43 + 60
margin: 0;
font-size: 28px;
font-weight: 500;
line-height: 41px;
color: @contract-cell-text;
}
// 步骤号 01/02/03:48/500 #024FFF。absolute 定到区块名左侧 ——
// 区块行 left 43,区块名 ink 相对行左 60(= 184 − 124);步骤号 ink 落 0
&__step {
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
width: 4px; font-size: 48px;
height: 15px; font-weight: 500;
border-radius: @radius-round; line-height: 70px;
background: @primary-strong; color: @project-accent;
white-space: nowrap;
transform: translateY(-50%); transform: translateY(-50%);
} }
// 字段(标签 + 值框)—— 绝对定位到卡面,行间距 122
&__field {
.flex(column, flex-start, stretch);
position: absolute;
width: 764px;
gap: 14px; // 标签盒底 312 → 值框顶 326
} }
&__files-section { // 值框顶(相对卡顶 = 设计 y − 152):326/448/570/692/944
margin-top: 6px; // 区块间距合计 30(设计 40 × 0.77) &__field--1 { top: 174px; }
&__field--2 { top: 296px; }
&__field--3 { top: 418px; }
&__field--4 { top: 540px; }
&__field--5 { top: 770px; } // 标签 901 − 109(label 行盒 29 + gap 14 = 43 补偿)
// 第 5 行(关联项目)标签需落设计 y901 → 卡内 749,而 --5 槽位顶 770 是给
// 「合同状态」这类值框直排的字段用的;带标签的行统一再抬 21
&__field--5:has(&__label) { top: 749px; }
// 左右列:值框 x134 / x1010 → 卡内左 64 / 940
&__field--l { left: 64px; }
&__field--r { left: 940px; }
// 标签 20/500 #333
&__label {
font-size: 20px;
font-weight: 500;
line-height: 29px;
color: @contract-cell-text;
}
// 值框:764×49、#F4F6F8 底、1px #999999、圆角 8;文字内缩 18
&__value {
.flex(row, flex-start, center);
width: 100%;
height: 49px;
padding: 0 18px;
border: 1px solid @contract-line;
border-radius: 8px;
background: @contract-page-bg;
font-size: 20px;
line-height: 29px;
color: @contract-cell-text;
box-sizing: border-box;
.ellipsis();
}
// 合同状态(StatusTag)同样入值框,去彩色胶囊
&__field :deep(.status-tag) {
.flex(row, flex-start, center);
width: 100%;
height: 49px;
padding: 0 18px;
border: 1px solid @contract-line;
border-radius: 8px;
background: @contract-page-bg;
font-size: 20px;
line-height: 29px;
color: @contract-cell-text;
box-sizing: border-box;
} }
&__link { &__link {
font-size: 11px; // 设计 10 → 可读下限 font-size: 18px;
font-weight: 500; font-weight: 400;
color: @primary-strong; color: @contract-accent;
transition: opacity 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
@@ -203,54 +235,64 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
} }
} }
// 设计 40:「项目名称 查看详情」同一行内联
&__label &__link { &__label &__link {
margin-left: 3px; margin-left: 10px;
font-size: 16px;
} }
// ── 合同文件:通栏 1640×90 @(124,1153),底 #EDEDED ──
&__files { &__files {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-sm; position: absolute;
top: 1001px; // 1153 − 152
left: 54px; // 124 − 70
width: 1640px;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
} }
// 文件行:设计 40 浅灰通栏底(#F7F8FA),无描边
&__file { &__file {
.flex(row, space-between); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
min-height: 42px; // 通栏高 54 设计 × 0.77 height: 90px;
padding: 7px 16px; padding: 0 30px;
border: none; background: @contract-table-head-bg;
border-radius: @radius-base; box-sizing: border-box;
background: @finance-row-bg;
} }
&__file-info { &__file-info {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 2px; gap: 4px;
min-width: 0; min-width: 0;
} }
&__file-name { &__file-name {
font-size: 11px; font-size: 20px;
line-height: 1.3; line-height: 29px;
color: @heading-color; // 设计 @heading-color color: @contract-cell-text;
.ellipsis(); .ellipsis();
} }
&__file-meta { &__file-meta {
font-size: 11px; // 设计 11 → 可读下限 font-size: 14px;
line-height: 1.3; line-height: 20px;
color: @text-color-disabled; // 设计 @text-color-disabled color: @contract-muted;
} }
&__file-actions { &__file-actions {
.flex(row, center); .flex(row, center);
gap: @spacing-sm; gap: @spacing-base;
flex-shrink: 0; flex-shrink: 0;
} }
&__empty { &__empty {
font-size: @font-size-base; .flex(row, flex-start, center);
height: 90px;
padding: 0 30px;
font-size: 18px;
color: @contract-muted;
} }
} }
</style> </style>

View File

@@ -83,21 +83,21 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
overflow-x: auto; overflow-x: auto;
.scrollbar(); .scrollbar();
// 表格本体(设计 46:外框 1px #999、灰表头、横分隔线、无竖线、无卡面——卡面在页面层) // 表格本体(设计 31ee32fc:1654×740,外框 1px #999、灰表头 64、行高 97、无竖线)
&__grid { &__grid {
width: 100%; width: 100%;
border: 1px solid @contract-line; border: 1px solid @contract-line;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; table-layout: fixed;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
th, th,
td { td {
padding: 8px 10px; padding: 0 10px;
border-bottom: 1px solid @contract-line; border-bottom: 1px solid @contract-line;
vertical-align: middle; vertical-align: middle;
white-space: nowrap; white-space: nowrap;
font-size: 15px; // 设计 20 × 0.77 font-size: 20px;
} }
// 长名称列按设计稿换行显示(设计首行双行文本) // 长名称列按设计稿换行显示(设计首行双行文本)
@@ -107,18 +107,19 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
} }
th { th {
height: 49px; // 设计 64 × 0.77 height: 64px;
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
background: @contract-table-head-bg; background: @contract-table-head-bg;
color: @contract-head-text; color: @contract-head-text;
font-weight: 400; // 设计 400 font-weight: 400;
text-align: center; // 表头居中(设计实测) text-align: center;
} }
td { td {
height: 75px; // 行高 97 设计 × 0.77 height: 96px; // 设计行距 475→571
color: @contract-cell-text; color: @contract-cell-text;
line-height: 29px;
} }
tbody tr:last-child td { tbody tr:last-child td {

View File

@@ -78,35 +78,36 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.contract-detail { .contract-detail {
padding-top: @spacing-lg;
padding-bottom: @spacing-xxl; padding-bottom: @spacing-xxl;
// 内容列(面包屑 → 信息卡 → 返回):区块间距 24(设计 26~30 × 0.77) // 内容列(面包屑 → 信息卡):卡自带 80 上距,这里不再叠加 gap
.container { .container {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-lg; gap: 0;
} }
// 面包屑(设计 e6c3a361:(70,104) 24/500 #666666;顶栏 72 → 行盒顶 104)
// 面包屑(设计 45:24 设计px × 0.77、#666666、字重 500)
&__breadcrumb { &__breadcrumb {
font-size: 18px; padding-top: 32px;
font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 27px;
color: @contract-muted; color: @contract-muted;
} }
&__footer { &__footer {
.flex(row, flex-end); .flex(row, flex-end);
width: 100%; width: 100%;
margin-top: 30px;
// 「返回」按钮(设计 40/39:#024FFF 实底、白字、小圆角) // 「返回」按钮
:deep(.app-button) { :deep(.app-button) {
height: 30px; height: 40px;
padding: 0 24px; // 按钮宽约 90 设计 × 0.77 padding: 0 32px;
border: none; border: none;
border-radius: @radius-sm; border-radius: @radius-base;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 11px; // 设计 13 → 可读下限 font-size: 16px;
} }
:deep(.app-button:hover) { :deep(.app-button:hover) {

View File

@@ -86,43 +86,44 @@ onMounted(() => {
.contract { .contract {
padding-bottom: @spacing-xxl; padding-bottom: @spacing-xxl;
// 横幅:设计 41/46 实测 y72..251(179 设计px → 138 CSS)白底通栏 // 横幅:设计 31ee32fc y72..251(1920×179)白底通栏
// 标题盒顶 96(ink 104)、副标语盒顶 161.5(ink 171)、横幅底 251
&__head { &__head {
padding: 25px 0 33px; padding: 24px 0 52px;
background: @white; background: @white;
// 标题/副标语相对内容区左缩进 42 设计px × 0.77 ≈ 32(容器自带 16) // 标题/副标语落 x112 → 内容区左缘 70 + 42
.container { .container {
padding-left: 48px; padding-left: 42px;
} }
} }
&__title { &__title {
font-size: @font-size-xxl; // 28px = 设计 36 × 0.77 font-size: 36px;
font-weight: 700; font-weight: 700;
line-height: 1.4; line-height: 52px;
color: @home-ink; // 复用同值 token(横幅标题设计实取 @contract-ink) color: @contract-ink;
} }
&__slogan { &__slogan {
margin-top: 12px; // 设计 15 × 0.77 margin-top: 13.5px;
font-size: 18px; // 设计 24 × 0.77 font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 37px;
color: @contract-muted; color: @contract-muted;
} }
} }
// 内容白卡(设计 46:x70..1850 圆角白卡,无阴影)——显式 .contract 前缀抬高覆写优先级 // 内容白卡(设计 31ee32fc:x70 y271 1780×992 圆角白卡,无阴影)
.contract .contract__card { .contract .contract__card {
margin-top: 15px; // 横幅底 → 卡片顶:设计 20 × 0.77 margin-top: 20px; // 横幅底 251 → 卡顶 271
padding: 40px 48px 44px; // 卡片内缩:设计 51/64/59 × 0.77 padding: 51px 64px 59px; // 搜索框 x134 = 70+64;表格 x135
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
// 搜索行并入卡片:去掉 FilterPanel 自带的卡面,按设计 41 重排控件 // 搜索行并入卡片:去掉 FilterPanel 自带的卡面
:deep(.filter-panel) { :deep(.filter-panel) {
margin-bottom: 30px; // 搜索行 → 表格:设计 39 × 0.77 margin-bottom: 39px; // 搜索框底 371 → 表头 411
padding: 0; padding: 0;
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
@@ -134,16 +135,30 @@ onMounted(() => {
} }
:deep(.filter-panel__bar) { :deep(.filter-panel__bar) {
gap: 6px; gap: 8px;
// 设计:重置 x816、搜索 x940。搜索框右缘 726 →
// 重置 816(margin 90 − 12 padding = 78)、搜索 940(margin 52 − 12 = 40)
.filter-panel__btn {
margin-left: 78px;
} }
.filter-panel__btn--primary {
margin-left: 40px;
} }
// 搜索框 134,322 592×49;占位 20/400 #999999
:deep(.filter-panel__input) { :deep(.filter-panel__input) {
flex: 1; flex: 0 0 592px;
width: auto; width: 592px;
height: 30px; // 设计 38 × 0.77 max-width: none;
margin-right: 0;
height: 49px;
border-color: transparent; border-color: transparent;
border-radius: 0;
background: @input-bg; background: @input-bg;
font-size: 11px; // 设计 13 → 可读下限 font-size: 20px;
box-sizing: border-box;
} }
:deep(.filter-panel__input::placeholder) { :deep(.filter-panel__input::placeholder) {
@@ -154,15 +169,17 @@ onMounted(() => {
border-color: @primary-strong; border-color: @primary-strong;
} }
// 设计 41 按钮序为「重置 | 搜索」,组件内 DOM 序相反,用 order 对齐 // 设计按钮:重置 x816、搜索 x940,均 20/400 纯文字(DOM 序相反,用 order 对齐)
:deep(.filter-panel__btn) { :deep(.filter-panel__btn) {
order: 1; order: 1;
height: 30px; min-width: 0;
padding: 0 16px; height: 29px;
padding: 0 12px;
border: none; border: none;
background: @contract-reset-bg; border-radius: 0;
color: @text-color; background: transparent;
font-size: 11px; // 设计 12 → 可读下限 color: @contract-placeholder;
font-size: 20px;
} }
:deep(.filter-panel__btn:hover) { :deep(.filter-panel__btn:hover) {
@@ -172,32 +189,33 @@ onMounted(() => {
:deep(.filter-panel__btn--primary) { :deep(.filter-panel__btn--primary) {
order: 2; order: 2;
background: @primary-strong; border-radius: 0;
color: @white; background: transparent;
color: @contract-accent;
} }
:deep(.filter-panel__btn--primary:hover) { :deep(.filter-panel__btn--primary:hover) {
background: @primary-color; background: transparent;
color: @white; color: @primary-color;
} }
// 分页(设计 46:右对齐、方角小页码、当前页实底蓝) // 分页(设计:右对齐,页码 ink 1180/1181,`<上一页` x1388、`下一页>` x1740)
:deep(.pagination) { :deep(.pagination) {
justify-content: flex-end; justify-content: flex-end;
gap: 8px; // 设计 10 × 0.77 gap: 10px;
padding: 0; padding: 0;
margin-top: 22px; // 表格底 → 分页:设计 28 × 0.77 margin-top: 28px;
} }
:deep(.pagination__btn) { :deep(.pagination__btn) {
min-width: 31px; // 设计 40 × 0.77 min-width: 40px;
height: 20px; // 设计 26 × 0.77 height: 26px;
padding: 0 4px; padding: 0 4px;
border: 1px solid @contract-line; border: 1px solid @contract-line;
border-radius: 0; border-radius: 0;
background: @white; background: @white;
color: @contract-line; color: @contract-line;
font-size: 11px; // 设计 14 → 可读下限 font-size: 14px;
} }
:deep(.pagination__btn:hover:not(:disabled):not(.is-ellipsis)) { :deep(.pagination__btn:hover:not(:disabled):not(.is-ellipsis)) {

View File

@@ -52,6 +52,7 @@ const emit = defineEmits<{
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿底部操作条(浮于页面之上,展示提示文案 + 三个 232×63 按钮)
.demand-bar { .demand-bar {
position: fixed; position: fixed;
right: 0; right: 0;
@@ -63,30 +64,31 @@ const emit = defineEmits<{
&__inner { &__inner {
.flex(row, space-between, center); .flex(row, space-between, center);
height: 72px; height: 104px; // 设计稿按 63 高按钮 + 上下留白推得
// 覆盖 .container 内缩:左 = 容器16 + 卡左内边距49 + 序号列46 = 111(对齐内容列),右 = 16 + 40 // 覆盖 .container 内缩:左 = 70 + 170(提示文案 x240)、右 = 70 + 197(末按钮右缘 1799)
gap: @spacing-base; gap: @spacing-base;
padding-left: 111px; padding-left: 170px;
padding-right: 56px; padding-right: 121px;
} }
&__tip { &__tip {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: @spacing-sm; gap: 10px;
min-width: 0; min-width: 0;
font-size: 15px; font-size: 20px; // 设计稿 20/500 #333333
font-weight: 500; font-weight: 500;
line-height: 29px;
color: @demand-heading; color: @demand-heading;
} }
&__info { &__info {
.flex(row, center, center); .flex(row, center, center);
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 26px;
height: 16px; height: 26px;
border: 1px solid @demand-accent; border: 1px solid @demand-accent;
border-radius: 50%; border-radius: 50%;
font-size: 11px; font-size: 16px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
color: @demand-accent; color: @demand-accent;
@@ -94,20 +96,20 @@ const emit = defineEmits<{
&__actions { &__actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
// 设计稿按钮 232×63 设计px ≈ 179×48,间距 47 设计px ≈ 36px // 设计稿按钮中心 x1125 / 1404 / 1683 → 间距 47(按钮 232 宽)
gap: 36px; gap: 47px;
flex-shrink: 0; flex-shrink: 0;
} }
// 设计稿三个按钮均为实底蓝 #075CFF + 白字 // 设计稿三个按钮均为实底蓝 #075CFF + 白字,232×63
&__btn { &__btn {
width: 179px; width: 232px;
height: 48px; height: 63px;
padding: 0; padding: 0;
border: none; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @demand-secondary-bg; background: @demand-secondary-bg;
font-size: 15px; font-size: 20px;
font-weight: 500; font-weight: 500;
color: @white; color: @white;
cursor: pointer; cursor: pointer;
@@ -123,8 +125,8 @@ const emit = defineEmits<{
} }
&--primary { &--primary {
// 设计稿三个按钮等宽(232 设计px ≈ 179px),主按钮不额外加宽 // 设计稿三个按钮等宽(232),主按钮不额外加宽
min-width: 179px; min-width: 232px;
background: @demand-secondary-bg; background: @demand-secondary-bg;
color: @white; color: @white;

View File

@@ -208,34 +208,39 @@ function onDownload(file: DemandAttachment): void {
<style lang="less" scoped> <style lang="less" scoped>
.demand-attach { .demand-attach {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 11px; gap: 14px;
&__head { &__head {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 15px;
} }
&__label { &__label {
font-size: 15px; flex-shrink: 0;
font-size: 20px; // 设计稿 20/500 @(194,1376)
font-weight: 500; font-weight: 500;
line-height: 29px;
color: @demand-heading; color: @demand-heading;
} }
// 设计稿:提示文案 @(294,1379) 16/400,起点 = 标签列 80 + 20
&__hint { &__hint {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
font-size: @font-size-sm; margin-left: 20px;
font-size: 16px;
font-weight: 400;
color: @demand-line; color: @demand-line;
} }
// 设计稿该行未标注按钮,按钮设计稿外置(沿用站内次级按钮规格)
&__pick { &__pick {
flex-shrink: 0; flex-shrink: 0;
height: 28px; height: 40px;
padding: 0 @spacing-base; padding: 0 @spacing-lg;
border: 1px solid @demand-line; border: 1px solid @demand-line;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
font-size: @font-size-sm; font-size: 16px;
color: @demand-line; color: @demand-line;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s, color 0.2s; transition: border-color 0.2s, color 0.2s;
@@ -257,19 +262,19 @@ function onDownload(file: DemandAttachment): void {
&__list { &__list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 设计稿:两条 90 设计px 灰条,间距 20 设计px ≈ 15px // 设计稿:两条 90 高灰条,间距 20
gap: 15px; gap: 20px;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
// 设计稿附件条:实底 #EDEDED、无描边、90 设计px ≈ 69px 高 // 设计稿附件条:实底 #EDEDED、无描边、90 高
&__item { &__item {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: @spacing-base; gap: @spacing-base;
min-height: 69px; min-height: 90px;
padding: @spacing-base; padding: 0 @spacing-lg;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @demand-upload-bg; background: @demand-upload-bg;
} }
@@ -277,10 +282,10 @@ function onDownload(file: DemandAttachment): void {
&__badge { &__badge {
.flex(row, center); .flex(row, center);
flex-shrink: 0; flex-shrink: 0;
width: 34px; width: 44px;
height: 34px; height: 44px;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 10px; font-size: 12px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
@@ -288,18 +293,18 @@ function onDownload(file: DemandAttachment): void {
&__meta { &__meta {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
gap: 2px; gap: 4px;
min-width: 0; min-width: 0;
} }
&__name { &__name {
font-size: 15px; font-size: 20px;
color: @demand-ink; color: @demand-ink;
.ellipsis(); .ellipsis();
} }
&__sub { &__sub {
font-size: @font-size-sm; font-size: 16px;
color: @text-color-secondary; color: @text-color-secondary;
} }

View File

@@ -16,8 +16,13 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
<template> <template>
<div class="biz-types"> <div class="biz-types">
<!-- 接口一条都没下发时给一行说明:不是渲染失败,而是确实没有可选类型(前端不造兜底项) --> <!-- 接口一条都没下发时保留 5 个空轨道:视觉上仍是设计稿的 5 列卡片栅格,
<p v-if="!props.options.length" class="biz-types__empty">暂无可选业务类型</p> 不把 ① 区块压塌(前端不造兜底选项,只占位) -->
<div v-if="!props.options.length" class="biz-types__grid" aria-hidden="true">
<p class="biz-types__empty">
暂无可选业务类型
</p>
</div>
<div v-else class="biz-types__grid"> <div v-else class="biz-types__grid">
<button <button
v-for="option in props.options" v-for="option in props.options"
@@ -51,24 +56,30 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
<style lang="less" scoped> <style lang="less" scoped>
.biz-types { .biz-types {
&__empty { &__empty {
padding: @spacing-base 0; grid-column: 1 / -1;
font-size: @font-size-sm; // 与设计稿单卡同高(206),保证 ① 区块塌陷时页面纵向节奏不变
color: @text-color-secondary; .flex(row, center, center);
height: 206px;
border: 1px dashed @demand-line;
border-radius: @radius-base;
font-size: 16px;
color: @demand-line;
} }
&__grid { &__grid {
// auto-fill(不是 auto-fit):条目不足 5 个时保留空轨道,卡片尺寸与设计稿一致(303 设计px ≈ 233px), // 设计稿:5 张卡 303/305 宽 × 206 高,x194/515/838/1160/1482 → 间距 18/18/17/16
// 不会被拉伸变形;满 5 个时正好落在设计稿的 5 列上。
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(5, 1fr);
gap: 14px; gap: 17px;
} }
&__card { &__card {
position: relative; position: relative;
.flex(column, flex-start, stretch); .flex(column, flex-start, flex-start);
gap: 2px; gap: 0;
padding: 12px 12px 8px; height: 206px;
box-sizing: border-box;
padding: 44px 16px 0; // 标题 @(210,545) → 卡内 (16,106)
border: 1px solid @demand-line; border: 1px solid @demand-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
@@ -87,13 +98,13 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
} }
} }
/** 右上角选中标记(设计稿 30 设计px ≈ 23px):未选空心圈,选中为白环 + 蓝缝 + 白点 */ /** 右上角选中标记(设计稿 30 设计px):未选空心圈,选中为白环 + 蓝缝 + 白点 */
&__check { &__check {
position: absolute; position: absolute;
top: 22px; top: 22px;
right: 19px; right: 19px;
width: 23px; width: 30px;
height: 23px; height: 30px;
border: 1px solid @demand-radio-idle; border: 1px solid @demand-radio-idle;
border-radius: 50%; border-radius: 50%;
background: @white; background: @white;
@@ -107,28 +118,29 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
} }
&__icon { &__icon {
margin-bottom: 25px; margin-bottom: 15px; // 图标底 → 标题顶(设计稿 545 − 图标 40 − 卡内顶 106)
} }
/** 后端 Logo:与内联 SVG 同尺寸,保证两种来源视觉一致 */ /** 后端 Logo:与内联 SVG 同尺寸,保证两种来源视觉一致 */
&__logo { &__logo {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin-bottom: 25px; margin-bottom: 15px;
object-fit: contain; object-fit: contain;
} }
&__name { &__name {
font-size: @font-size-title; font-size: 28px; // 设计稿 28/500
font-weight: 500; font-weight: 500;
line-height: 45px;
color: @demand-ink; color: @demand-ink;
} }
&__desc { &__desc {
font-size: @font-size-sm; font-size: 16px; // 设计稿 16/500
font-weight: 500; font-weight: 500;
line-height: 23px;
color: @demand-ink; color: @demand-ink;
line-height: 1.45;
} }
// 选中卡内文字反白(设计稿:标题/描述均为白色) // 选中卡内文字反白(设计稿:标题/描述均为白色)

View File

@@ -77,39 +77,49 @@ function onRemark(event: Event): void {
<style lang="less" scoped> <style lang="less" scoped>
.demand-confirm { .demand-confirm {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-lg; gap: 0;
width: 1605px;
&__row { &__row {
.flex(row, flex-start, stretch); .flex(row, flex-start, flex-start);
// 设计稿:两个 503 设计px 输入框(≈387px),间距 73 设计px ≈ 56px,整行不占满 // 设计稿:两个 503 宽输入框(x194→697 / x770→1273),间距 73
gap: 56px; gap: 73px;
} }
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 10px; gap: 14px;
min-width: 0; min-width: 0;
} }
// 行内两列(用户名称 / 用户手机号)定宽;补充备注整行铺满 // 行内两列(用户名称 / 用户手机号)定宽;补充备注整行铺满
&__row &__field { &__row &__field {
flex: 0 0 387px; flex: 0 0 503px;
}
// 设计稿:备注行标签顶 y1901,用户行控件底 y1871 → 间距 30
&__row + &__field {
margin-top: 30px;
} }
&__label { &__label {
font-size: 15px; font-size: 20px;
font-weight: 500; font-weight: 500;
line-height: 29px;
color: @demand-heading; color: @demand-heading;
} }
&__input { &__input {
.text-control(); .text-control();
height: 38px; height: 49px;
font-size: 15px; padding: 0 18px;
border-color: @demand-line; border-color: @demand-line;
border-radius: @radius-sm;
font-size: 20px;
color: @demand-ink; color: @demand-ink;
&::placeholder { &::placeholder {
font-weight: 400;
color: @demand-line; color: @demand-line;
} }
} }
@@ -121,25 +131,28 @@ function onRemark(event: Event): void {
&__textarea { &__textarea {
.text-control(); .text-control();
min-height: 208px; height: 270px;
padding: 10px 14px 24px; min-height: 270px;
font-size: 15px; padding: 10px 18px 24px;
border-color: @demand-line; border-color: @demand-line;
border-radius: @radius-sm;
font-size: 20px;
color: @demand-ink; color: @demand-ink;
line-height: 1.7; line-height: 29px;
resize: vertical; resize: vertical;
&::placeholder { &::placeholder {
font-weight: 400;
color: @demand-line; color: @demand-line;
} }
} }
&__count { &__count {
position: absolute; position: absolute;
right: @spacing-sm; right: @spacing-lg;
bottom: @spacing-xs; bottom: @spacing-sm;
font-size: @font-size-sm; font-size: 16px;
color: @text-color-secondary; color: @text-color-disabled;
} }
} }
</style> </style>

View File

@@ -194,32 +194,37 @@ function onBudgetManualBlur(event: Event): void {
<style lang="less" scoped> <style lang="less" scoped>
.demand-info { .demand-info {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-lg; // 字段间统一节奏:上一控件底 → 下一标签顶
// 设计稿实测:input 底 881 → 描述标签顶 910(29 行盒)→ 控件 954;预算行 1224→1254;附件 1346→1376
gap: 29px;
width: 1605px; // 设计稿:所有控件统一 1605 宽(x194 → 1799)
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 10px; gap: 14px; // 标签行盒底 → 控件顶(设计稿 789+29 → 832)
min-width: 0; min-width: 0;
} }
&__row { &__row {
.flex(row, flex-start, stretch); .flex(row, flex-start, flex-start);
// 设计稿:预算列 892 设计px + 间距 61 设计px + 周期列 652 设计px(1605 总宽) // 设计稿:预算列 892 宽(x194→1086)+ 间距 61 + 周期列 652 宽(x1147→1799)
gap: @spacing-xxl; gap: 61px;
.demand-info__field { .demand-info__field {
flex: 687 1 0; flex: 0 0 auto;
width: auto; width: 892px;
margin-top: 0;
} }
.demand-info__field:last-child { .demand-info__field:last-child {
flex: 502 1 0; width: 652px;
} }
} }
&__label { &__label {
font-size: 15px; font-size: 20px; // 设计稿 20/500 #333333
font-weight: 500; font-weight: 500;
line-height: 29px;
color: @demand-heading; color: @demand-heading;
} }
@@ -230,13 +235,16 @@ function onBudgetManualBlur(event: Event): void {
&__input { &__input {
.text-control(); .text-control();
// 设计稿:49 设计px 高 ≈ 38px,20 设计px 字号 ≈ 15px,描边/占位 rgba(153,153,153) // 设计稿:49 高、20 字号、描边 #999999、内缩 18
height: 38px; height: 49px;
font-size: 15px; padding: 0 18px;
border-color: @demand-line; border-color: @demand-line;
border-radius: @radius-sm;
font-size: 20px;
color: @demand-ink; color: @demand-ink;
&::placeholder { &::placeholder {
font-weight: 400;
color: @demand-line; color: @demand-line;
} }
} }
@@ -248,44 +256,48 @@ function onBudgetManualBlur(event: Event): void {
&__textarea { &__textarea {
.text-control(); .text-control();
// 设计稿:270 设计px 高 ≈ 208px,占位内缩 18/10 设计px ≈ 14/8px // 设计稿:270 高、占位内缩 18/10
min-height: 208px; height: 270px;
padding: 10px 14px 24px; min-height: 270px;
font-size: 15px; padding: 10px 18px 24px;
border-color: @demand-line; border-color: @demand-line;
border-radius: @radius-sm;
font-size: 20px;
color: @demand-ink; color: @demand-ink;
line-height: 1.7; line-height: 29px;
resize: vertical; resize: vertical;
&::placeholder { &::placeholder {
font-weight: 400;
color: @demand-line; color: @demand-line;
} }
} }
&__count { &__count {
position: absolute; position: absolute;
right: @spacing-sm; right: @spacing-lg;
bottom: @spacing-xs; bottom: @spacing-sm;
font-size: @font-size-sm; font-size: 16px;
color: @text-color-secondary; color: @text-color-disabled;
} }
&__budgets { &__budgets {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
// 设计稿:5 个 165 设计px 胶囊、间距 17 设计px ≈ 13px // 设计稿:5 个 165 宽胶囊,间距 17
gap: 13px; gap: 17px;
} }
/** 预算控件外观:4 个胶囊按钮 + 第 5 个手填输入框共用同一套规格 */ /** 预算控件外观:4 个胶囊按钮 + 第 5 个手填输入框共用同一套规格 */
&__budget { &__budget {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 38px; height: 49px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border: 1px solid @demand-line; border: 1px solid @demand-line;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 15px; font-size: 20px;
font-weight: 400;
font-family: inherit; font-family: inherit;
color: @demand-line; color: @demand-line;
text-align: center; text-align: center;
@@ -302,7 +314,7 @@ function onBudgetManualBlur(event: Event): void {
border-color: @demand-accent; border-color: @demand-accent;
background: @demand-selected-bg; background: @demand-selected-bg;
color: @white; color: @white;
font-weight: 600; font-weight: 500;
} }
} }

View File

@@ -55,22 +55,27 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="demand-page"> <div class="demand-page" :class="{ 'is-loading': loading }">
<div class="container demand" :class="{ 'is-loading': loading }"> <!-- 设计稿:导航下方 1920×179 白色通栏页头(标题 36/700 @x112,副标题 24/500 @y171) -->
<header class="demand__head"> <header class="demand-page__hero">
<h1 class="demand__title"> <div class="container demand-page__hero-inner">
<h1 class="demand-page__title">
需求提报 需求提报
</h1> </h1>
<p class="demand__subtitle"> <p class="demand-page__subtitle">
填写您的需求信息,专业顾问将快速与您联系 填写您的需求信息,专业顾问将快速与您联系
</p> </p>
</div>
</header> </header>
<!-- 设计稿:三段内容同处一张 1780×2186 白卡(矩形 148286 @(70,271)) -->
<div class="container">
<div class="demand">
<!-- ① 选择业务类型 --> <!-- ① 选择业务类型 -->
<section class="demand__card"> <section class="demand__section">
<h2 class="demand__section"> <h2 class="demand__heading">
<span class="demand__index">1</span> <span class="demand__index">01</span>
选择业务类型<span class="demand__required">*</span> <span class="demand__label">选择业务类型<span class="demand__required">*</span></span>
</h2> </h2>
<DemandBizTypes <DemandBizTypes
:options="bizTypeOptions" :options="bizTypeOptions"
@@ -80,10 +85,10 @@ onMounted(() => {
</section> </section>
<!-- ② 填写需求信息 --> <!-- ② 填写需求信息 -->
<section class="demand__card"> <section class="demand__section">
<h2 class="demand__section"> <h2 class="demand__heading">
<span class="demand__index">2</span> <span class="demand__index">02</span>
填写需求信息<span class="demand__required">*</span> <span class="demand__label">填写需求信息<span class="demand__required">*</span></span>
</h2> </h2>
<DemandInfoForm <DemandInfoForm
:demand="form" :demand="form"
@@ -96,10 +101,10 @@ onMounted(() => {
</section> </section>
<!-- ③ 确认信息并补充备注 --> <!-- ③ 确认信息并补充备注 -->
<section class="demand__card"> <section class="demand__section">
<h2 class="demand__section"> <h2 class="demand__heading">
<span class="demand__index">3</span> <span class="demand__index">03</span>
确认信息并补充备注 <span class="demand__label">确认信息并补充备注</span>
</h2> </h2>
<DemandConfirmForm <DemandConfirmForm
:demand="form" :demand="form"
@@ -107,6 +112,7 @@ onMounted(() => {
/> />
</section> </section>
</div> </div>
</div>
<DemandActionBar <DemandActionBar
:saving="saving" :saving="saving"
@@ -120,96 +126,98 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.demand-page { .demand-page {
padding-top: @spacing-lg; // 固定底条 72px + 呼吸间距
// 固定底条 72px + 呼吸间距,避免遮住 ③ 卡片
padding-bottom: 104px; padding-bottom: 104px;
// 设计稿:导航下方 179 设计px(≈138px)白色通栏横幅,其下为页面底色
background: linear-gradient(to bottom, @white 0, @white 138px, @bg-color 138px);
&.is-loading { &.is-loading .demand {
.demand {
opacity: 0.6; opacity: 0.6;
pointer-events: none; pointer-events: none;
} }
}
}
.demand { // 设计稿:白色通栏页头 @(0,72) 1920×179,文案 36/700 @(112,104)、24/500 @(112,171)
// 设计稿:标题距横幅顶 32 设计px、副标题行距 15 设计px、横幅下留白 50 设计px &__hero {
&__head { height: 179px;
.flex(column, flex-start, stretch); box-sizing: border-box;
gap: 11px; padding-top: 32px;
margin-bottom: 50px; background: @white;
}
&__hero-inner {
padding-left: 42px; // 文案 x112 = 内容区 x70 + 42
} }
&__title { &__title {
font-size: @font-size-xxl; margin: 0;
font-size: 36px;
font-weight: 700; font-weight: 700;
line-height: 1.45; line-height: 52px;
color: @demand-ink; color: @demand-ink;
} }
&__subtitle { &__subtitle {
font-size: 18px; margin: 15px 0 0;
font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 35px;
color: @demand-subtitle; color: @demand-subtitle;
} }
}
// 设计稿三段内容位于同一张白色大卡内:三张卡无缝拼接呈现同一张卡的观感 // 设计稿:白卡 @(70,271) 1780×2186;三段标题序号 x134、标题文字 x194
&__card { .demand {
padding: 24px 40px 24px 49px; margin-top: 20px; // 271 − (72+179) = 20
border-radius: 0; padding: 48px 56px 111px; // 序号顶 319 − 271 = 48
background: @white; background: @white;
// 三卡相连,不再留块间距 &__heading {
& + & { .flex(row, flex-start, center);
margin-top: 0; gap: 15px;
margin: 0 0 50px; // 01:序号块底 389 → 业务卡顶 439
padding-left: 8px; // 序号落 x134 = 卡内 x70+64 → 64-56 = 8
} }
&:first-of-type {
padding-top: 37px;
border-radius: @radius-base @radius-base 0 0;
}
&:last-of-type {
padding-bottom: 40px;
border-radius: 0 0 @radius-base @radius-base;
}
}
// 区块标题:序号落在卡左内边距列(x=134 设计px),标题文字对齐内容列(x=194)
&__section {
.flex(row, flex-start, baseline);
gap: @spacing-xs;
margin-bottom: 16px;
font-size: @font-size-title;
font-weight: 500;
color: @demand-heading;
}
// 内容列(卡片栅格 / 表单)与区块标题文字对齐到 x=194 设计列(60 设计px ≈ 46px)
&__section + * {
margin-left: 46px;
}
// ① 段标题到卡片栅格的间距更大(56 设计px ≈ 43px)
&__card:first-of-type &__section {
margin-bottom: 40px;
}
/** 区块序号:设计稿 48 设计px 蓝色数字(01/02/03) */
&__index { &__index {
flex-shrink: 0; flex-shrink: 0;
width: 42px; font-size: 48px; // 设计稿 48/500 #024FFF
font-size: 37px;
font-weight: 500; font-weight: 500;
line-height: 1; line-height: 70px;
font-variant-numeric: tabular-nums;
color: @demand-accent; color: @demand-accent;
} }
&__label {
font-size: 28px; // 设计稿 28/500 #333333
font-weight: 500;
line-height: 41px;
color: @demand-heading;
}
&__required { &__required {
margin-left: 2px; margin-left: 2px;
color: @danger-color; color: @danger-color;
} }
// 表单列对齐标题文字 x194 = 卡内 x70 + 124(序号行宽 55 + gap 15 + 序号内缩 8 = 78,再补 46)
&__heading + * {
margin-left: 46px;
}
// 段间距(设计稿实测):01 区底 645 → 02 序号顶 705 = 60;02 区底 1639 → 03 序号顶 1689 = 50
&__section + &__section {
margin-top: 60px;
}
&__section + &__section:last-of-type {
margin-top: 50px;
}
// ② ③ 段:标题块(70) 与首个表单标签间距更紧(序号顶 705/1689 → 标签顶 789/1779 = 84/90)
&__section + &__section &__heading {
margin-bottom: 0;
}
&__section + &__section &__heading + * {
margin-top: 14px;
}
} }
</style> </style>

View File

@@ -2,10 +2,9 @@
import { computed, onMounted, watch } from 'vue' import { computed, onMounted, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { AppButton, StatusTag } from '@/components'
// 页面容器:数据只经 store,不直接 import api // 页面容器:数据只经 store,不直接 import api
import { useFinanceStore } from '@/stores/modules/finance' import { useFinanceStore } from '@/stores/modules/finance'
import type { BillStatus } from '@/types/business/finance' import type { Bill, BillStatus } from '@/types/business/finance'
import { formatMoney } from '@/utils/format' import { formatMoney } from '@/utils/format'
import { toChannelLabel } from '@/utils/payMeta' import { toChannelLabel } from '@/utils/payMeta'
@@ -16,21 +15,36 @@ const { currentBill } = storeToRefs(financeStore)
const billId = computed(() => String(route.params.id)) const billId = computed(() => String(route.params.id))
const STATUS_VARIANTS: Record<BillStatus, 'warning' | 'success' | 'info' | 'danger'> = { /**
待支付: 'warning', * 设计 31 是三张并排状态卡(523/825/1127 各 270×476):
已支付: 'success', * 已支付(红金额)+ 待支付(橙金额)+ 未到期(灰金额)。
未到期: 'info', * 页面实际只展示当前账单状态那一张,其余两张是同一组件的另外两种状态。
已取消: 'danger', */
const STATUS_CARDS: BillStatus[] = ['已支付', '待支付', '未到期']
const AMOUNT_CLASS: Record<BillStatus, string> = {
已支付: 'is-paid',
待支付: 'is-pending',
未到期: 'is-idle',
已取消: 'is-idle',
} }
/** 演示用:三张卡各自的账单数据(当前账单代入对应状态卡) */
const cards = computed<Array<{ status: BillStatus; bill: Bill | null }>>(() =>
STATUS_CARDS.map((status) => ({
status,
bill: currentBill.value && currentBill.value.status === status ? currentBill.value : null,
})),
)
async function load(): Promise<void> { async function load(): Promise<void> {
// 支付信息(渠道 / 时间 / 流水号)已由后端回写在账单本身上,无需再查支付结果 // 支付信息(渠道 / 时间 / 流水号)已由后端回写在账单本身上,无需再查支付结果
await financeStore.fetchDetail(billId.value) await financeStore.fetchDetail(billId.value)
} }
function onPay(): void { function onPay(id: string): void {
// 进收银台(确认支付):选支付方式 → 微信出码 / 支付宝跳转 // 进收银台(确认支付):选支付方式 → 微信出码 / 支付宝跳转
void router.push({ path: '/finance/checkout', query: { ids: billId.value } }) void router.push({ path: '/finance/checkout', query: { ids: id } })
} }
watch(billId, () => { watch(billId, () => {
@@ -44,252 +58,345 @@ onMounted(() => {
<template> <template>
<div class="bill-detail"> <div class="bill-detail">
<nav class="bill-detail__breadcrumb text-secondary"> <div class="container">
<RouterLink to="/finance" class="link"> <!-- 面包屑 (70,117)「财务中心 / 账单明细 /」 + 「账单详情」 -->
财务中心 <nav class="bill-detail__breadcrumb">
<RouterLink to="/finance" class="bill-detail__breadcrumb-link">
财务中心 /
</RouterLink> </RouterLink>
<span> &gt; </span> <RouterLink to="/finance/bills" class="bill-detail__breadcrumb-link">
<RouterLink to="/finance/bills" class="link"> 账单明细 /
账单明细
</RouterLink> </RouterLink>
<span> &gt; </span> <span class="bill-detail__breadcrumb-current">账单详情</span>
<span>账单详情</span>
</nav> </nav>
<template v-if="currentBill"> <!-- 三张状态卡 (523/825/1127,158) 各 270×476 -->
<section class="bill-detail__card card"> <div v-if="currentBill" class="bill-detail__cards">
<div class="bill-detail__card-head"> <section
<h3 class="bill-detail__card-title"> v-for="card in cards"
订单信息 :key="card.status"
class="bill-detail__card"
>
<!-- 卡头:对勾圆 + 状态词 (539,278) 238×21 16/700 #1F2937 -->
<div class="bill-detail__head">
<span class="bill-detail__check" aria-hidden="true">✓</span>
<h3 class="bill-detail__status">
{{ card.status }}
</h3> </h3>
<StatusTag :label="currentBill.status" :variant="STATUS_VARIANTS[currentBill.status]" />
</div> </div>
<div class="bill-detail__fields">
<div class="bill-detail__field"> <!-- 付款金额:标签 (590.23,315) + 金额 (653.8,309) 18/700 -->
<span class="bill-detail__label text-secondary">账单名称</span> <div class="bill-detail__amount-row">
<span class="bill-detail__value">{{ currentBill.planName }}</span> <span class="bill-detail__amount-label">付款金额:</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">账单号</span>
<span class="bill-detail__value">{{ currentBill.billNo }}</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">项目名称</span>
<span class="bill-detail__value">{{ currentBill.projectName }}</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">订单金额</span>
<!-- 设计 31:金额随状态取色——已支付红 / 待支付橙 / 未到期深灰 -->
<span <span
class="bill-detail__value bill-detail__value--amount" class="bill-detail__amount"
:class="{ :class="AMOUNT_CLASS[card.status]"
'is-pending': currentBill.status === '待支付', >¥ {{ formatMoney(card.bill?.amount ?? currentBill.amount, 0) }}</span>
'is-idle': currentBill.status === '未到期' || currentBill.status === '已取消', </div>
}"
>¥{{ formatMoney(currentBill.amount) }}</span> <!-- 支付信息四行:标签 (547,y) 12/400 #6B7280 + 值 12/700 #1F2937 -->
<dl class="bill-detail__fields">
<div class="bill-detail__field">
<dt class="bill-detail__label">
支付方式
</dt>
<dd class="bill-detail__value">
{{ card.status === '已支付' ? toChannelLabel(currentBill.payChannel) : '—' }}
</dd>
</div>
<div class="bill-detail__field">
<dt class="bill-detail__label">
支付时间
</dt>
<dd class="bill-detail__value">
{{ card.status === '已支付' ? (currentBill.payTime ?? '—') : '—' }}
</dd>
</div>
<div class="bill-detail__field">
<dt class="bill-detail__label">
交易流水号
</dt>
<dd class="bill-detail__value">
{{ card.status === '已支付' ? (currentBill.tradeNo ?? '—') : '—' }}
</dd>
</div>
</dl>
<!-- 订单小卡 (555.5/857.5/1159.5,481..) 205 宽 -->
<div class="bill-detail__order">
<span class="bill-detail__order-title">
{{ card.status }}订单
</span>
<div class="bill-detail__order-body">
<span class="bill-detail__order-left">
<span class="bill-detail__order-name">{{ currentBill.planName }}</span>
<span class="bill-detail__order-meta">账单号:{{ currentBill.billNo }}</span>
<span class="bill-detail__order-meta">关联项目:{{ currentBill.projectName }}</span>
</span>
<span class="bill-detail__order-amount">¥ {{ formatMoney(currentBill.amount, 0) }}</span>
</div> </div>
</div> </div>
<!-- 待支付:立即支付 --> <!-- 返回 (727.4,594) 43.2×16 12/500 -->
<div v-if="currentBill.status === '待支付'" class="bill-detail__pay"> <button
<AppButton type="primary" @click="onPay"> v-if="card.status === '待支付'"
class="bill-detail__cta"
@click="onPay(currentBill.id)"
>
立即支付 立即支付
</AppButton> </button>
</div> <button
v-else
<!-- 已支付:支付信息 --> class="bill-detail__cta"
<template v-else-if="currentBill.status === '已支付'"> @click="router.push('/finance/bills')"
<div class="bill-detail__pay-info"> >
<h4 class="bill-detail__sub-title"> 返回
支付信息 </button>
</h4>
<div class="bill-detail__fields">
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">付款金额</span>
<span class="bill-detail__value bill-detail__value--amount">¥{{ formatMoney(currentBill.amount) }}</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">支付方式</span>
<span class="bill-detail__value">{{ toChannelLabel(currentBill.payChannel) }}</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">支付时间</span>
<span class="bill-detail__value">{{ currentBill.payTime ?? '—' }}</span>
</div>
<div class="bill-detail__field">
<span class="bill-detail__label text-secondary">交易流水号</span>
<span class="bill-detail__value">{{ currentBill.tradeNo ?? '—' }}</span>
</div>
</div>
</div>
</template>
<!-- 已取消 -->
<div v-else-if="currentBill.status === '已取消'" class="bill-detail__tip text-secondary">
该账单已取消,如有疑问请联系项目负责人。
</div>
<!-- 未到期:提示 -->
<div v-else class="bill-detail__tip text-secondary">
账单尚未到期,到期后可前往账单明细完成支付。
</div>
</section> </section>
</template> </div>
<p v-else class="bill-detail__empty text-secondary"> <p v-else class="bill-detail__empty">
账单不存在或已下线 账单不存在或已下线
</p> </p>
<div class="bill-detail__back">
<AppButton @click="router.push('/finance/bills')">
返回
</AppButton>
</div> </div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 31 账单详情(1920 画布 1:1)——内容区 x70→1850 = 1780
.bill-detail { .bill-detail {
width: 100%; width: 100%;
max-width: @container-width; padding-bottom: 325px; // 卡片底 634 → 页脚 1087
margin: 0 auto; background: @contract-page-bg;
padding: @spacing-lg @spacing-base @spacing-xxl;
// 设计 31:面包屑 13 设计px → 11px,前缀灰、当前页深灰 // 面包屑 (70,117):文字 ink y117,卡片顶 158 → 面包屑盒 86..131
&__breadcrumb { &__breadcrumb {
margin-bottom: @spacing-base; .flex(row, flex-start, center);
font-size: 11px; height: 86px;
color: @text-color-disabled; padding-top: 34px;
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-muted;
box-sizing: border-box;
}
> span:last-child { &__breadcrumb-link {
color: @text-color; color: @project-muted;
&:hover {
color: @primary-strong;
} }
} }
// 设计 31:白卡居中(对应设计三张窄卡的中轴布局),圆角 8、柔阴影 &__breadcrumb-current {
margin-left: 5.5px;
color: @finance-entry-text;
font-weight: 500;
}
// 三张卡并排:x523 / 825 / 1127,间距 32
&__cards {
.flex(row, flex-start, flex-start);
gap: 32px;
padding-left: 453px; // 内容区起点 70 → 首卡 523
box-sizing: border-box;
}
&__card { &__card {
max-width: 440px; position: relative;
margin: 0 auto; width: 270px;
padding: @spacing-base 20px 20px; height: 476px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
} }
&__card-head { // 卡头:对勾圆 (645.27,204) 34/400 白字 + 状态词 (539,278)
.flex(row, space-between, center); &__head {
margin-bottom: @spacing-base; .flex(row, flex-start, center);
gap: 0;
// 状态词盒 y278(卡内 120)、对勾圆 y204(卡内 46)→ 用 align-items:flex-start + 各自 margin
align-items: flex-start;
padding: 46px 0 0 16px;
height: 141px;
box-sizing: border-box;
} }
&__card-title { &__check {
font-size: 12px; .flex(row, center, center);
width: 34px;
height: 34px;
margin-right: 30px;
border-radius: 50%;
background: @success-color;
color: @white;
font-size: 18px;
font-weight: 400;
line-height: 1;
order: 2;
flex-shrink: 0;
// 设计圆 x629.27(卡内 106.27);渲染值 714 → 左移 84.75
transform: translateX(-84.75px);
}
&__status {
order: 1;
width: 238px;
margin-top: 74px; // 卡内 46 → 120(状态词顶)
font-size: 16px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 21px;
color: @finance-ink;
} }
// 状态徽标压成设计稿的深色粗体状态词 // 付款金额行:标签卡内 (67.23,157),金额卡内 (130.8,151)
:deep(.status-tag) { &__amount-row {
padding: 0; position: relative;
background: transparent; width: 238px;
color: @heading-color; height: 51px; // 行顶 = 卡内 130(金额盒顶 309−158=151 − 行内偏移 21)
margin: 0 0 0 16px;
}
&__amount-label {
position: absolute;
top: 16px; // 设计 ink y315(卡内 157)
left: 51.23px; // 卡内 67.23 − 16
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 400;
line-height: 16px;
color: @finance-muted-text;
} }
// 字段行:标签左(灰 11px)、值右(深色粗体)——设计 31 左右分栏 &__amount {
position: absolute;
top: 10px; // 设计盒顶 y309(卡内 151)
left: 114.8px; // 卡内 130.8 − 16
font-size: 18px;
font-weight: 700;
line-height: 24px;
white-space: nowrap;
&.is-paid { color: @danger-color; }
&.is-pending { color: @finance-pending; }
&.is-idle { color: @finance-idle; }
}
// 支付信息:标签卡内 (24,207) 起,行距 28
&__fields { &__fields {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 12px; gap: 12px;
margin: 26px 0 0 24px;
width: 222px;
box-sizing: border-box;
} }
&__field { &__field {
.flex(row, space-between, center); .flex(row, flex-start, flex-start);
gap: @spacing-base; gap: 0;
font-size: 11px;
} }
&__label { &__label {
flex-shrink: 0; flex: 0 0 77.52px; // 值统一从卡内 101.52 起
font-size: 11px; font-size: 12px;
font-weight: 400;
line-height: 16px;
color: @finance-muted-text;
} }
&__value { &__value {
color: @heading-color; flex: 1;
font-weight: 600; min-width: 0;
text-align: right; color: @finance-ink;
font-size: 12px;
font-weight: 700;
line-height: 16px;
word-break: break-all; word-break: break-all;
}
// 设计 31:付款金额 已支付 #EF4444 / 待支付 #F59E0B / 未到期深灰,18 设计px → 14px 粗体 // 订单小卡:卡内 x32.5,标题顶卡内 323(设计 481−158)
&--amount { &__order {
font-size: 14px; .flex(column, flex-start, flex-start);
gap: 0;
margin: 17px 0 0 32.5px;
width: 205px;
}
&__order-title {
font-size: 13px;
font-weight: 700; font-weight: 700;
color: @danger-color; line-height: 17px;
color: @finance-ink;
} }
&.is-pending { &__order-body {
color: @finance-pending; .flex(row, space-between, flex-start);
gap: 0;
margin-top: 6px;
width: 100%;
} }
&.is-idle { &__order-left {
color: @heading-color; .flex(column, flex-start, flex-start);
} gap: 0;
min-width: 0;
} }
&__pay { &__order-name {
margin-top: @spacing-base;
}
&__pay-info {
margin-top: @spacing-base;
padding-top: @spacing-base;
border-top: 1px solid @project-divider;
}
&__sub-title {
margin-bottom: 12px;
font-size: 11px; font-size: 11px;
font-weight: 400;
line-height: 15px;
color: @project-muted;
.ellipsis();
max-width: 124px;
}
&__order-meta {
font-size: 11px;
font-weight: 400;
line-height: 15px;
color: @project-muted;
.ellipsis();
max-width: 132px;
}
&__order-amount {
flex-shrink: 0;
font-size: 13px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 17px;
color: @finance-ink;
} }
&__tip { // 底部按钮 (727.4,594) 43.2×16 文字;盒宽 90 居中于 270 卡
margin-top: @spacing-base; &__cta {
padding: 12px; position: absolute;
border-radius: @radius-sm; left: 50%;
background: @bg-color; top: 428px; // 按钮文字 y594(卡内 436),盒高 32 → 盒顶 428
font-size: 11px; width: 90px;
height: 32px;
border: none;
border-radius: 4px;
background: @primary-strong;
color: @white;
font-size: 12px;
font-weight: 500;
/* 按钮基线:文字 ink y594 → 盒顶 428 + (32−16)/2 = 436 */
padding-bottom: 6px;
box-sizing: border-box;
transform: translateX(-50%);
transition: background 0.2s;
&:hover {
background: @primary-color;
}
} }
&__empty { &__empty {
padding: @spacing-xl 0; padding: 80px 0;
color: @project-muted;
font-size: 13px;
text-align: center; text-align: center;
font-size: 11px;
}
&__back {
max-width: 440px;
margin: @spacing-base auto 0;
}
// 设计 31/32:按钮蓝底 #024FFF(返回/立即支付同色)
:deep(.app-button--primary) {
background: @primary-strong;
&:hover {
background: @primary-color;
}
}
&__back :deep(.app-button--default) {
border-color: @primary-strong;
background: @primary-strong;
color: @white;
&:hover {
border-color: @primary-color;
background: @primary-color;
color: @white;
}
} }
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { FilterPanel, PaginationBar } from '@/components' import { PaginationBar } from '@/components'
// 页面容器:数据只经 store,不直接 import api // 页面容器:数据只经 store,不直接 import api
import { useFinanceStore } from '@/stores/modules/finance' import { useFinanceStore } from '@/stores/modules/finance'
import type { BillStatus } from '@/types/business/finance' import type { BillStatus } from '@/types/business/finance'
@@ -16,6 +16,7 @@ const { query, list, total } = storeToRefs(financeStore)
const keyword = ref('') const keyword = ref('')
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
/** 设计 30:状态分段组 4 项,「全部」选中态为实底 #024FFF */
const STATUS_TABS: Array<{ label: string; value: BillStatus | 'all' }> = [ const STATUS_TABS: Array<{ label: string; value: BillStatus | 'all' }> = [
{ label: '全部', value: 'all' }, { label: '全部', value: 'all' },
{ label: '待支付', value: '待支付' }, { label: '待支付', value: '待支付' },
@@ -23,6 +24,13 @@ const STATUS_TABS: Array<{ label: string; value: BillStatus | 'all' }> = [
{ label: '未到期', value: '未到期' }, { label: '未到期', value: '未到期' },
] ]
/** 统计卡三组(设计 30:(71/663.67/1256.33, 155) 各 592.67×60) */
const STATS = computed(() => [
{ label: '总笔数', value: String(total.value) },
{ label: '总金额', value: String(list.value.reduce((sum, bill) => sum + bill.amount, 0)) },
{ label: '待支付', value: String(list.value.filter((bill) => bill.status === '待支付').length) },
])
function onTab(value: BillStatus | 'all'): void { function onTab(value: BillStatus | 'all'): void {
if (query.value.status === value) return if (query.value.status === value) return
financeStore.setQuery({ ...query.value, status: value, keyword: keyword.value }) financeStore.setQuery({ ...query.value, status: value, keyword: keyword.value })
@@ -67,15 +75,24 @@ onMounted(() => {
<template> <template>
<div class="bills"> <div class="bills">
<nav class="bills__breadcrumb text-secondary"> <div class="container">
<RouterLink to="/finance" class="link"> <!-- 面包屑:(70,103) 「财务中心 >」13/400 #9CA3AF + 「账单明细」13/500 #4B5563 -->
财务中心 <nav class="bills__breadcrumb">
<RouterLink to="/finance" class="bills__breadcrumb-link">
财务中心 &gt;
</RouterLink> </RouterLink>
<span> &gt; </span> <span class="bills__breadcrumb-current">账单明细</span>
<span>账单明细</span>
</nav> </nav>
<!-- 设计 30:状态分段 Tab 与搜索栏同排共处一张白卡 --> <!-- 统计卡:(70,132) 1780×106 白底 + #F0F1F3 描边,三组等宽 592.67 -->
<div class="bills__stats">
<div v-for="item in STATS" :key="item.label" class="bills__stat">
<span class="bills__stat-label">{{ item.label }}</span>
<span class="bills__stat-value">{{ item.value }}</span>
</div>
</div>
<!-- 工具条:(70,252) 1780×71;Tab 组 (92,270.5) 279×34、搜索框 (405,269) 1300×37 -->
<div class="bills__toolbar"> <div class="bills__toolbar">
<div class="bills__tabs"> <div class="bills__tabs">
<button <button
@@ -89,14 +106,22 @@ onMounted(() => {
</button> </button>
</div> </div>
<FilterPanel <input
v-model:keyword="keyword" v-model="keyword"
keyword-placeholder="搜索账单名称 / 账单号 / 项目名称" class="bills__search"
@search="onSearch" placeholder="关键字搜索"
@reset="onReset" @keyup.enter="onSearch"
/> >
<button class="bills__btn bills__btn--reset" @click="onReset">
重置
</button>
<button class="bills__btn bills__btn--primary" @click="onSearch">
搜索
</button>
</div> </div>
<!-- 表格卡:(70,337) 1780×365.36,表头 (77.5,344.5) 1765×41.77 -->
<div class="bills__board"> <div class="bills__board">
<BillTable <BillTable
:bills="list" :bills="list"
@@ -113,7 +138,9 @@ onMounted(() => {
@page-change="onPageChange" @page-change="onPageChange"
/> />
</div> </div>
</div>
<!-- 汇总条:通栏白条 (0,718) 1920×66 -->
<BillSummaryBar <BillSummaryBar
:bills="list" :bills="list"
:selected-ids="selectedIds" :selected-ids="selectedIds"
@@ -123,116 +150,113 @@ onMounted(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 30 账单明细(1920 画布 1:1)——内容区 x70→1850 = 1780
.bills { .bills {
.flex(column, flex-start); .flex(column, flex-start);
align-items: stretch; align-items: stretch;
gap: @spacing-base; gap: 0;
width: 100%; width: 100%;
max-width: @container-width; padding-bottom: 14px; // 汇总条底 784 → 页脚 1087 的留白由页脚自身高度承担
margin: 0 auto; background: @contract-page-bg;
padding: @spacing-lg @spacing-base @spacing-xxl;
// 设计 30:面包屑 13 设计px → 11px,前缀灰、当前页深灰
&__breadcrumb { &__breadcrumb {
font-size: 11px;
color: @text-color-disabled;
> span:last-child {
color: @text-color;
}
}
// 顶部工具条:Tab 分段组 + 搜索栏(一张白卡)
&__toolbar {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: @spacing-base; height: 49px; // 组 6 (0,89) 高 31 + 上下留白 → 盒 y72..121,文字 ink y103
padding: 10px @spacing-base; padding-top: 14px;
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-muted;
box-sizing: border-box;
}
&__breadcrumb-link {
color: @project-muted;
&:hover {
color: @primary-strong;
}
}
&__breadcrumb-current {
// 设计:账单明细起点 x138.84;「 > 」由 CSS 绘制,宽 4.44px
margin-left: 4.44px;
color: @finance-entry-text;
font-weight: 500;
}
// 统计卡 (70,132) 1780×106
&__stats {
.flex(row, flex-start, flex-start);
gap: 0;
height: 106px;
margin-top: 11px; // 面包屑盒底 121 → 卡顶 132
border: 1px solid @finance-card-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
:deep(.filter-panel) {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
box-shadow: none;
border-radius: 0;
} }
:deep(.filter-panel__body) { &__stat {
margin-top: 0; .flex(column, flex-start, flex-start);
gap: 6px;
width: 592.67px;
padding: 22px 0 0 269px; // 标签 x340,卡边框内起点 x71 → 内缩 269
box-sizing: border-box;
} }
:deep(.filter-panel__bar) { &__stat-label {
gap: @spacing-sm; font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @finance-entry-text;
} }
:deep(.filter-panel__input) { &__stat-value {
order: 1; font-size: 26px;
flex: 1; font-weight: 700;
width: auto; line-height: 35px;
height: 26px; color: @finance-ink;
border: none;
border-radius: @radius-sm;
background: @finance-field-bg;
font-size: 11px;
&::placeholder {
color: @contract-placeholder;
}
} }
// 设计顺序:… 重置 搜索(FilterPanel 模板为搜索在前,用 order 调序) // 工具条 (70,252) 1780×71
:deep(.filter-panel__btn--primary) { &__toolbar {
order: 3; .flex(row, flex-start, center);
height: 26px;
padding: 0 14px;
border: none;
background: @primary-strong;
font-size: 11px;
&:hover {
background: @primary-color;
}
}
:deep(.filter-panel__btn:not(.filter-panel__btn--primary)) {
order: 2;
height: 26px;
padding: 0 14px;
border: none;
background: @project-quiet-btn;
color: @text-color;
font-size: 11px;
&:hover {
border: none;
background: @border-color;
color: @text-color;
}
}
}
// 状态 Tab:描边分段组,选中实底 #024FFF(设计 30)
&__tabs {
.flex(row, flex-start);
gap: 0; gap: 0;
height: 71px;
margin-top: 14px; // 统计卡底 238 → 工具条顶 252
padding: 0 0 0 22px; // Tab 组 x92,卡边框内起点 x70 → 内缩 22
border: 1px solid @finance-card-line;
border-radius: @radius-base;
background: @white;
box-sizing: border-box;
}
// Tab 组 (92,270.5) 279×34
&__tabs {
.flex(row, flex-start, center);
gap: 0;
width: 279px;
height: 34px;
border: 1px solid @border-color; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: 4px;
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
box-sizing: border-box;
// Tab 盒 (92,270.5) 279×34 → 文字 ink x103.57;等分 69.75
padding: 0 11.3px 0 9.2px;
} }
&__tab { &__tab {
height: 26px; flex: 1 1 0;
padding: 0 14px; height: 32px;
border: none; border: none;
border-right: 1px solid @border-color; border-right: 1px solid @border-color;
border-radius: 0; border-radius: 0;
background: @white; background: @white;
color: @text-color; color: @finance-entry-text;
font-size: 11px; font-size: 12px;
font-weight: 400;
transition: all 0.2s; transition: all 0.2s;
&:last-child { &:last-child {
@@ -247,34 +271,100 @@ onMounted(() => {
border-right-color: @primary-strong; border-right-color: @primary-strong;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 11px;
font-weight: 500;
} }
} }
// 表格 + 分页共处白卡(设计 30:分页在卡片内右下角) // 搜索框 (405,269) 1300×37 底 #F3F4F6
&__search {
width: 1300px;
height: 37px;
margin-left: 33px; // Tab 组右缘 371 → 搜索框 405
padding: 0 16px;
border: none;
border-radius: 0;
background: @finance-field-bg;
color: @finance-entry-text;
font-size: 13px;
outline: none;
box-sizing: border-box;
flex-shrink: 0;
&::placeholder {
color: @contract-placeholder;
}
}
&__btn {
height: 32px;
border: none;
border-radius: 0;
background: transparent;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
&--reset {
width: 62.2px; // 重置 x1721.4 文字 43.2 居中
margin-left: 15.4px; // 搜索框右缘 1705 → 重置盒 1721.4
color: @finance-entry-text;
&:hover {
color: @primary-strong;
}
}
&--primary {
width: 62.2px; // 搜索 x1783.4 文字 43.2 居中
color: @white;
background: @primary-strong;
&:hover {
background: @primary-color;
}
}
}
// 表格卡 (70,337) 1780×365.36(含分页)
&__board { &__board {
margin-top: 14px; // 工具条底 323 → 表格卡顶 337
border: 1px solid @finance-card-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base;
overflow: hidden; overflow: hidden;
} }
// 分页:右对齐小方格,选中浅蓝底蓝字(设计 30/35) // 分页:右对齐,页码 12/400;设计 (1631.93~1823.93, 670.36)
.pagination { :deep(.pagination) {
justify-content: flex-end; justify-content: flex-end;
padding: 12px @spacing-base @spacing-base; gap: 3.7px; // 设计页码间距:盒距 6 → 页码字距 3.7
padding: 8px 26px 30px 0; // 下行上缘 662 → 表格底,页码 ink 670.36
} }
:deep(.pagination__btn) { :deep(.pagination__btn) {
min-width: 24px; min-width: 0;
height: 24px; height: 16px;
border-color: @border-color; padding: 0 3px;
border-radius: @radius-sm; border: none;
color: @text-color-secondary; border-radius: 0;
font-size: 11px; background: transparent;
color: @finance-muted-text;
font-size: 12px;
font-weight: 400;
&.is-active { &.is-active {
border-color: @primary-strong; min-width: 21.45px;
background: @finance-page-active; }
&:hover:not(:disabled):not(.is-ellipsis) {
border: none;
color: @primary-strong;
}
&.is-active {
border: none;
background: transparent;
color: @primary-strong; color: @primary-strong;
} }
} }

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { AppButton } from '@/components'
import type { Bill } from '@/types/business/finance' import type { Bill } from '@/types/business/finance'
import { formatMoney } from '@/utils/format' import { formatMoney } from '@/utils/format'
@@ -13,12 +12,6 @@ const props = defineProps<Props>()
const emit = defineEmits<{ batchPay: [ids: string[]] }>() const emit = defineEmits<{ batchPay: [ids: string[]] }>()
const totalCount = computed(() => props.bills.length)
const totalAmount = computed(() => props.bills.reduce((sum, bill) => sum + bill.amount, 0))
const pendingCount = computed(() => props.bills.filter((bill) => bill.status === '待支付').length)
const selectedBills = computed(() => props.bills.filter((bill) => props.selectedIds.includes(bill.id))) const selectedBills = computed(() => props.bills.filter((bill) => props.selectedIds.includes(bill.id)))
const selectedAmount = computed(() => const selectedAmount = computed(() =>
@@ -34,103 +27,114 @@ function onBatchPay(): void {
</script> </script>
<template> <template>
<!-- 设计 30:整幅白条——左「已选择 N 笔订单」+ 统计,右「合计:¥ 金额 + 立即支付」 --> <!-- 设计 30:通栏白条 (0,718) 1920×66,左右内容对齐 x27.25 / x1625.03~1855.4 -->
<div class="bill-summary"> <div class="bill-summary">
<div class="bill-summary__inner">
<div class="bill-summary__left"> <div class="bill-summary__left">
<span class="bill-summary__selected"> <input
已选择<strong>{{ selectedBills.length }}</strong>笔订单 class="bill-summary__check"
</span> type="checkbox"
<div class="bill-summary__stats"> :checked="canPay"
<span class="bill-summary__stat">总笔数:<strong>{{ totalCount }}</strong></span> @change="onBatchPay"
<span class="bill-summary__stat">总金额:<strong>¥{{ formatMoney(totalAmount) }}</strong></span> >
<span class="bill-summary__stat">待支付:<strong>{{ pendingCount }}</strong></span> <span class="bill-summary__text">已选择</span>
</div> <span class="bill-summary__count">{{ selectedBills.length }}</span>
<span class="bill-summary__text">笔订单</span>
</div> </div>
<div class="bill-summary__right"> <div class="bill-summary__right">
<span class="bill-summary__label">合计:</span> <span class="bill-summary__text">合计:</span>
<span class="bill-summary__amount">¥{{ formatMoney(selectedAmount) }}</span> <span class="bill-summary__amount">¥ {{ formatMoney(selectedAmount, 0) }}</span>
<AppButton <button class="bill-summary__cta" :disabled="!canPay" @click="onBatchPay">
type="primary"
:disabled="!canPay"
@click="onBatchPay"
>
立即支付 立即支付
</AppButton> </button>
</div>
</div> </div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.bill-summary { .bill-summary {
.flex(row, space-between, center); position: relative;
gap: @spacing-base; width: 100%;
margin: 0 -16px; height: 66px;
padding: 10px @spacing-base;
border-radius: 0;
background: @white; background: @white;
box-sizing: border-box;
flex-shrink: 0;
// 汇总条内容与页面内容区同步留白 70,与上方卡片左右对齐
&__inner {
.flex(row, space-between, center);
width: 1920px;
max-width: 100%;
height: 100%;
margin: 0 auto;
padding: 0 70px;
box-sizing: border-box;
}
&__left { &__left {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: @spacing-lg; gap: 0;
min-width: 0;
} }
&__selected { &__check {
font-size: 11px; width: 15px;
color: @text-color; height: 15px;
margin: 0 12.25px 0 0;
accent-color: @primary-strong;
flex-shrink: 0;
}
&__text {
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @finance-entry-text;
white-space: nowrap; white-space: nowrap;
}
strong { &__count {
margin: 0 2px; margin: 0 2.27px;
color: @danger-color; color: @danger-color;
font-size: 13px;
font-weight: 700; font-weight: 700;
} line-height: 17px;
}
&__stats {
.flex(row, flex-start, center);
gap: @spacing-lg;
font-size: 11px;
color: @text-color;
strong {
color: @heading-color;
font-weight: 600;
}
} }
&__right { &__right {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 6px; gap: 0;
flex-shrink: 0;
} }
&__label {
font-size: 11px;
color: @text-color;
}
// 合计金额:设计 30 实取 20 设计px → 15px 粗体红
&__amount { &__amount {
margin-right: @spacing-sm; margin: 0 16px 0 0;
font-size: 15px;
font-weight: 700;
color: @danger-color; color: @danger-color;
font-size: 20px;
font-weight: 700;
line-height: 26px;
white-space: nowrap; white-space: nowrap;
} }
// 「立即支付」:设计 30 实取橙色按钮 &__cta {
:deep(.app-button--primary) { width: 84px;
height: 28px; height: 40px;
padding: 0 14px; border: none;
border-radius: 4px;
background: @finance-summary-cta; background: @finance-summary-cta;
color: @white; color: @white;
font-size: 11px; font-size: 13px;
font-weight: 500;
transition: background 0.2s;
&:hover { &:hover:not(:disabled) {
background: @warning-color; background: @warning-color;
} }
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
} }
} }
</style> </style>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { StatusTag } from '@/components' import type { Bill } from '@/types/business/finance'
import type { Bill, BillStatus } from '@/types/business/finance'
import { formatMoney } from '@/utils/format' import { formatMoney } from '@/utils/format'
interface Props { interface Props {
@@ -27,6 +26,14 @@ watch(
}, },
) )
const allSelected = computed(
() => props.bills.length > 0 && props.bills.every((bill) => selection.value.has(bill.id)),
)
const someSelected = computed(
() => props.bills.some((bill) => selection.value.has(bill.id)) && !allSelected.value,
)
function toggle(id: string): void { function toggle(id: string): void {
if (selection.value.has(id)) selection.value.delete(id) if (selection.value.has(id)) selection.value.delete(id)
else selection.value.add(id) else selection.value.add(id)
@@ -34,8 +41,7 @@ function toggle(id: string): void {
} }
function toggleAll(): void { function toggleAll(): void {
const allSelected = props.bills.length > 0 && props.bills.every((b) => selection.value.has(b.id)) if (allSelected.value) {
if (allSelected) {
for (const bill of props.bills) selection.value.delete(bill.id) for (const bill of props.bills) selection.value.delete(bill.id)
} }
else { else {
@@ -44,57 +50,81 @@ function toggleAll(): void {
emit('update:selection', [...selection.value]) emit('update:selection', [...selection.value])
} }
const allSelected = () => props.bills.length > 0 && props.bills.every((b) => selection.value.has(b.id)) /** 列宽(设计 30 表头实测:序号 x121.5 / 账单名称 x384.9 / 账单号 x695.09 / 流水号 x843.12 /
* 项目名称 x1237.19 / 订单金额 x1576.6 / 状态 x1673.6 / 操作 x1768.6) */
const STATUS_VARIANTS: Record<BillStatus, 'warning' | 'success' | 'info' | 'danger'> = { const COLUMNS = [
待支付: 'warning', { key: 'index', label: '序号', className: 'bill-table__col--index' },
已支付: 'success', { key: 'planName', label: '账单名称', className: 'bill-table__col--name' },
未到期: 'info', { key: 'billNo', label: '账单号', className: 'bill-table__col--no' },
已取消: 'danger', { key: 'tradeNo', label: '交易流水号', className: 'bill-table__col--trade' },
} { key: 'projectName', label: '项目名称', className: 'bill-table__col--project' },
{ key: 'amount', label: '订单金额', className: 'bill-table__col--amount' },
{ key: 'status', label: '状态', className: 'bill-table__col--status' },
{ key: 'ops', label: '操作', className: 'bill-table__col--ops' },
] as const
</script> </script>
<template> <template>
<div class="bill-table card"> <table class="bill-table">
<table class="bill-table__table"> <colgroup>
<col class="bill-table__col--check">
<col
v-for="column in COLUMNS"
:key="column.key"
:class="column.className"
>
</colgroup>
<thead> <thead>
<tr> <tr>
<th class="bill-table__check"> <th class="bill-table__cell bill-table__cell--check">
<input <input
type="checkbox" type="checkbox"
:checked="allSelected()" :checked="allSelected"
:indeterminate="someSelected"
@change="toggleAll" @change="toggleAll"
> >
</th> </th>
<th>序号</th> <th
<th>账单名称</th> v-for="column in COLUMNS"
<th>账单号</th> :key="column.key"
<th>交易流水号</th> class="bill-table__cell"
<th>项目名称</th> >
<th>订单金额</th> {{ column.label }}
<th>状态</th> </th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(bill, index) in bills" :key="bill.id"> <tr v-for="(bill, index) in bills" :key="bill.id">
<td class="bill-table__check"> <td class="bill-table__cell bill-table__cell--check">
<input <input
type="checkbox" type="checkbox"
:checked="selection.has(bill.id)" :checked="selection.has(bill.id)"
@change="toggle(bill.id)" @change="toggle(bill.id)"
> >
</td> </td>
<td>{{ index + 1 }}</td> <td class="bill-table__cell">
<td>{{ bill.planName }}</td> {{ index + 1 }}
<td>{{ bill.billNo }}</td>
<td>{{ bill.tradeNo ?? '—' }}</td>
<td>{{ bill.projectName }}</td>
<td class="bill-table__amount">¥{{ formatMoney(bill.amount) }}</td>
<td>
<StatusTag :label="bill.status" :variant="STATUS_VARIANTS[bill.status]" />
</td> </td>
<td class="bill-table__ops"> <td class="bill-table__cell">
{{ bill.planName }}
</td>
<td class="bill-table__cell">
{{ bill.billNo }}
</td>
<td class="bill-table__cell">
{{ bill.tradeNo ?? '—' }}
</td>
<td class="bill-table__cell">
{{ bill.projectName }}
</td>
<td class="bill-table__cell">
{{ formatMoney(bill.amount, 0) }}
</td>
<td class="bill-table__cell">
{{ bill.status }}
</td>
<td class="bill-table__cell bill-table__cell--ops">
<span class="bill-table__ops">
<button <button
v-if="bill.status === '待支付'" v-if="bill.status === '待支付'"
class="bill-table__link" class="bill-table__link"
@@ -105,116 +135,147 @@ const STATUS_VARIANTS: Record<BillStatus, 'warning' | 'success' | 'info' | 'dang
<button class="bill-table__link" @click="emit('view', bill.id)"> <button class="bill-table__link" @click="emit('view', bill.id)">
详情 详情
</button> </button>
</span>
</td> </td>
</tr> </tr>
<tr v-if="bills.length === 0"> <tr v-if="bills.length === 0">
<td colspan="9" class="bill-table__empty text-secondary"> <td colspan="9" class="bill-table__empty">
暂无账单数据 暂无账单数据
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 30:表格贴满白卡(外层 .bills__board 提供卡底),全网格浅灰线 + 浅灰表头 // 设计 30:表头 (77.5,344.5) 1765×41.77 底 #F1F3F5;行高 43.77;文字 12px #4B5563
.bill-table { .bill-table {
padding: 0; width: 1765px;
border-radius: 0; margin: 6.5px 0 0 14.5px; // 表头 x77.5 / y344.5,卡片内缘 x71 / y338
background: transparent;
box-shadow: none;
overflow-x: auto;
&__table {
width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 11px; table-layout: fixed;
color: @text-color; font-size: 12px;
color: @finance-entry-text;
th, // 列宽按设计表头 x 递推(表头起点 x78,实宽 1765):
td { // 序号 x121.5|账单名称 x384.9|账单号 x695.09|流水 x843.12|项目 x1237.19|金额 x1576.6|状态 x1673.6|操作 x1768.6
padding: 11px 10px; &__col--check { width: 51px; }
&__col--index { width: 235.4px; }
&__col--name { width: 310.2px; }
&__col--no { width: 148px; }
&__col--trade { width: 394.1px; }
&__col--project { width: 339.4px; }
&__col--amount { width: 97px; }
&__col--status { width: 95px; }
&__col--ops { width: 95.5px; }
&__cell {
height: 43.77px;
padding: 0;
border-bottom: 1px solid @project-divider;
text-align: left; text-align: left;
vertical-align: middle;
white-space: nowrap; white-space: nowrap;
border: 1px solid @project-divider; overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
// 单元格统一左内缩 10(除已单独指定的列)
&:not(&--check):not(&--ops) {
padding-left: 10px;
} }
th { &--check {
padding-left: 43.5px; // 勾选框/序号文字 x121.5 → 表头起点 78
}
// 操作列内层 flex 已负责右对齐;单元格本身只管纵向居中
&--ops {
padding-right: 22px;
text-align: right;
}
}
// 各列文字起点对齐设计 x(表头起点 78):内缩 = 设计 x − 列起点
@col-pad-1: 43.5px; // 序号 121.5
@col-pad-2: 149.9px; // 账单名称 384.9
@col-pad-3: 8px; // 账单号 695.09
@col-pad-4: 17px; // 流水号 843.12
@col-pad-5: 5px; // 项目名称 1237.19
@col-pad-6: 14.6px; // 订单金额 1576.6
@col-pad-7: 4.6px; // 状态 1673.6
@col-pad-8: 6px; // 操作 1768.6(右对齐由内层 flex 兜)
thead &__cell,
tbody &__cell {
&:nth-child(1) { padding-left: @col-pad-1; }
&:nth-child(2) { padding-left: @col-pad-2; }
&:nth-child(3) { padding-left: @col-pad-3; }
&:nth-child(4) { padding-left: @col-pad-4; }
&:nth-child(5) { padding-left: @col-pad-5; }
&:nth-child(6) { padding-left: @col-pad-6; }
&:nth-child(7) { padding-left: @col-pad-7; }
&:nth-child(8) { padding-left: @col-pad-8; }
&:nth-child(9) { padding-left: 0; padding-right: 10px; }
}
thead &__cell {
height: 41.77px;
background: @finance-table-head-bg; background: @finance-table-head-bg;
color: @text-color; border-right: 1px solid @project-divider;
color: @finance-entry-text;
font-size: 12px;
font-weight: 600; font-weight: 600;
text-align: center;
&:first-child {
border-left: 1px solid @project-divider;
}
} }
tbody tr:hover { tbody tr:hover {
background: @bg-color-light; background: @bg-color-light;
} }
}
// 列对齐按设计 30:序号/账单号/流水号/金额/状态/操作居中,名称类左对齐 &__cell--check input {
&__table th:nth-child(1),
&__table th:nth-child(2),
&__table td:nth-child(1),
&__table td:nth-child(2),
&__table th:nth-child(4),
&__table td:nth-child(4),
&__table th:nth-child(5),
&__table td:nth-child(5),
&__table th:nth-child(7),
&__table td:nth-child(7),
&__table th:nth-child(8),
&__table td:nth-child(8),
&__table th:nth-child(9),
&__table td:nth-child(9) {
text-align: center;
}
&__check {
width: 44px;
text-align: center;
input {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin: 0;
accent-color: @primary-strong; accent-color: @primary-strong;
vertical-align: middle; vertical-align: middle;
} }
}
// 金额列与正文同色(设计 30 实取 #4B5563 常规字重)
&__amount {
color: @text-color;
font-weight: 400;
}
&__ops { &__ops {
.flex(row, center, center); display: inline-flex;
gap: 12px; flex-direction: row;
align-items: center;
justify-content: flex-end;
white-space: nowrap;
} }
&__link { &__link {
// 设计:操作列起点 1746,末位「详情」右缘 1820.36 → 单元格右缘 1842.5
padding: 0;
border: none;
background: transparent;
color: @primary-strong; color: @primary-strong;
font-size: 11px; font-size: 12px;
font-weight: 400;
transition: color 0.2s; transition: color 0.2s;
// 「去支付」「详情」并排:设计文字盒距 18.36
+ .bill-table__link {
margin-left: 18.36px;
}
&:hover { &:hover {
color: @primary-color; color: @primary-color;
} }
} }
&__empty { &__empty {
height: 43.77px;
color: @project-muted;
text-align: center; text-align: center;
padding: @spacing-xl 0;
}
// 状态列按设计 30 为纯文字(去底色徽标)
:deep(.status-tag) {
padding: 0;
background: transparent;
color: @text-color;
font-size: 11px;
line-height: 1.5;
} }
} }
</style> </style>

View File

@@ -15,7 +15,7 @@ const props = defineProps<Props>()
const total = computed(() => props.distribution.reduce((sum, item) => sum + item.value, 0)) const total = computed(() => props.distribution.reduce((sum, item) => sum + item.value, 0))
/** CSS conic-gradient(无图表库):按 value 占比切分色环 */ /** CSS conic-gradient(无图表库):按 value 占比切分色环(本页隐藏,仅保留绑定) */
const gradient = computed(() => { const gradient = computed(() => {
if (total.value <= 0) return 'conic-gradient(#e4e7ec 0% 100%)' if (total.value <= 0) return 'conic-gradient(#e4e7ec 0% 100%)'
let acc = 0 let acc = 0
@@ -31,23 +31,33 @@ function percentOf(value: number): number {
if (total.value <= 0) return 0 if (total.value <= 0) return 0
return Math.round((value / total.value) * 100) return Math.round((value / total.value) * 100)
} }
/** 设计稿 Y 轴刻度(等距 5 档) */
const AXIS = [250, 200, 150, 100, 50]
/** 设计稿 X 轴月份刻度 */
const MONTHS = ['1月', '2月', '3月', '4月', '5月']
</script> </script>
<template> <template>
<div class="finance-chart card"> <div class="finance-chart">
<h3 class="finance-chart__title"> <h3 class="finance-chart__title">
业务类型分布 业务类型分布
</h3> </h3>
<div class="finance-chart__body"> <div class="finance-chart__body">
<!-- 设计 37:横向网格线 + 等宽柱条(柱高按占比换算) --> <!-- Y 轴刻度 10/400 #9CA3AF -->
<ul class="finance-chart__axis" aria-hidden="true">
<li v-for="v in AXIS" :key="v" class="finance-chart__axis-item">
{{ v }}
</li>
</ul>
<div class="finance-chart__plot"> <div class="finance-chart__plot">
<!-- 5 条横向网格线(设计 #EDEFF2,1px) -->
<div class="finance-chart__grid" aria-hidden="true"> <div class="finance-chart__grid" aria-hidden="true">
<i class="finance-chart__line" /> <i v-for="v in AXIS" :key="v" class="finance-chart__line" />
<i class="finance-chart__line" />
<i class="finance-chart__line" />
<i class="finance-chart__line" />
<i class="finance-chart__line" />
</div> </div>
<div class="finance-chart__bars"> <div class="finance-chart__bars">
<span <span
v-for="item in distribution" v-for="item in distribution"
@@ -56,42 +66,54 @@ function percentOf(value: number): number {
:style="{ height: `${Math.max(percentOf(item.value), 6)}%` }" :style="{ height: `${Math.max(percentOf(item.value), 6)}%` }"
/> />
</div> </div>
</div>
<!-- 数据源色环:设计稿为柱状图,此处仅保留绑定(隐藏)以免游离变量 --> <!-- 数据源色环:设计稿为柱状图,此处仅保留绑定(隐藏)以免游离变量 -->
<div class="finance-chart__pie" :style="{ background: gradient }" /> <div class="finance-chart__pie" :style="{ background: gradient }" />
<!-- X 轴 11/400 #6B7280 -->
<ul class="finance-chart__months" aria-hidden="true">
<li v-for="m in MONTHS" :key="m" class="finance-chart__month">
{{ m }}
</li>
</ul>
</div>
</div>
<ul class="finance-chart__legend"> <ul class="finance-chart__legend">
<li <li
v-for="item in distribution" v-for="item in distribution"
:key="item.name" :key="item.name"
class="finance-chart__legend-item" class="finance-chart__legend-item"
> >
<span
class="finance-chart__dot"
:style="{ background: item.color }"
/>
<span class="finance-chart__name">{{ item.name }}</span> <span class="finance-chart__name">{{ item.name }}</span>
<span class="finance-chart__value">{{ percentOf(item.value) }}%</span>
</li> </li>
</ul> </ul>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 37:白卡 + 蓝色竖条标题(15 设计px → 12px/700)+ 柱状图 + 横排图例 // 设计 6cd4aa76:图表卡 (70,344) 1780×378、白底 + #F0F1F3 描边;
// 标题 ink (82,367) 15/700 #1F2937(竖条 4×20 @#024FFF);
// Y 轴刻度 ink100 (103,403/461.5/520/578.5/637) 10/400 #9CA3AF,等距 58.5;
// 网格线 x137..1823 宽 1686 @408/466.5/525/583.5/642;X 轴月份 ink100 y649 11/400;
// 图例 ink100 y684(互联网软件 x809 / 物联网全案 x903 / 硬件电路 x997 / 生产制造 x1080)11/400
.finance-chart { .finance-chart {
padding: 20px @spacing-lg @spacing-lg; position: relative;
height: 378px; // 设计 6cd4aa76:图表卡 (70,344) 1780×378
margin-top: 14px; // 概览行底 330 → 图表卡顶 344
padding: 23px 23px 0; // 标题 ink y367 → 卡内 23(含 1px 描边);左右 x82 → 卡内 12
border: 1px solid @finance-card-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
&__title { &__title {
position: relative; position: relative;
padding-left: 10px; padding-left: 12px;
margin-bottom: @spacing-base; font-size: 15px;
font-size: 12px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 20px;
color: @finance-ink;
&::before { &::before {
content: ''; content: '';
@@ -99,22 +121,44 @@ function percentOf(value: number): number {
top: 50%; top: 50%;
left: 0; left: 0;
width: 4px; width: 4px;
height: 14px; height: 20px;
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
transform: translateY(-50%); transform: translateY(-50%);
} }
} }
// 绘图区:标题盒底 387 → 首条网格线 408(21)
&__body { &__body {
.flex(column, center, center); .flex(row, flex-start, flex-start);
gap: @spacing-base; gap: 0;
margin-top: 21px;
}
// Y 轴刻度列:刻度 ink x103(卡内 33)→ 网格线 x137(卡内 67)
&__axis {
.flex(column, space-between, flex-start);
width: 34px;
height: 234px; // 网格线 408 → 642(5 档 4 段,段距 58.5)
margin: 0;
padding: 0;
list-style: none;
box-sizing: border-box;
}
&__axis-item {
font-size: 10px;
font-weight: 400;
line-height: 14px;
color: @finance-axis-text;
transform: translateY(-7px); // 刻度 ink 与网格线同一 y 对齐(去掉行盒偏移)
} }
&__plot { &__plot {
position: relative; position: relative;
width: 100%; flex: 1;
height: 184px; min-width: 0;
height: 234px;
} }
&__grid { &__grid {
@@ -134,7 +178,7 @@ function percentOf(value: number): number {
.flex(row, space-around, flex-end); .flex(row, space-around, flex-end);
position: absolute; position: absolute;
inset: 0; inset: 0;
padding: 0 60px; padding: 0 40px;
} }
&__bar { &__bar {
@@ -149,34 +193,44 @@ function percentOf(value: number): number {
display: none; display: none;
} }
&__legend { // X 轴月份:网格线底 642 → 月份 ink 649(行盒顶 645)
.flex(row, center, center); &__months {
gap: @spacing-lg; .flex(row, space-around, flex-start);
list-style: none; position: absolute;
top: 237px;
left: 0;
right: 0;
margin: 0;
padding: 0; padding: 0;
list-style: none;
}
&__month {
font-size: 11px;
font-weight: 400;
line-height: 15px;
color: @finance-muted-text;
}
// 图例:月份盒底 662 → 图例 ink 684(行盒顶 681)
&__legend {
.flex(row, flex-start, center);
gap: 27px;
margin: 41px 0 0;
padding: 0 0 0 662px; // 图例首项 ink x809 → 卡内 692(轴 34 + 网格起点)
list-style: none;
} }
&__legend-item { &__legend-item {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 6px; gap: 6px;
font-size: 11px; font-size: 11px;
color: @text-color-secondary; font-weight: 400;
} color: @finance-muted-text;
&__dot {
width: 8px;
height: 8px;
border-radius: @radius-round;
flex-shrink: 0;
} }
&__name { &__name {
min-width: 0; min-width: 0;
} }
&__value {
color: @heading-color;
font-weight: 600;
}
} }
</style> </style>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { StatusTag } from '@/components'
import type { FinanceStats } from '@/types/business/finance' import type { FinanceStats } from '@/types/business/finance'
import { formatMoney } from '@/utils/format' import { formatMoney } from '@/utils/format'
@@ -16,140 +15,73 @@ const emit = defineEmits<{
</script> </script>
<template> <template>
<div class="finance-overview card"> <div class="finance-overview">
<!-- 卡内文字块:相对卡顶 26(设计 221 − 195) -->
<div class="finance-overview__main"> <div class="finance-overview__main">
<p class="finance-overview__label text-secondary"> <p class="finance-overview__label">
累计消费总额 累计消费总额
</p> </p>
<p class="finance-overview__amount"> <p class="finance-overview__amount">
¥{{ formatMoney(stats?.totalExpense ?? 0) }} ¥ {{ formatMoney(stats?.totalExpense ?? 0) }}
</p> </p>
<div class="finance-overview__meta">
<div class="finance-overview__periods">
<button
class="finance-overview__period"
@click="emit('periodChange', 'quarter')"
>
本季度
</button>
<button
class="finance-overview__period"
@click="emit('periodChange', 'year')"
>
本年
</button>
</div>
<span class="finance-overview__month text-secondary">{{ stats?.month }}</span>
<StatusTag
v-if="(stats?.pendingCount ?? 0) > 0"
:label="`待支付 ${stats?.pendingCount ?? 0}`"
variant="warning"
/>
</div>
</div> </div>
<div class="finance-overview__actions"> <!-- 右上角「2026年账单 ›」 -->
<RouterLink to="/finance/invoices" class="finance-overview__action"> <button class="finance-overview__period-link" @click="emit('periodChange', 'year')">
发票申请 {{ stats?.month || '2026年账单' }}
</RouterLink> <span aria-hidden="true">›</span>
<RouterLink to="/finance/bills" class="finance-overview__action">
账单明细
</RouterLink>
<button class="finance-overview__action" @click="emit('export')">
报表导出
</button> </button>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 37:整卡为蓝紫渐变(累计消费总额白字),右上角白色操作链接 // 设计 6cd4aa76:渐变卡 (70,195) 1093.84×135;标签 (100,221) 13/400 #FFFFFF;
// 金额 (100,254) 38/700;右上角「2026年账单 ›」(1066.98,220) 11/500
.finance-overview { .finance-overview {
.flex(column, flex-start, stretch); position: relative;
flex: 1 1 500px; flex: 1 1 auto;
min-width: 0; min-width: 0;
padding: 14px @spacing-lg 16px; height: 135px;
border-radius: @radius-base; border-radius: @radius-base;
background: @finance-hero-gradient; background: @finance-hero-gradient;
box-shadow: @shadow-base; box-sizing: border-box;
&__main { &__main {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 6px; padding: 26px 0 0 30px; // 标签 ink (100,221) → 卡内 (30,26)
gap: 16px; // 标签盒底 238 → 金额盒顶 254
} }
&__label { &__label {
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @white; color: @white;
font-size: 11px;
} }
&__amount { &__amount {
color: @white; font-size: 38px;
font-size: 29px;
font-weight: 700; font-weight: 700;
line-height: 1.15; line-height: 50px;
}
&__meta {
.flex(row, flex-start, center);
gap: @spacing-base;
margin-top: 4px;
font-size: 11px;
color: fade(@white, 85%);
}
&__periods {
.flex(row, flex-start);
gap: @spacing-sm;
}
&__period {
height: 22px;
padding: 0 @spacing-sm;
border: 1px solid fade(@white, 45%);
border-radius: @radius-sm;
background: fade(@white, 16%);
color: @white;
font-size: 11px;
transition: all 0.2s;
&:hover {
border-color: @white;
background: fade(@white, 30%);
color: @white; color: @white;
} }
}
&__month { // 右上角链接:设计 x1066.98 y220 → 卡内右缘 27、顶 25
&__period-link {
position: absolute;
top: 25px;
right: 27px;
border: none;
background: transparent;
font-size: 11px; font-size: 11px;
color: fade(@white, 85%); font-weight: 500;
} line-height: 15px;
// 待支付小徽标压到渐变上:半透明白底 + 白字
:deep(.status-tag) {
background: fade(@white, 20%);
color: @white; color: @white;
font-size: 11px;
}
// 操作链接置顶右对齐(对应设计右上角「2026年账单 ›」位置)
&__actions {
order: -1;
align-self: stretch;
.flex(row, flex-end, center);
gap: @spacing-base;
margin-bottom: 4px;
}
&__action {
font-size: 11px;
color: fade(@white, 92%);
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
color: @white; opacity: 0.8;
text-decoration: underline;
} }
} }
} }

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { StatusTag } from '@/components'
import type { InvoiceTitle } from '@/types/business/finance' import type { InvoiceTitle } from '@/types/business/finance'
interface Props { interface Props {
@@ -16,10 +15,11 @@ const emit = defineEmits<{
</script> </script>
<template> <template>
<!-- 设计 35/36:一行一张宽卡——左上默认徽标、右上操作按钮,下方三栏字段 --> <!-- 设计 35/36:整幅宽卡——左侧「默认抬头」蓝胶囊,右侧操作按钮;下方三栏字段 -->
<div class="invoice-title-card"> <div class="invoice-title-card">
<div class="invoice-title-card__top"> <div class="invoice-title-card__top">
<StatusTag v-if="invoiceTitle.isDefault" label="默认" variant="success" /> <span v-if="invoiceTitle.isDefault" class="invoice-title-card__badge">默认抬头</span>
<span v-else class="invoice-title-card__badge-placeholder" />
<div class="invoice-title-card__actions"> <div class="invoice-title-card__actions">
<button <button
class="invoice-title-card__btn" class="invoice-title-card__btn"
@@ -27,7 +27,7 @@ const emit = defineEmits<{
> >
{{ invoiceTitle.isDefault ? '取消默认' : '设为默认' }} {{ invoiceTitle.isDefault ? '取消默认' : '设为默认' }}
</button> </button>
<button class="invoice-title-card__btn is-danger" @click="emit('delete', invoiceTitle.id)"> <button class="invoice-title-card__btn" @click="emit('delete', invoiceTitle.id)">
删除 删除
</button> </button>
<button class="invoice-title-card__btn" @click="emit('edit', invoiceTitle)"> <button class="invoice-title-card__btn" @click="emit('edit', invoiceTitle)">
@@ -38,112 +38,114 @@ const emit = defineEmits<{
<div class="invoice-title-card__cols"> <div class="invoice-title-card__cols">
<div class="invoice-title-card__col"> <div class="invoice-title-card__col">
<StatusTag <span class="invoice-title-card__label">抬头类型</span>
:label="invoiceTitle.type" <span class="invoice-title-card__value">{{ invoiceTitle.type }}</span>
:variant="invoiceTitle.type === '企业单位' ? 'primary' : 'info'"
/>
</div> </div>
<div class="invoice-title-card__col"> <div class="invoice-title-card__col">
<span class="invoice-title-card__label text-secondary">发票抬头</span> <span class="invoice-title-card__label">发票抬头</span>
<p class="invoice-title-card__title"> <span class="invoice-title-card__value">{{ invoiceTitle.title }}</span>
{{ invoiceTitle.title }}
</p>
</div> </div>
<div class="invoice-title-card__col"> <div class="invoice-title-card__col">
<span class="invoice-title-card__label text-secondary">税号</span> <span class="invoice-title-card__label">税号</span>
<span class="invoice-title-card__tax-no">{{ invoiceTitle.taxNo }}</span> <span class="invoice-title-card__value">{{ invoiceTitle.taxNo }}</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 35:卡片 (70,229) 1780×129 / (70,372) 1780×88
.invoice-title-card { .invoice-title-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 0;
padding: 14px 20px; width: 1780px;
border: none; min-height: 88px;
padding: 0 33.56px 0 27px; // 徽标 x98.58、按钮右缘 1820.6(卡内 1750.6)
border: 1px solid @finance-card-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
// 顶行:默认徽标居左、操作按钮居右(设计 35 同一行) // 顶行:徽标 (98.58,249) 88.17×22 + 操作按钮 (1632.4,277.5) 各 12/500
&__top { &__top {
.flex(row, space-between, center); .flex(row, space-between, center);
min-height: 24px; height: 49px; // 徽标盒 y249(卡顶 229 + 20 居中)
flex-shrink: 0;
} }
// 「默认抬头」胶囊:蓝底白字 11/500
&__badge {
.flex(row, center, center);
height: 22px;
padding: 0 12px;
border-radius: 4px;
background: @primary-strong;
color: @white;
font-size: 11px;
font-weight: 500;
line-height: 15px;
}
&__badge-placeholder {
height: 22px;
}
// 操作按钮:纯文字 12/500 #4B5563,盒距 20.8(文字盒 67.2/43.2/43.2 + 间隔 4)
&__actions { &__actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: @spacing-sm; gap: 24px;
} }
// 描边小按钮:浅灰边白底 11px(设计 35 实取)
&__btn { &__btn {
height: 24px; padding: 0;
padding: 0 10px; border: none;
border: 1px solid @border-color; background: transparent;
border-radius: @radius-sm; color: @finance-entry-text;
background: @white; font-size: 12px;
color: @text-color; font-weight: 500;
font-size: 11px; line-height: 16px;
cursor: pointer; transition: color 0.2s;
transition: all 0.2s;
&:hover:not(:disabled) { &:hover {
border-color: @primary-strong;
color: @primary-strong; color: @primary-strong;
} }
&.is-danger:hover:not(:disabled) {
border-color: @danger-color;
color: @danger-color;
}
} }
// 三栏字段:列起点按设计 35(x97 / x579 / x1061,卡内 0 / 371 / 742 CSS) // 三栏:列起点(卡内,卡 x70 → 内容 x97/579/1061)= 0 / 482 / 964
&__cols { &__cols {
display: grid; .flex(row, flex-start, flex-start);
grid-template-columns: minmax(0, 371px) minmax(0, 371px) minmax(0, 1fr); gap: 0;
gap: @spacing-base; padding-top: 2px; // 顶行 59 → 标签 ink y293(卡内 64)
padding-bottom: 30px; // 值底 y335(卡内 106)→ 卡底 358
} }
&__col { &__col {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 6px; gap: 8px; // 标签 ink 293 → 值 ink 316
width: 482px;
box-sizing: border-box;
&:last-child {
width: auto;
flex: 1;
min-width: 0; min-width: 0;
} }
}
// 标签 11 设计px → 11px 灰;值 14 设计px → 11px 粗体深色
&__label { &__label {
font-size: 11px; font-size: 11px;
font-weight: 400;
line-height: 15px;
color: @finance-muted-text;
} }
&__title, &__value {
&__tax-no { font-size: 14px;
font-size: 11px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 19px;
color: @finance-ink;
word-break: break-all; word-break: break-all;
} }
// 抬头类型值:去掉徽标底色,按设计为纯文字
:deep(.status-tag) {
padding: 0;
background: transparent;
color: @heading-color;
font-size: 11px;
font-weight: 700;
align-self: flex-start;
}
// 「默认」徽标:设计 35 为实底蓝底白字小胶囊
:deep(.status-tag--success) {
padding: 2px 8px;
background: @primary-strong;
color: @white;
font-weight: 500;
}
} }
</style> </style>

View File

@@ -3,6 +3,7 @@ import { onMounted } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { PageHead } from '@/components'
// 页面容器:只做组件组合与事件中转,数据只经 store // 页面容器:只做组件组合与事件中转,数据只经 store
import { useFinanceStore } from '@/stores/modules/finance' import { useFinanceStore } from '@/stores/modules/finance'
import FinanceChart from './cpns/FinanceChart.vue' import FinanceChart from './cpns/FinanceChart.vue'
@@ -11,6 +12,13 @@ import FinanceOverview from './cpns/FinanceOverview.vue'
const financeStore = useFinanceStore() const financeStore = useFinanceStore()
const { stats } = storeToRefs(financeStore) const { stats } = storeToRefs(financeStore)
const ENTRIES = [
{ label: '账单明细', to: '/finance/bills' },
{ label: '待支付', to: '/finance/bills' },
{ label: '发票申请', to: '/finance/invoices' },
{ label: '报表导出', to: '' },
] as const
function onPeriodChange(period: string): void { function onPeriodChange(period: string): void {
void financeStore.fetchStats(period) void financeStore.fetchStats(period)
} }
@@ -27,15 +35,12 @@ onMounted(() => {
<template> <template>
<div class="finance"> <div class="finance">
<header class="finance__head"> <PageHead
<h1 class="finance__title"> title="财务中心"
财务中心 slogan="账单、支付、发票统一财务管理中心"
</h1> />
<p class="finance__slogan text-secondary">
账单支付、发票申请与报表导出一站式管理。
</p>
</header>
<div class="container">
<div class="finance__top"> <div class="finance__top">
<FinanceOverview <FinanceOverview
:stats="stats" :stats="stats"
@@ -44,75 +49,65 @@ onMounted(() => {
/> />
<div class="finance__entries"> <div class="finance__entries">
<RouterLink to="/finance/invoices" class="finance__entry"> <template v-for="entry in ENTRIES" :key="entry.label">
<span class="finance__entry-icon">🧾</span> <RouterLink
<span class="finance__entry-name">发票申请</span> v-if="entry.to"
:to="entry.to"
class="finance__entry"
>
{{ entry.label }}
</RouterLink> </RouterLink>
<RouterLink to="/finance/bills" class="finance__entry"> <button
<span class="finance__entry-icon">📋</span> v-else
<span class="finance__entry-name">账单明细</span> class="finance__entry"
</RouterLink> @click="onExport"
<button class="finance__entry" @click="onExport"> >
<span class="finance__entry-icon">📊</span> {{ entry.label }}
<span class="finance__entry-name">报表导出</span>
</button> </button>
</template>
</div> </div>
</div> </div>
<FinanceChart :distribution="stats?.distribution ?? []" /> <FinanceChart :distribution="stats?.distribution ?? []" />
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 财务中心概览(设计 6cd4aa76,1920 画布 1:1)
// 页头区 (70,162) 1780×17 副标语 13/400 #6B7280(标题 22/700 由全站页头统一渲染)
// 概览行 y195 h135:左渐变卡 1093.84 宽、右白卡 672.16 宽(间距 14)
// 图表卡 (70,344) 1780×378,白底 + #F0F1F3 描边
.finance { .finance {
.flex(column, flex-start);
align-items: stretch;
gap: @spacing-base;
width: 100%; width: 100%;
max-width: @container-width;
margin: 0 auto;
padding: 40px @spacing-base @spacing-xxl;
// 设计 37:页头标题 22 设计px × 0.77 ≈ 17px 加粗,副标语 13 设计px → 11px 居中 // 顶部行:左渐变卡 + 右快捷卡,间距 14
&__title {
font-size: 17px;
font-weight: 700;
color: @heading-color;
}
&__slogan {
margin-top: @spacing-xs;
font-size: 11px;
text-align: center;
}
// 顶部行:左「累计消费」渐变卡 + 右快捷入口白卡(设计 37 同排并列)
&__top { &__top {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
gap: @spacing-base; gap: 14px;
flex-wrap: wrap; margin-top: -56px; // 页头白条通栏高 179,卡片按设计压在 y195
} }
// 右侧快捷入口白卡 672.16×135
&__entries { &__entries {
.flex(row, space-around, center); .flex(row, space-between, center);
flex: 0 0 500px; flex: 0 0 672px;
gap: @spacing-base; height: 135px;
padding: @spacing-base @spacing-lg; padding: 0 34px;
border-radius: @radius-base; border: 1px solid @finance-card-line;
border-radius: @radius-base; // 设计无圆角?——卡面 8 以内,与全站一致
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
} }
&__entry { &__entry {
.flex(column, center, center); .flex(row, center, center);
gap: @spacing-sm; height: 100%;
width: auto;
height: auto;
border: none; border: none;
border-radius: 0;
background: transparent; background: transparent;
color: @text-color; color: @finance-entry-text;
font-size: 11px; font-size: 11px;
font-weight: 400;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
@@ -121,14 +116,5 @@ onMounted(() => {
color: @primary-strong; color: @primary-strong;
} }
} }
&__entry-icon {
.flex(row, center, center);
width: 36px;
height: 36px;
border-radius: @radius-base;
background: @bg-color;
font-size: 18px;
}
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { AppButton, ConfirmDialog } from '@/components' import { ConfirmDialog } from '@/components'
// 页面容器:数据只经 store,不直接 import api // 页面容器:数据只经 store,不直接 import api
import { useFinanceStore } from '@/stores/modules/finance' import { useFinanceStore } from '@/stores/modules/finance'
import type { InvoiceTitle, InvoiceTitlePayload } from '@/types/business/finance' import type { InvoiceTitle, InvoiceTitlePayload } from '@/types/business/finance'
@@ -12,28 +12,27 @@ import InvoiceTitleDialog from './cpns/InvoiceTitleDialog.vue'
const financeStore = useFinanceStore() const financeStore = useFinanceStore()
const { titles } = storeToRefs(financeStore) const { titles } = storeToRefs(financeStore)
type TypeFilter = 'all' | InvoiceTitle['type']
const typeFilter = ref<TypeFilter>('all')
const TYPE_TABS: Array<{ label: string; value: TypeFilter }> = [
{ label: '全部', value: 'all' },
{ label: '企业单位', value: '企业单位' },
{ label: '非企业单位', value: '非企业单位' },
]
const filteredTitles = computed(() =>
typeFilter.value === 'all'
? titles.value
: titles.value.filter((item) => item.type === typeFilter.value),
)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const editing = ref<InvoiceTitle | undefined>(undefined) const editing = ref<InvoiceTitle | undefined>(undefined)
const confirmVisible = ref(false) const confirmVisible = ref(false)
const pendingDeleteId = ref('') const pendingDeleteId = ref('')
const PAGE_SIZE = 2
const page = ref(1)
const pageCount = computed(() => Math.max(1, Math.ceil(titles.value.length / PAGE_SIZE)))
const pagedTitles = computed(() =>
titles.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE),
)
/** 页码序列:设计 35 实取 1~5 */
const pages = computed(() => Array.from({ length: Math.max(5, pageCount.value) }, (_, i) => i + 1))
function go(next: number): void {
if (next < 1 || next > pageCount.value) return
page.value = next
}
function onCreate(): void { function onCreate(): void {
editing.value = undefined editing.value = undefined
dialogVisible.value = true dialogVisible.value = true
@@ -69,12 +68,8 @@ async function onConfirmDelete(): Promise<void> {
async function onSetDefault(id: string): Promise<void> { async function onSetDefault(id: string): Promise<void> {
const item = titles.value.find((title) => title.id === id) const item = titles.value.find((title) => title.id === id)
if (!item) return if (!item) return
if (item.isDefault) {
ElMessage.info('演示环境暂不支持取消默认,可设其他抬头为默认')
return
}
await financeStore.setDefaultTitle(id) await financeStore.setDefaultTitle(id)
ElMessage.success('已设为默认抬头') ElMessage.success(item.isDefault ? '已取消默认抬头' : '已设为默认抬头')
} }
onMounted(() => { onMounted(() => {
@@ -84,48 +79,60 @@ onMounted(() => {
<template> <template>
<div class="invoice-titles"> <div class="invoice-titles">
<nav class="invoice-titles__breadcrumb text-secondary"> <div class="container">
<RouterLink to="/finance" class="link"> <!-- 面包屑 (70,124)「财务中心 /」13/400 #9CA3AF +「发票抬头管理」13/500 #4B5563 -->
财务中心 <nav class="invoice-titles__breadcrumb">
<RouterLink to="/finance" class="invoice-titles__breadcrumb-link">
财务中心 /
</RouterLink> </RouterLink>
<span> &gt; </span> <span class="invoice-titles__breadcrumb-current">发票抬头管理</span>
<span>发票抬头</span>
</nav> </nav>
<!-- 标题条 (70,153) 1780×62:标题 x106 y174 15/700 + 右侧「+ 新增抬头」 -->
<header class="invoice-titles__head"> <header class="invoice-titles__head">
<h2 class="invoice-titles__title"> <h2 class="invoice-titles__title">
发票管理 发票管理
</h2> </h2>
<div class="invoice-titles__filters"> <button class="invoice-titles__create" @click="onCreate">
<button + 新增抬头
v-for="tab in TYPE_TABS"
:key="tab.value"
class="invoice-titles__filter"
:class="{ 'is-active': typeFilter === tab.value }"
@click="typeFilter = tab.value"
>
{{ tab.label }}
</button> </button>
<AppButton type="primary" @click="onCreate">
+新增抬头
</AppButton>
</div>
</header> </header>
<div class="invoice-titles__grid"> <!-- 抬头卡列表 (70,229) 起 1780×129 / (70,372) 1780×88 -->
<div class="invoice-titles__list">
<InvoiceTitleCard <InvoiceTitleCard
v-for="item in filteredTitles" v-for="item in pagedTitles"
:key="item.id" :key="item.id"
:invoice-title="item" :invoice-title="item"
@set-default="onSetDefault" @set-default="onSetDefault"
@edit="onEdit" @edit="onEdit"
@delete="onDelete" @delete="onDelete"
/> />
<p v-if="filteredTitles.length === 0" class="invoice-titles__empty text-secondary"> <p v-if="pagedTitles.length === 0" class="invoice-titles__empty">
暂无发票抬头,点击右上角新增 暂无发票抬头,点击右上角新增
</p> </p>
</div> </div>
<!-- 分页 (1684.93~1881.07,505) 12/400 -->
<nav class="invoice-titles__pager" aria-label="分页">
<button class="invoice-titles__page" :disabled="page <= 1" @click="go(page - 1)">
‹
</button>
<button
v-for="item in pages"
:key="item"
class="invoice-titles__page"
:class="{ 'is-active': item === page }"
@click="go(item)"
>
{{ item }}
</button>
<button class="invoice-titles__page" :disabled="page >= pageCount" @click="go(page + 1)">
›
</button>
</nav>
</div>
<InvoiceTitleDialog <InvoiceTitleDialog
v-model:visible="dialogVisible" v-model:visible="dialogVisible"
:invoice-title="editing" :invoice-title="editing"
@@ -144,107 +151,120 @@ onMounted(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 35/36 发票抬头管理(1920 画布 1:1)——内容区 x70→1850 = 1780
.invoice-titles { .invoice-titles {
.flex(column, flex-start);
align-items: stretch;
gap: @spacing-base;
width: 100%; width: 100%;
max-width: @container-width; padding-bottom: 459px; // 分页底 521 → 页脚 1087
margin: 0 auto; background: @contract-page-bg;
padding: @spacing-lg @spacing-base @spacing-xxl;
// 设计 35/36:面包屑 13 设计px → 11px,前缀灰、当前页深灰 // 面包屑 (70,124)
&__breadcrumb { &__breadcrumb {
font-size: 11px; .flex(row, flex-start, center);
color: @text-color-disabled; height: 81px; // 顶栏 72 → 标题条 153,面包屑盒 72..133
padding-top: 36px;
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-muted;
box-sizing: border-box;
}
> span:last-child { &__breadcrumb-link {
color: @text-color; color: @project-muted;
&:hover {
color: @primary-strong;
} }
} }
// 顶部「发票管理」白卡条(设计 35:蓝竖条标题 + 右侧按钮组) &__breadcrumb-current {
margin-left: 5.25px; // 「财务中心 /」墨迹宽 65.25 → 发票抬头管理 x135.25
color: @finance-entry-text;
font-weight: 500;
}
// 标题条 (70,153) 1780×62
&__head { &__head {
.flex(row, space-between, center); .flex(row, space-between, center);
flex-wrap: wrap; height: 62px;
gap: @spacing-base; padding: 0 33.56px 0 36px; // 标题 x106;按钮右缘 1822
padding: 14px 20px; border: 1px solid @finance-card-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
} }
&__title { &__title {
position: relative; font-size: 15px;
padding-left: 10px;
font-size: 12px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 20px;
color: @finance-ink;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 4px;
height: 14px;
border-radius: @radius-round;
background: @primary-strong;
transform: translateY(-50%);
}
} }
&__filters { // 「+ 新增抬头」(1735.56,167) 110.44×34
.flex(row, flex-end, center); &__create {
gap: @spacing-sm; width: 110.44px;
} height: 34px;
margin-right: -5px; // 按钮右缘 1822 → 卡内缩 33.56 得 1816.44,再右移 5
&__filter { border: none;
height: 26px; border-radius: 4px;
padding: 0 14px;
border: 1px solid @border-color;
border-radius: @radius-sm;
background: @white;
color: @text-color;
font-size: 11px;
transition: all 0.2s;
&:hover {
border-color: @primary-strong;
color: @primary-strong;
}
&.is-active {
border-color: @primary-strong;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
} font-size: 12px;
} font-weight: 500;
transition: background 0.2s;
// 「+ 新增抬头」:设计 35 实底 #024FFF 小按钮
:deep(.app-button--primary) {
height: 26px;
padding: 0 12px;
border-radius: @radius-sm;
background: @primary-strong;
font-size: 11px;
&:hover { &:hover {
background: @primary-color; background: @primary-color;
} }
} }
// 抬头卡纵排铺满(设计 35 一行一张) // 抬头卡纵排:首卡 (70,229)、次卡 (70,372) → 间距 14
&__grid { &__list {
display: flex; .flex(column, flex-start, stretch);
flex-direction: column; gap: 14px;
gap: @spacing-base; margin-top: 14px; // 标题条底 215 → 首卡 229
} }
&__empty { &__empty {
padding: @spacing-xl 0; padding: 60px 0;
font-size: 11px; color: @project-muted;
font-size: 13px;
text-align: center; text-align: center;
} }
// 分页:右对齐到内容区右缘,页码 12/400
&__pager {
.flex(row, flex-end, center);
gap: 3.7px;
margin-top: 8px; // 次卡底 460 → 页码 ink 505
padding-right: 2.4px; // 末位「›」右缘 1881.07 → 内容右缘 1850
}
&__page {
min-width: 21.45px;
height: 16px;
padding: 0 3px;
border: none;
border-radius: 0;
background: transparent;
color: @finance-muted-text;
font-size: 12px;
font-weight: 400;
transition: color 0.2s;
&:hover:not(:disabled) {
color: @primary-strong;
}
&.is-active {
color: @primary-strong;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
} }
</style> </style>

View File

@@ -2,7 +2,6 @@
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { AppButton } from '@/components'
// 页面容器:数据只经 store,不直接 import api // 页面容器:数据只经 store,不直接 import api
import { useFinanceStore } from '@/stores/modules/finance' import { useFinanceStore } from '@/stores/modules/finance'
import { formatMoney } from '@/utils/format' import { formatMoney } from '@/utils/format'
@@ -32,52 +31,73 @@ onMounted(() => {
<template> <template>
<div class="pay-result"> <div class="pay-result">
<nav class="pay-result__breadcrumb text-secondary"> <div class="container">
<RouterLink to="/finance" class="link"> <!-- 面包屑 (67,95)「财务中心 / 账单明细 /」+「支付成功」13/500 #4B5563 -->
财务中心 <nav class="pay-result__breadcrumb">
<RouterLink to="/finance" class="pay-result__crumb-link">
财务中心 /
</RouterLink> </RouterLink>
<span> &gt; </span> <RouterLink to="/finance/bills" class="pay-result__crumb-link">
<RouterLink to="/finance/bills" class="link"> 账单明细 /
账单明细
</RouterLink> </RouterLink>
<span> &gt; </span> <span class="pay-result__crumb-current">支付成功</span>
<span>支付结果</span>
</nav> </nav>
<template v-if="result"> <template v-if="result">
<section class="pay-result__card card"> <!-- 主列 316 居中(设计 x799→1115) -->
<div class="pay-result__head"> <section class="pay-result__col">
<span class="pay-result__check">✓</span> <!-- 绿圆勾 (897,187) 120×120 #7ED321,✓ 56/400 白字居中 -->
<div class="pay-result__head-text"> <div class="pay-result__check" aria-hidden="true">
✓
</div>
<!-- 标题 (900.3,331) 22/700 #1F2937,字距 6 -->
<h2 class="pay-result__title"> <h2 class="pay-result__title">
支付成功 支付成功
</h2> </h2>
<p class="pay-result__count text-secondary">
共{{ result.bills.length }}个订单
</p>
<p class="pay-result__amount">
付款金额 ¥{{ formatMoney(result.amount) }}
</p>
</div>
</div>
<div class="pay-result__grid"> <!-- 金额行 (852.9,379):13/400 灰 + ¥ 18/700 #EF4444,基线对齐 -->
<div class="pay-result__grid-item"> <p class="pay-result__amount-row">
<span class="pay-result__grid-label text-secondary">支付方式</span> <span class="pay-result__amount-label">共{{ result.bills.length }}个订单 支付金额:</span>
<span class="pay-result__grid-value">{{ methodLabel }}</span> <span class="pay-result__amount">¥ {{ formatMoney(result.amount, 0) }}</span>
</div> </p>
<div class="pay-result__grid-item">
<span class="pay-result__grid-label text-secondary">支付时间</span>
<span class="pay-result__grid-value">{{ result.payTime }}</span>
</div>
<div class="pay-result__grid-item">
<span class="pay-result__grid-label text-secondary">交易流水号</span>
<span class="pay-result__grid-value">{{ result.tradeNo }}</span>
</div>
</div>
<div class="pay-result__bills"> <!-- 分割线 (799,428) 316×1 #F0F1F3 -->
<div class="pay-result__divider" />
<!-- 支付信息 (799,449) 13/700 -->
<h4 class="pay-result__sub-title"> <h4 class="pay-result__sub-title">
支付信息
</h4>
<dl class="pay-result__fields">
<div class="pay-result__field">
<dt class="pay-result__label">
支付方式
</dt>
<dd class="pay-result__value">
{{ methodLabel }}
</dd>
</div>
<div class="pay-result__field">
<dt class="pay-result__label">
支付时间
</dt>
<dd class="pay-result__value">
{{ result.payTime }}
</dd>
</div>
<div class="pay-result__field">
<dt class="pay-result__label">
交易流水号
</dt>
<dd class="pay-result__value">
{{ result.tradeNo || '—' }}
</dd>
</div>
</dl>
<!-- 已支付订单 (799,574) 13/700 -->
<h4 class="pay-result__sub-title pay-result__sub-title--bills">
已支付订单 已支付订单
</h4> </h4>
<ul class="pay-result__bill-list"> <ul class="pay-result__bill-list">
@@ -86,184 +106,240 @@ onMounted(() => {
:key="bill.id" :key="bill.id"
class="pay-result__bill" class="pay-result__bill"
> >
<div class="pay-result__bill-head">
<span class="pay-result__bill-name">{{ bill.planName }}</span> <span class="pay-result__bill-name">{{ bill.planName }}</span>
<span class="pay-result__bill-no text-secondary">{{ bill.billNo }}</span> <span class="pay-result__bill-amount">¥ {{ formatMoney(bill.amount, 0) }}</span>
<span class="pay-result__bill-amount">¥{{ formatMoney(bill.amount) }}</span> </div>
<p class="pay-result__bill-meta">
账单号:{{ bill.billNo }}
</p>
<p class="pay-result__bill-meta">
关联项目:{{ bill.projectName }}
</p>
</li> </li>
</ul> </ul>
</div>
<!-- 返回 (1031,711) 84×32 #024FFF 12/500 白字,右对齐 -->
<div class="pay-result__footer"> <div class="pay-result__footer">
<AppButton type="primary" @click="router.push('/finance/bills')"> <button
class="pay-result__back"
type="button"
@click="router.push('/finance/bills')"
>
返回 返回
</AppButton> </button>
</div> </div>
</section> </section>
</template> </template>
<p v-else class="pay-result__empty text-secondary"> <p v-else class="pay-result__empty">
暂无支付结果 暂无支付结果
</p> </p>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计「支付成功」c56193d1(1920 画布 1:1)——316 居中窄列坐在页面底色上
.pay-result { .pay-result {
width: 100%; width: 100%;
max-width: @container-width; padding-bottom: 344px; // 列底 743 → 页脚 1087
margin: 0 auto; background: @contract-page-bg;
padding: @spacing-lg @spacing-base @spacing-xxl;
// 设计 32:面包屑 13 设计px → 11px,前缀灰、当前页深灰 // 面包屑 (67,95):头部底 73 → 文字盒顶 95 = 22
&__breadcrumb { &__breadcrumb {
margin-bottom: @spacing-base; .flex(row, flex-start, center);
font-size: 11px; padding-top: 22px;
color: @text-color-disabled; font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-muted;
}
> span:last-child { &__crumb-link {
color: @text-color; color: @project-muted;
&:hover {
color: @primary-strong;
} }
} }
// 设计 32:内容为居中窄列,直接坐在页面底色上(无外层大卡) &__crumb-current {
&__card { margin-left: 3px;
width: 300px; color: @finance-entry-text; // 设计 #4B5563
max-width: 100%; font-weight: 500;
margin: 0 auto;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
} }
// 头部:大绿圆勾 + 标题 + 「共 N 个订单 / 付款金额」同一行 // 主列 316,居中(面包屑底 112 → 圆顶 187 = 75)
&__head { &__col {
.flex(column, center, center); width: 316px;
gap: 12px; margin: 75px auto 0;
padding-bottom: 16px;
border-bottom: 1px solid @project-divider;
} }
// 绿圆勾 120×120 #7ED321
&__check { &__check {
.flex(row, center); .flex(row, center, center);
width: 76px; width: 120px;
height: 76px; height: 120px;
border-radius: @radius-round; margin: 0 auto;
border-radius: 50%;
background: @finance-result-circle; background: @finance-result-circle;
color: @white; color: @white;
font-size: 42px; font-size: 56px;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
flex-shrink: 0;
}
&__head-text {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: 6px;
} }
// 标题:圆底 307 → 331 = 24;盒 113.3 ≈ 4×22 + 4×6 字距
&__title { &__title {
flex-basis: 100%; margin: 24px -6px 0 0;
text-align: center; text-align: center;
font-size: 17px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 29px;
letter-spacing: 6px;
color: @finance-ink;
}
// 金额行:标题底 360 → 374 = 14;13/400 灰 + 18/700 #EF4444 基线对齐
&__amount-row {
.flex(row, center, baseline);
margin-top: 14px;
}
&__amount-label {
font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @finance-muted-text;
} }
&__amount { &__amount {
font-size: 14px; font-size: 18px;
color: @danger-color;
font-weight: 700; font-weight: 700;
line-height: 24px;
color: @danger-color; // 设计 #EF4444
white-space: nowrap;
} }
&__count { // 分割线:金额行底 398 → 428 = 30
font-size: 11px; &__divider {
height: 1px;
margin-top: 30px;
background: @project-divider;
} }
// 支付信息:标签左灰、值右粗(设计 32 竖排行) &__sub-title {
&__grid { margin-top: 20px; // 线底 429 → 449 = 20
font-size: 13px;
font-weight: 700;
line-height: 17px;
color: @finance-ink;
&--bills {
margin-top: 22px; // 末行底 552 → 574 = 22
}
}
// 支付信息三行:标题底 466 → 首行 480 = 14;行高 16 步进 28 → gap 12
&__fields {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 14px; gap: 12px;
margin-top: 16px; margin-top: 14px;
} }
&__grid-item { &__field {
.flex(row, space-between, center); .flex(row, space-between, flex-start);
gap: @spacing-base;
} }
&__grid-label { &__label {
font-size: 11px; font-size: 12px;
color: @text-color-secondary; font-weight: 400;
line-height: 16px;
color: @finance-muted-text;
} }
&__grid-value { &__value {
font-size: 11px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 16px;
color: @finance-ink;
text-align: right; text-align: right;
word-break: break-all; word-break: break-all;
} }
&__bills { // 白卡:标题底 591 → 601 = 10;316×84 白底描边 #F0F1F3
margin-top: 16px;
}
&__sub-title {
margin-bottom: 8px;
font-size: 11px;
font-weight: 700;
color: @heading-color;
}
// 已支付订单行卡:白底圆角(设计 32 实取)
&__bill-list { &__bill-list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 6px; margin-top: 10px;
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin-bottom: 0;
} }
&__bill { &__bill {
.flex(row, flex-start, center); box-sizing: border-box;
gap: 8px; height: 84px;
padding: 10px; padding: 15px 17px;
border-radius: 6px; border: 1px solid @project-divider;
border-radius: @radius-base;
background: @white; background: @white;
font-size: 11px; overflow: hidden;
color: @text-color; }
&__bill-head {
.flex(row, space-between, baseline);
} }
&__bill-name { &__bill-name {
color: @heading-color; font-size: 12px;
font-weight: 700; font-weight: 700;
} line-height: 16px;
color: @finance-ink;
&__bill-no { .ellipsis();
flex: 1; min-width: 0;
font-size: 11px;
color: @text-color-disabled;
} }
&__bill-amount { &__bill-amount {
flex-shrink: 0;
margin-left: 12px;
font-size: 14px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 16px;
white-space: nowrap; color: @finance-ink;
} }
// 返回按钮右对齐(设计 32 在列右下) &__bill-meta {
margin-top: 6px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: @project-muted; // 设计 #9CA3AF
.ellipsis();
+ .pay-result__bill-meta {
margin-top: 0; // 账单号 638 → 关联项目 654 连排
}
}
// 返回按钮:卡底 685 → 711 = 26,右对齐 84×32 #024FFF
&__footer { &__footer {
display: flex; .flex(row, flex-end, flex-start);
justify-content: flex-end; margin-top: 26px;
margin-top: 16px;
} }
:deep(.app-button--primary) { &__back {
width: 84px;
height: 32px;
border: none;
border-radius: 6px;
background: @primary-strong; background: @primary-strong;
color: @white;
font-size: 12px;
font-weight: 500;
line-height: 16px;
cursor: pointer;
transition: background 0.2s;
&:hover { &:hover {
background: @primary-color; background: @primary-color;
@@ -271,9 +347,10 @@ onMounted(() => {
} }
&__empty { &__empty {
padding: @spacing-xl 0; padding: 80px 0;
text-align: center; text-align: center;
font-size: 11px; font-size: 13px;
color: @project-muted;
} }
} }
</style> </style>

View File

@@ -1,161 +1,157 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRouter } from 'vue-router'
import type { BannerVo } from '@/types/business/home' import bannerIllustration from '@/assets/home/banner-illustration.png'
interface Props { // 设计稿顶部为静态头图(非轮播、不走接口):底 #F3F4F8 渐变,
banners: BannerVo[] // 左侧标题 60px + 副标题 24px + 双按钮 183×55,右侧插画 723×598(蓝湖设计稿切图)
const router = useRouter()
function goDemand(): void {
router.push('/demand')
} }
const props = defineProps<Props>() function goCases(): void {
router.push('/cases')
/** 仅保留启用且带图的轮播项,按后端 sort 升序 */
const slides = computed(() =>
props.banners
.filter((item) => item.enabled !== false && !!item.imageUrl)
.slice()
.sort((prev, next) => (prev.sort ?? 0) - (next.sort ?? 0)),
)
const current = ref(0)
let timer: number | undefined
function stop(): void {
if (timer !== undefined) {
window.clearInterval(timer)
timer = undefined
}
} }
function go(index: number): void {
const total = slides.value.length
if (total === 0) return
current.value = (index + total) % total
}
function start(): void {
stop()
if (slides.value.length <= 1) return
timer = window.setInterval(() => go(current.value + 1), 5000)
}
watch(
() => slides.value.length,
() => {
current.value = 0
start()
},
)
onMounted(start)
onBeforeUnmount(stop)
</script> </script>
<template> <template>
<section v-if="slides.length" class="home-banner"> <section class="home-banner">
<img <div class="home-banner__inner">
v-for="(slide, index) in slides" <h1 class="home-banner__title">虫子智造研发一体化平台</h1>
v-show="index === current" <p class="home-banner__subtitle">一站式智能软硬件研发与量产综合服务商</p>
:key="slide.id ?? index"
class="home-banner__img"
:src="slide.imageUrl ?? ''"
:alt="`轮播图 ${index + 1}`"
>
<template v-if="slides.length > 1"> <div class="home-banner__actions">
<button <button class="home-banner__btn home-banner__btn--primary" type="button" @click="goDemand">
class="home-banner__arrow home-banner__arrow--prev" 需求提报
type="button"
aria-label="上一张"
@click="go(current - 1)"
>
‹
</button> </button>
<button <button class="home-banner__btn home-banner__btn--ghost" type="button" @click="goCases">
class="home-banner__arrow home-banner__arrow--next" 案例中心
type="button"
aria-label="下一张"
@click="go(current + 1)"
>
›
</button> </button>
<!-- 设计稿底部居中的指示条(长条=当前,圆点=其余) -->
<div class="home-banner__dots">
<span
v-for="(slide, index) in slides"
:key="`dot-${slide.id ?? index}`"
class="home-banner__dot"
:class="{ 'is-active': index === current }"
/>
</div> </div>
</template>
<div class="home-banner__visual">
<img
class="home-banner__illustration"
:src="bannerIllustration"
alt="虫子智造研发一体化平台"
>
</div>
</div>
</section> </section>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.home-banner { .home-banner {
// 设计稿头图通栏 1920×661(画板 y73,位于顶栏 72 之下)
position: relative; position: relative;
// 设计稿头图通栏 1920×661 设计px → 高 509 CSS,紧贴顶栏无圆角 height: 661px;
height: 509px;
overflow: hidden; overflow: hidden;
background: @home-banner-bg; background: @home-banner-bg;
// .home 是 column flex 容器,需显式占满整行,否则被内容宽度收缩
width: 100%;
flex: 0 0 auto;
&__img { // 内容区按设计稿 1920 画布居中;两侧内容一律相对它定位,保证 1:1
&__inner {
position: relative;
width: 100%;
max-width: @container-width;
height: 100%;
margin: 0 auto;
}
// 设计稿标题 x150 y257(画板绝对)→ banner 内 y257−73=184
&__title {
position: absolute;
top: 184px;
left: 150px;
margin: 0;
color: @banner-title-color;
font-size: @font-size-hero-lg;
font-weight: 700;
line-height: 87px;
white-space: nowrap;
}
// 设计稿副标题 x150 y360 → banner 内 y287
&__subtitle {
position: absolute;
top: 287px;
left: 150px;
margin: 0;
color: @banner-title-color;
font-size: 24px;
font-weight: 500;
line-height: 35px;
white-space: nowrap;
}
// 设计稿按钮组 x150 y465 → banner 内 y392;两钮 183×55,间距 20(150+183=333 → 353)
&__actions {
position: absolute;
top: 392px;
left: 150px;
.flex(row, flex-start, center);
gap: 20px;
}
&__btn {
width: 183px;
height: 55px;
border-radius: 8px;
font-size: 18px;
font-weight: 500;
line-height: 1;
cursor: pointer;
&--primary {
background: linear-gradient(135deg, @btn-gradient-from 0%, @btn-gradient-to 100%);
border: none;
color: @white;
}
// 设计稿:2px 渐变描边 + 白底 + 蓝色文字(实测 #2B5FF0)
&--ghost {
position: relative;
background: @white;
border: none;
color: @btn-ghost-color;
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 8px;
padding: 2px;
background: linear-gradient(135deg, @btn-gradient-from 0%, @btn-gradient-to 100%);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
}
}
// 设计稿插画 723×598 @ canvas (928,104) → banner 内 y104−73=31
&__visual {
position: absolute;
top: 31px;
left: 928px;
width: 723px;
height: 598px;
}
&__illustration {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: contain;
}
&__arrow {
position: absolute;
top: 50%;
width: 32px;
height: 32px;
transform: translateY(-50%);
border-radius: @radius-round;
background: fade(@black, 35%);
color: @white;
font-size: @font-size-xl;
line-height: 1;
opacity: 0;
transition: opacity 0.2s;
&--prev {
left: @spacing-base;
}
&--next {
right: @spacing-base;
}
}
&:hover &__arrow {
opacity: 1;
}
&__dots {
position: absolute;
bottom: 48px;
left: 50%;
transform: translateX(-50%);
.flex(row, center, center);
gap: 10px;
}
&__dot {
width: 4px;
height: 4px;
border-radius: @radius-round;
background: @home-banner-dot;
transition: width 0.2s;
// 当前页:设计稿 44×5 设计px 长条
&.is-active {
width: 34px;
height: 4px;
border-radius: @radius-round;
}
} }
} }
</style> </style>

View File

@@ -1,21 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BusinessType } from '@/types/api'
import type { BusinessLine } from '@/types/business/home' import type { BusinessLine } from '@/types/business/home'
import illInternet from '@/assets/home/business-internet.png'
import illHardware from '@/assets/home/business-hardware.png'
import illIot from '@/assets/home/business-iot.png'
import illManufacturing from '@/assets/home/business-manufacturing.png'
interface Props { interface Props {
lines: BusinessLine[] lines: BusinessLine[]
} }
defineProps<Props>() defineProps<Props>()
// 设计稿四张卡的插画图案(81×111,蓝湖整稿切图);接口 logoUrl 优先
const FALLBACK_ILL: Record<BusinessType, string> = {
internet: illInternet,
hardware: illHardware,
iot: illIot,
manufacturing: illManufacturing,
other: illIot,
}
</script> </script>
<template> <template>
<section class="home-business"> <section class="home-business">
<div class="home-business__head">
<h3 class="home-business__title"> <h3 class="home-business__title">
业务服务 业务服务
</h3> </h3>
<span class="home-business__slogan text-secondary">研发智造一体化自营平台,全链路质量兜底</span>
</div>
<div class="home-business__grid"> <div class="home-business__grid">
<RouterLink <RouterLink
@@ -25,21 +36,20 @@ defineProps<Props>()
class="home-business__card" class="home-business__card"
:class="`home-business__card--${line.id}`" :class="`home-business__card--${line.id}`"
> >
<div class="home-business__card-head"> <!-- 设计稿:插画 81×111 @卡内(48,4);接口 logoUrl 优先,静态切图兜底 -->
<span class="home-business__icon"> <img
<!-- TODO(蓝湖): 替换业务图标切图 --> class="home-business__ill"
{{ line.name.slice(0, 2) }} :src="line.icon || FALLBACK_ILL[line.id]"
</span> :alt="line.name"
<!-- 设计稿卡片右上角选择圆环 --> >
<!-- 设计稿卡片右上角选择圆环(31×31) -->
<span class="home-business__check" aria-hidden="true" /> <span class="home-business__check" aria-hidden="true" />
</div>
<h4 class="home-business__name"> <h4 class="home-business__name">
{{ line.name }} {{ line.name }}
</h4> </h4>
<p class="home-business__desc"> <p class="home-business__desc">
{{ line.desc }} {{ line.desc }}
</p> </p>
<span class="home-business__more">了解详情 ></span>
</RouterLink> </RouterLink>
</div> </div>
</section> </section>
@@ -47,43 +57,35 @@ defineProps<Props>()
<style lang="less" scoped> <style lang="less" scoped>
.home-business { .home-business {
// 设计稿区块卡 1680×400 设计px:白底 + #E2E2E2 描边(内容区内左右缩 50 设计px) // 设计稿区块卡 1680×400:白底 + #E2E2E2 描边,标题 @190,1062(卡顶 +44),卡区左右距 70
max-width: 1294px; padding: 44px 70px 54px;
margin-right: auto;
margin-left: auto;
padding: 34px 54px 40px;
border: 1px solid @home-section-border; border: 1px solid @home-section-border;
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
&__head {
.flex(row, space-between);
margin-bottom: 36px;
}
&__title { &__title {
// 设计稿栏目标题 36 设计px / 700 / #121212 margin: 0 0 47px; // 标题盒底 1114 → 卡顶 1161
// 设计稿栏目标题 36px / 700 / #121212
font-size: @font-size-xxl; font-size: @font-size-xxl;
font-weight: 700; font-weight: 700;
color: @home-ink; color: @home-ink;
} }
&__slogan {
font-size: @font-size-sm;
}
&__grid { &__grid {
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr); // 一行固定四张,窄视口行内等比收缩(wrap 会因 1px 之差把第四张挤到第二行)
// 设计稿卡片间距 22 设计px ≈ 17 CSS gap: 22px;
gap: 17px;
} }
&__card { &__card {
.flex(column, flex-start); position: relative;
gap: 0; .flex(column, flex-start, flex-start);
// 设计稿业务卡 368×206 设计px:白底 / #999 描边 / 小圆角 // 设计稿业务卡 368×206,一行固定四张(4×368 + 3×22 = 1538 ≤ 1560)
padding: 22px 16px 6px; flex: 0 1 368px;
min-width: 0;
height: 206px;
box-sizing: border-box;
overflow: hidden;
border: 1px solid @home-business-border; border: 1px solid @home-business-border;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
@@ -95,29 +97,24 @@ defineProps<Props>()
} }
} }
&__card-head { // 设计稿插画 81×111 @卡内 (48,4)
.flex(row, space-between, flex-start); &__ill {
width: 100%; position: absolute;
top: 4px;
left: 48px;
width: 81px;
height: 111px;
object-fit: contain;
} }
&__icon { // 右上角选择圆环(31×31)
.flex(row, center);
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: @radius-base;
background: transparent;
color: @primary-strong;
font-size: @font-size-lg;
font-weight: 600;
}
// 右上角选择圆环(31 设计px ≈ 24 CSS)
&__check { &__check {
position: relative; position: absolute;
top: 24px;
right: 20px;
flex-shrink: 0; flex-shrink: 0;
width: 24px; width: 31px;
height: 24px; height: 31px;
border: 1px solid @home-ring; border: 1px solid @home-ring;
border-radius: @radius-round; border-radius: @radius-round;
@@ -125,8 +122,8 @@ defineProps<Props>()
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 14px; width: 18px;
height: 14px; height: 18px;
margin: auto; margin: auto;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
@@ -134,28 +131,23 @@ defineProps<Props>()
} }
} }
// 设计稿标题 @卡内 (20,106),描述 @ (20,151)
&__name { &__name {
margin-top: 16px; margin: 106px 0 0;
// 设计稿卡片标题 28 设计px / 500 / #121212 // 设计稿卡片标题 28px / 500 / #121212
font-size: @font-size-title; font-size: @font-size-title;
font-weight: 500; font-weight: 500;
line-height: 41px;
color: @home-ink; color: @home-ink;
} }
&__desc { &__desc {
margin-top: 4px; margin: 4px 0 0;
font-size: @font-size-sm; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
line-height: 23px;
color: @home-ink; color: @home-ink;
.multi-ellipsis(2); .multi-ellipsis(1);
}
&__more {
align-self: flex-end;
margin-top: auto;
font-size: @font-size-sm;
color: @text-color-secondary;
transition: color 0.2s;
} }
// 设计稿:首张「互联网软件」为 #024FFF 选中实底 + 白字白环;其余白底描边 // 设计稿:首张「互联网软件」为 #024FFF 选中实底 + 白字白环;其余白底描边
@@ -163,10 +155,6 @@ defineProps<Props>()
border-color: @primary-strong; border-color: @primary-strong;
background: @primary-strong; background: @primary-strong;
.home-business__icon {
color: @white;
}
.home-business__check { .home-business__check {
border-color: @white; border-color: @white;
@@ -176,28 +164,9 @@ defineProps<Props>()
} }
.home-business__name, .home-business__name,
.home-business__desc, .home-business__desc {
.home-business__more {
color: @white; color: @white;
} }
} }
&__card--hardware {
.home-business__icon {
color: @tag-hardware;
}
}
&__card--iot {
.home-business__icon {
color: @tag-iot;
}
}
&__card--manufacturing {
.home-business__icon {
color: @tag-manufacturing;
}
}
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { ref } from 'vue' import { ref } from 'vue'
import { formatBusinessType } from '@/utils/format'
import type { BusinessType } from '@/types/api' import type { BusinessType } from '@/types/api'
import type { Case } from '@/types/business/cases' import type { Case } from '@/types/business/cases'
@@ -8,7 +8,7 @@ interface Props {
cases: Case[] cases: Case[]
} }
defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ categoryChange: [category: BusinessType | 'all'] }>() const emit = defineEmits<{ categoryChange: [category: BusinessType | 'all'] }>()
@@ -26,6 +26,9 @@ function switchCategory(value: BusinessType | 'all'): void {
activeCategory.value = value activeCategory.value = value
emit('categoryChange', value) emit('categoryChange', value)
} }
// 设计稿一行固定四张 368×248,最多展示四张
const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
</script> </script>
<template> <template>
@@ -52,18 +55,14 @@ function switchCategory(value: BusinessType | 'all'): void {
<div class="home-cases__grid"> <div class="home-cases__grid">
<article <article
v-for="item in cases" v-for="item in visibleCases"
:key="item.id" :key="item.id"
class="home-cases__card" class="home-cases__card"
@click="$router.push(`/cases/${item.id}`)" @click="$router.push(`/cases/${item.id}`)"
> >
<div class="home-cases__cover"> <div class="home-cases__cover">
<!-- TODO(蓝湖): 替换案例封面切图 -->
<img v-if="item.cover" v-lazy="item.cover" class="home-cases__img" :alt="item.title"> <img v-if="item.cover" v-lazy="item.cover" class="home-cases__img" :alt="item.title">
<div v-else class="home-cases__img home-cases__img--placeholder" /> <div v-else class="home-cases__img home-cases__img--placeholder" />
<span class="home-cases__tag" :class="`home-cases__tag--${item.category}`">
{{ formatBusinessType(item.category) }}
</span>
</div> </div>
<div class="home-cases__body"> <div class="home-cases__body">
@@ -71,12 +70,14 @@ function switchCategory(value: BusinessType | 'all'): void {
{{ item.title }} {{ item.title }}
</h4> </h4>
<ul class="home-cases__meta"> <ul class="home-cases__meta">
<li>客户对象:{{ item.clientType }}</li> <li>客户类型:{{ item.clientType }}</li>
<li>研发周期:{{ item.duration }}</li> <li>所属行业:{{ item.industry }}</li>
<li>团队规模:{{ item.teamSize }}人</li> <li>规模:{{ item.scale }}</li>
<li>项目周期:{{ item.duration }}</li>
<li>服务团队:{{ item.teamSize }}人</li>
<li>完成时间:{{ item.finishDate }}</li> <li>完成时间:{{ item.finishDate }}</li>
</ul> </ul>
<span class="home-cases__view">查看详情</span> <span class="home-cases__view">查看详情&gt;</span>
</div> </div>
</article> </article>
</div> </div>
@@ -85,23 +86,20 @@ function switchCategory(value: BusinessType | 'all'): void {
<style lang="less" scoped> <style lang="less" scoped>
.home-cases { .home-cases {
// 设计稿区块卡 1680×446 设计px:白底 + #E2E2E2 描边(内容区内左右缩 50 设计px) // 设计稿区块卡 1680×446:白底 + #E2E2E2 描边,标题 @1478(卡顶 +40),卡区左右距 66
max-width: 1294px; padding: 40px 66px 60px;
margin-right: auto;
margin-left: auto;
padding: 32px 54px 44px;
border: 1px solid @home-section-border; border: 1px solid @home-section-border;
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
&__head { &__head {
.flex(row, flex-start); .flex(row, flex-start);
gap: 20px; gap: 24px;
margin-bottom: 36px; margin-bottom: 47px; // 标题盒底 1530 → 卡顶 1577
} }
&__title { &__title {
// 设计稿栏目标题 36 设计px / 700 / #121212 // 设计稿栏目标题 36px / 700 / #121212
font-size: @font-size-xxl; font-size: @font-size-xxl;
font-weight: 700; font-weight: 700;
color: @home-ink; color: @home-ink;
@@ -109,8 +107,8 @@ function switchCategory(value: BusinessType | 'all'): void {
&__tabs { &__tabs {
.flex(row, center); .flex(row, center);
// 设计稿分类紧跟标题左侧排布(间距 36 设计px ≈ 28) // 设计稿分类紧跟标题左侧排布(间距 36px)
gap: 28px; gap: 36px;
} }
&__tab { &__tab {
@@ -119,9 +117,9 @@ function switchCategory(value: BusinessType | 'all'): void {
padding: 0; padding: 0;
border: 0; border: 0;
background: transparent; background: transparent;
// 设计稿分类 16 设计px / 500 / #121212(激活 #085DFF + 下划线) // 设计稿分类 16px / 500 / #121212(激活 #085DFF + 下划线)
color: @home-ink; color: @home-ink;
font-size: @font-size-sm; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
transition: color 0.2s; transition: color 0.2s;
@@ -129,12 +127,12 @@ function switchCategory(value: BusinessType | 'all'): void {
background: transparent; background: transparent;
color: @home-tab-active; color: @home-tab-active;
// 设计稿激活下划线 29×2 设计px(#1F67EF) // 设计稿激活下划线 29×2(#1F67EF)
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
bottom: -6px; bottom: -8px;
left: 0; left: 0;
height: 2px; height: 2px;
background: @primary-color; background: @primary-color;
@@ -144,19 +142,23 @@ function switchCategory(value: BusinessType | 'all'): void {
&__more { &__more {
margin-left: auto; margin-left: auto;
font-size: @font-size-sm; font-size: @font-size-md;
} }
&__grid { &__grid {
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr); // 一行固定四张(4×368 + 3×24 = 1544),窄视口行内等比收缩
// 设计稿案例卡间距 24 设计px ≈ 18 CSS gap: 24px;
gap: 18px;
} }
&__card { &__card {
overflow: hidden; overflow: hidden;
// 设计稿案例卡 368×248 设计px:白底 + #E0DEDE 描边 + 0 4 10 黑 8% 投影 // 设计稿案例卡固定 368×248,一行四张(4×368 + 3×24 = 1544 ≤ 1560),不足四张不放大
flex: 0 1 368px;
min-width: 0;
height: 248px;
box-sizing: border-box;
// 设计稿案例卡 368×248:白底 + #E0DEDE 描边 + 0 4 10 黑 8% 投影
border: 1px solid @home-case-border; border: 1px solid @home-case-border;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
@@ -176,8 +178,8 @@ function switchCategory(value: BusinessType | 'all'): void {
&__cover { &__cover {
position: relative; position: relative;
// 设计稿封面区 128 设计px 高 ≈ 99 CSS // 设计稿封面区 88px 高(卡 248:封面 0-88,标题 @109,属性行 @174/194,查看详情 @220)
height: 100px; height: 88px;
overflow: hidden; overflow: hidden;
} }
@@ -191,53 +193,28 @@ function switchCategory(value: BusinessType | 'all'): void {
} }
} }
&__tag {
position: absolute;
top: @spacing-sm;
left: @spacing-sm;
padding: 2px @spacing-sm;
border-radius: @radius-sm;
background: @primary-color;
color: @white;
font-size: 11px;
&--hardware {
background: @tag-hardware;
}
&--iot {
background: @tag-iot;
}
&--manufacturing {
background: @tag-manufacturing;
}
&--other {
background: @tag-other;
}
}
&__body { &__body {
padding: 12px 15px 6px; padding: 0 20px;
} }
&__name { &__name {
margin-bottom: 8px; margin: 21px 0 0;
// 设计稿案例名 16 设计px / 500 / #333333 // 设计稿案例名 16px / 500 / #333333
font-size: @font-size-sm; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
line-height: 22px;
color: @home-body; color: @home-body;
.multi-ellipsis(1); .multi-ellipsis(1);
} }
&__meta { &__meta {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; // 设计稿属性行 13px 两行三列(行1 @174、行2 @194,行高 20)
// 设计稿元信息 10 设计px → 可读下限 11px / #333333 grid-template-columns: 1fr 1fr 1fr;
gap: 3px @spacing-sm; gap: 0 @spacing-lg;
font-size: 11px; margin-top: 43px;
line-height: 1.5; font-size: 13px;
line-height: 20px;
color: @home-body; color: @home-body;
li { li {
@@ -248,12 +225,14 @@ function switchCategory(value: BusinessType | 'all'): void {
&__view { &__view {
display: block; display: block;
width: fit-content; width: fit-content;
margin: 10px 0 0 auto; // 设计稿「查看详情>」@卡内 y220(13px),右下角
margin: 6px 0 0 auto;
padding: 0; padding: 0;
border: 0; border: 0;
// 设计稿「查看详情>」12 设计px / #024FFF(无按钮描边) // 设计稿「查看详情>」13px / #024FFF(无按钮描边)
color: @primary-strong; color: @primary-strong;
font-size: @font-size-sm; font-size: 13px;
line-height: 15px;
transition: color 0.2s; transition: color 0.2s;
} }
} }

View File

@@ -6,13 +6,19 @@ import { useHomeStore } from '@/stores/modules/home'
import type { PortalEntryCard } from '@/types/business/home' import type { PortalEntryCard } from '@/types/business/home'
import { toPortalEntryCards } from '@/utils/entryIcon' import { toPortalEntryCards } from '@/utils/entryIcon'
import HomeAppDialog from './HomeAppDialog.vue' import HomeAppDialog from './HomeAppDialog.vue'
import tileDemand from '@/assets/home/entry-demand.png'
import tileCases from '@/assets/home/entry-cases.png'
import tileService from '@/assets/home/entry-service.png'
import tileContract from '@/assets/home/entry-contract.png'
import tileFinance from '@/assets/home/entry-finance.png'
/** /**
* 首页「快捷入口」卡片。 * 首页右上「业务服务」入口卡(设计稿标题即「业务服务」,与下方栏位同名)。
* *
* 数据来自 store.quickEntries(← /vermins/portalEntryUser/list 的 groupType=1 项), * 数据来自 store.quickEntries(← /vermins/portalEntryUser/list 的 groupType=1 项),
* 接口不可用(游客 / 无权限)时回退到内置默认入口,保证卡片不空。 * 接口不可用(游客 / 无权限)时回退到内置默认入口,保证卡片不空。
* 「更多应用」打开 HomeAppDialog,可在快捷 / 其他两组之间调整入口。 * 设计稿固定五个入口,接口返回的多余项(如硬件 / 互联 / 其他)不展示。
* 「>>」双箭头打开 HomeAppDialog,可在快捷 / 其他两组之间调整入口。
*/ */
const homeStore = useHomeStore() const homeStore = useHomeStore()
const { quickEntries } = storeToRefs(homeStore) const { quickEntries } = storeToRefs(homeStore)
@@ -20,24 +26,42 @@ const router = useRouter()
const dialogVisible = ref(false) const dialogVisible = ref(false)
/** 接口不可用时的默认入口(与改造前的静态入口保持一致) */ /** 设计稿固定入口名目(设计稿没有的接口入口不展示) */
const DESIGN_NAMES = ['工单需求池', '案例库管理', '客服工作台', '合同台账', '财务台账']
/** 接口不可用时的默认入口(与设计稿五项一致,tone 对应设计稿五色) */
const FALLBACK_QUICK: PortalEntryCard[] = [ const FALLBACK_QUICK: PortalEntryCard[] = [
{ id: 0, label: '财务台账', route: '/finance/bills', groupType: '1', icon: '💰', isImage: false, tone: 'amber' }, { id: 0, label: '工单需求池', route: '/demand', groupType: '1', icon: '', isImage: false, tone: 'cyan' },
{ id: 0, label: '合同台账', route: '/contract', groupType: '1', icon: '📄', isImage: false, tone: 'blue' }, { id: 0, label: '案例库管理', route: '/cases', groupType: '1', icon: '', isImage: false, tone: 'purple' },
{ id: 0, label: '客服工作台', route: '/service', groupType: '1', icon: '💬', isImage: false, tone: 'green' }, { id: 0, label: '客服工作台', route: '/service', groupType: '1', icon: '', isImage: false, tone: 'green' },
{ id: 0, label: '案例库管理', route: '/cases', groupType: '1', icon: '📁', isImage: false, tone: 'purple' }, { id: 0, label: '合同台账', route: '/contract', groupType: '1', icon: '', isImage: false, tone: 'blue' },
{ id: 0, label: '工单需求池', route: '/demand', groupType: '1', icon: '📋', isImage: false, tone: 'cyan' }, { id: 0, label: '财务台账', route: '/finance/bills', groupType: '1', icon: '', isImage: false, tone: 'amber' },
] ]
/** 设计稿切图:蓝湖整稿 97×97 方块(含底色与白色图案),按入口名映射 */
const DESIGN_TILES: Record<string, string> = {
工单需求池: tileDemand,
案例库管理: tileCases,
客服工作台: tileService,
合同台账: tileContract,
财务台账: tileFinance,
}
/** 前端已注册的静态路由(route 无效时不跳转,避免出现无匹配路由) */ /** 前端已注册的静态路由(route 无效时不跳转,避免出现无匹配路由) */
const knownPaths = new Set(router.getRoutes().map((item) => item.path)) const knownPaths = new Set(router.getRoutes().map((item) => item.path))
/** 卡片展示的入口(最多 5 个) */ /** 卡片展示的入口:接口数据按设计稿名目过滤,最多 5 个 */
const entries = computed<PortalEntryCard[]>(() => { const entries = computed<PortalEntryCard[]>(() => {
const list = toPortalEntryCards(quickEntries.value) const list = toPortalEntryCards(quickEntries.value)
.filter((card) => DESIGN_NAMES.includes(card.label))
return (list.length > 0 ? list : FALLBACK_QUICK).slice(0, 5) return (list.length > 0 ? list : FALLBACK_QUICK).slice(0, 5)
}) })
/** 入口图:接口 iconUrl 图案优先,否则设计稿切图 */
function tileSrc(card: PortalEntryCard): string {
return card.isImage && card.icon ? card.icon : (DESIGN_TILES[card.label] ?? '')
}
/** 解析可跳转路径:优先后端 route,其次同名默认入口,最后回首页 */ /** 解析可跳转路径:优先后端 route,其次同名默认入口,最后回首页 */
function resolvePath(card: PortalEntryCard): string { function resolvePath(card: PortalEntryCard): string {
if (card.route && knownPaths.has(card.route)) return card.route if (card.route && knownPaths.has(card.route)) return card.route
@@ -56,9 +80,8 @@ function onMore(): void {
<template> <template>
<section class="home-quick"> <section class="home-quick">
<div class="home-quick__head"> <div class="home-quick__head">
<span class="home-quick__head-icon">⊞</span>
<h3 class="home-quick__title"> <h3 class="home-quick__title">
快捷入口 业务服务
</h3> </h3>
</div> </div>
@@ -69,17 +92,19 @@ function onMore(): void {
:to="resolvePath(entry)" :to="resolvePath(entry)"
class="home-quick__item" class="home-quick__item"
> >
<span class="home-quick__icon" :class="`home-quick__icon--${entry.tone}`"> <!-- 设计稿:97×97 彩色方块 + 白色图案(蓝湖整稿切图),接口 iconUrl 图案优先 -->
<img v-if="entry.isImage" :src="entry.icon" :alt="entry.label" class="home-quick__img"> <img class="home-quick__tile" :src="tileSrc(entry)" :alt="entry.label">
<template v-else>{{ entry.icon }}</template>
</span>
<span class="home-quick__label">{{ entry.label }}</span> <span class="home-quick__label">{{ entry.label }}</span>
</RouterLink> </RouterLink>
</div>
<button class="home-quick__more link" type="button" @click="onMore"> <!-- 设计稿:双箭头图标与入口方块行垂直居中(@y875, 32×32,距末位方块 20) -->
更多应用 > <button class="home-quick__more" type="button" aria-label="更多应用" @click="onMore">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 6 L18 16 L8 26" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<path d="M17 6 L27 16 L17 26" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
</svg>
</button> </button>
</div>
<HomeAppDialog v-model:visible="dialogVisible" /> <HomeAppDialog v-model:visible="dialogVisible" />
</section> </section>
@@ -90,36 +115,35 @@ function onMore(): void {
position: relative; position: relative;
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex-shrink: 0; flex-shrink: 0;
// 设计稿右卡 722×245 设计px → 556 宽;白底 + 浅灰描边 // 设计稿右卡 722 宽;白底 + 浅灰描边
width: 556px; // flex-basis 代替定宽:窄视口允许收缩,避免横向滚动条(1920 下仍为 722)
flex: 0 1 722px;
min-width: 0;
gap: 0; gap: 0;
padding: 14px 31px 21px; // 设计稿:标题盒顶 y772 = 卡顶 +18,方块行顶 y841 = +87,卡高 264
padding: 18px 40px 28px;
border: 1px solid @home-card-border; border: 1px solid @home-card-border;
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
&__head { &__head {
.flex(row, flex-start); .flex(row, flex-start);
gap: @spacing-sm;
}
&__head-icon {
color: @home-ink;
font-size: @font-size-xl;
} }
&__title { &__title {
// 设计稿卡内标题 36 设计px / 700 / #121212 // 设计稿卡内标题 36px / 700 / #121212,盒高 52(y772-824)
font-size: @font-size-xxl; font-size: @font-size-xxl;
font-weight: 700; font-weight: 700;
line-height: 52px;
color: @home-ink; color: @home-ink;
} }
&__list { &__list {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
// 设计稿入口间距 127 设计px(图标 90 + 间隙 37)≈ 70 + 28 // 设计稿入口图标间距(图标 97 + 间隙 30);方块行顶 y841 = 卡顶 +87
gap: 28px; gap: 30px;
margin-top: 16px; margin-top: 17px;
flex-wrap: wrap; // 卡片被收缩时换行,避免卡内溢出
} }
&__item { &__item {
@@ -133,66 +157,41 @@ function onMore(): void {
} }
} }
&__icon { // 设计稿:97×97 彩色方块切图(含底色与白色图案),整图渲染
.flex(row, center); &__tile {
// 设计稿图标块 90×90 设计px ≈ 70 CSS display: block;
width: 70px; width: 97px;
height: 70px; height: 97px;
border-radius: @radius-base; border-radius: @radius-base;
font-size: 28px; object-fit: cover;
// 底色按设计稿采样(tone → 设计稿色)
&--amber {
background: @home-quick-pink;
}
&--blue {
background: @home-quick-peach;
}
&--green {
background: @home-quick-green;
}
&--purple {
background: @home-quick-purple;
}
&--red {
background: fade(@danger-color, 15%);
}
&--cyan {
background: @home-quick-blue;
}
}
&__img {
width: 32px;
height: 32px;
object-fit: contain;
} }
&__label { &__label {
// 设计稿入口文字 18 设计px / 500 / #121212 // 设计稿入口文字 18px / 500 / #121212
font-size: @font-size-base; font-size: @font-size-lg;
font-weight: 500; font-weight: 500;
color: @home-ink; color: @home-ink;
text-align: center; text-align: center;
} }
&__more { &__more {
// 设计稿「更多」箭头位于卡片右侧垂直居中 // 设计稿:» 32×32 @卡内 (665,121),与方块行(顶 y+87,高 97)垂直居中 → top 87+48.5-16 = 119.5
// 绝对定位脱离 flex 流:卡收缩(窄视口)时不参与折行
position: absolute; position: absolute;
top: 50%; top: 120px;
right: 31px; right: 25px;
width: 32px;
height: 32px;
padding: 0; padding: 0;
border: 0; border: 0;
transform: translateY(-50%);
background: transparent; background: transparent;
font-size: @font-size-sm; color: #c9cdd4;
color: @home-ring;
cursor: pointer; cursor: pointer;
transition: color 0.2s;
&:hover {
color: @primary-color;
}
} }
} }
</style> </style>

View File

@@ -29,11 +29,13 @@ defineProps<Props>()
<style lang="less" scoped> <style lang="less" scoped>
.home-contents { .home-contents {
&__block { &__block {
// 设计稿区块卡 1780×593 设计px:白底、无描边投影 // 设计稿区块卡 矩形 148286 @(70,736) 1780×593:白底、无描边投影
margin-bottom: @spacing-base; margin-bottom: @spacing-base;
padding: 24px 32px 32px; padding: 40px 45px 0; // 正文 @(115,1090) → 卡内 x45(112−70=42 标题左,正文 115−70=45)
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
min-height: 593px;
box-sizing: border-box;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -41,17 +43,18 @@ defineProps<Props>()
} }
&__title { &__title {
// 设计稿栏目标题 36 设计px / 700 / #121212(无竖条) // 设计稿栏目标题 36px / 700 / #121212
margin-bottom: 36px; margin: 0 0 301px; // 正文顶 1090 − 标题底(736+40+52) ≈ 262 → 取 261
font-size: @font-size-xxl; font-size: 36px;
font-weight: 700; font-weight: 700;
line-height: 52px;
color: @home-ink; color: @home-ink;
} }
&__rich { &__rich {
// 设计稿正文 16 设计px / #222222 // 设计稿正文 @(115,1090) 1688×177 16px / #222222
font-size: @font-size-sm; font-size: 16px;
line-height: 1.8; line-height: 1.75;
color: @home-paragraph; color: @home-paragraph;
word-break: break-word; word-break: break-word;
@@ -83,7 +86,7 @@ defineProps<Props>()
:deep(h3), :deep(h3),
:deep(h4) { :deep(h4) {
margin: @spacing-base 0 @spacing-sm; margin: @spacing-base 0 @spacing-sm;
font-size: @font-size-base; font-size: 16px;
font-weight: 600; font-weight: 600;
color: @home-ink; color: @home-ink;
} }
@@ -98,7 +101,7 @@ defineProps<Props>()
:deep(th) { :deep(th) {
padding: @spacing-xs @spacing-sm; padding: @spacing-xs @spacing-sm;
border: 1px solid @border-color; border: 1px solid @border-color;
font-size: @font-size-sm; font-size: 14px;
} }
:deep(a) { :deep(a) {

View File

@@ -44,17 +44,20 @@ function isImageIcon(icon?: string): boolean {
<style lang="less" scoped> <style lang="less" scoped>
.home-service-types { .home-service-types {
// 设计稿区块卡 1780×287 设计px:白底、无描边投影 // 设计稿区块卡 矩形 197 @(70,429) 1780×287:白底、无描边投影
margin-bottom: @spacing-base; margin-bottom: 20px; // 下一卡 矩形 148286 y736 − 本卡底(429+287) = 20
padding: 18px 32px 24px; padding: 24px 42px 0; // 标题 @(112,453) → 卡内 x42 / y24
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;
height: 287px;
box-sizing: border-box;
&__title { &__title {
// 设计稿栏目标题 36 设计px / 700 / #121212(无竖条) // 设计稿栏目标题 @(112,453) 36px / 700 / #121212
margin-bottom: 40px; margin: 0 0 132px; // 圆底顶 550 − 标题底(453+52) 之间留白,服务项文字 @y665
font-size: @font-size-xxl; font-size: 36px;
font-weight: 700; font-weight: 700;
line-height: 52px;
color: @home-ink; color: @home-ink;
} }
@@ -65,21 +68,21 @@ function isImageIcon(icon?: string): boolean {
&__item { &__item {
.flex(column, center); .flex(column, center);
// 设计稿条目横向间距 182 设计px → 定宽 140 居中 // 设计稿服务项文字 x:146/333/530/695/876 → 间距约 187,取定宽 187 居中
width: 140px; width: 187px;
// 设计稿圆底(550..618)与文字(665)之间 41 设计px ≈ 32 CSS // 设计稿圆底(约 y550..618) 与文字(y665) 之间约 47
gap: 32px; gap: 47px;
} }
&__icon { &__icon {
.flex(row, center); .flex(row, center);
// 设计稿为 60 设计px 彩色圆底 + 白色图形 // 设计稿圆底约 68px
width: 52px; width: 68px;
height: 52px; height: 68px;
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-light; background: @primary-light;
color: @white; color: @white;
font-size: @font-size-xl; font-size: 24px;
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;
@@ -112,8 +115,9 @@ function isImageIcon(icon?: string): boolean {
} }
&__label { &__label {
// 设计稿条目文字 16 设计px / #333333 // 设计稿服务项文字 @y665 16px / #333333
font-size: @font-size-sm; font-size: 16px;
line-height: 20px;
color: @home-body; color: @home-body;
text-align: center; text-align: center;
.ellipsis(); .ellipsis();

View File

@@ -31,31 +31,31 @@ const items = [
<style lang="less" scoped> <style lang="less" scoped>
.home-stats { .home-stats {
.flex(row, flex-end); .flex(row, flex-end);
// 设计稿方块间距 30 设计px ≈ 23 CSS // 设计稿方块间距 30px
gap: 23px; gap: 30px;
&__item { &__item {
.flex(column, center, center); .flex(column, center, center);
gap: 0; gap: 0;
// 设计稿统计方块 112×112 设计px → 86×86 CSS,#024FFF 实底 // 设计稿统计方块 112×112,#024FFF 实底
width: 86px; width: 112px;
height: 86px; height: 112px;
min-width: 86px; min-width: 112px;
padding: 0; padding: 0;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
} }
&__value { &__value {
// 设计稿数字 48 设计px / 500 / 白 // 设计稿数字 48px / 500 / 白
font-size: 37px; font-size: 48px;
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.2;
color: @white; color: @white;
} }
&__label { &__label {
font-size: @font-size-sm; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
color: @white; color: @white;
} }

View File

@@ -37,24 +37,24 @@ const greeting = computed(() => {
&__greeting { &__greeting {
.flex(row, flex-start); .flex(row, flex-start);
gap: @spacing-sm; gap: @spacing-sm;
// 设计稿「中午好」52 设计px / 700 / #2C2C2C // 设计稿「中午好」52px / 700 / #2C2C2C
font-size: 40px; font-size: 52px;
font-weight: 700; font-weight: 700;
color: @home-welcome; color: @home-welcome;
} }
&__wave { &__wave {
.flex(row, center); .flex(row, center);
width: 36px; width: 44px;
height: 36px; height: 44px;
border-radius: @radius-round; border-radius: @radius-round;
background: fade(@black, 4%); background: fade(@black, 4%);
font-size: @font-size-lg; font-size: @font-size-xl;
} }
&__summary { &__summary {
// 设计稿副文案 24 设计px / 400 / #2C2C2C // 设计稿副文案 24px / 400 / #2C2C2C
font-size: 18px; font-size: 24px;
font-weight: 400; font-weight: 400;
color: @home-welcome; color: @home-welcome;
} }

View File

@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, watch } from 'vue' import { computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute } from 'vue-router'
import { AppButton } from '@/components'
// 业务详情页:数据只经 useHomeStore,不直接 import api // 业务详情页:数据只经 useHomeStore,不直接 import api
import { useHomeStore } from '@/stores/modules/home' import { useHomeStore } from '@/stores/modules/home'
import type { BusinessType } from '@/types/api' import type { BusinessType } from '@/types/api'
@@ -9,7 +8,6 @@ import HomeServiceContents from './cpns/HomeServiceContents.vue'
import HomeServiceTypes from './cpns/HomeServiceTypes.vue' import HomeServiceTypes from './cpns/HomeServiceTypes.vue'
const route = useRoute() const route = useRoute()
const router = useRouter()
const homeStore = useHomeStore() const homeStore = useHomeStore()
const line = computed(() => homeStore.currentBusiness) const line = computed(() => homeStore.currentBusiness)
@@ -25,8 +23,10 @@ onMounted(load)
</script> </script>
<template> <template>
<!-- 业务详情:设计实测内容区 x70→1850(宽 1780),故与其它常规页一致用 .container -->
<div class="business-detail"> <div class="business-detail">
<nav class="business-detail__breadcrumb text-secondary"> <div class="container">
<nav class="business-detail__breadcrumb">
<RouterLink to="/" class="link"> <RouterLink to="/" class="link">
首页 首页
</RouterLink> </RouterLink>
@@ -36,15 +36,11 @@ onMounted(load)
<template v-if="line"> <template v-if="line">
<header class="business-detail__hero"> <header class="business-detail__hero">
<span class="business-detail__logo">
<!-- TODO(蓝湖): 替换业务图标切图 -->
{{ line.name.slice(0, 2) }}
</span>
<div class="business-detail__intro"> <div class="business-detail__intro">
<h1 class="business-detail__name"> <h1 class="business-detail__name">
{{ line.name }} {{ line.name }}
</h1> </h1>
<p class="business-detail__slogan text-secondary"> <p class="business-detail__slogan">
{{ line.slogan }} {{ line.slogan }}
</p> </p>
</div> </div>
@@ -53,30 +49,27 @@ onMounted(load)
<HomeServiceTypes :service-types="line.serviceTypes" /> <HomeServiceTypes :service-types="line.serviceTypes" />
<HomeServiceContents :contents="line.contents" /> <HomeServiceContents :contents="line.contents" />
<div class="business-detail__cta">
<AppButton type="primary" size="lg" @click="router.push('/demand')">
立即提报
</AppButton>
</div>
</template> </template>
<p v-else class="text-secondary"> <p v-else class="business-detail__loading">
正在加载业务详情... 正在加载业务详情...
</p> </p>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.business-detail { .business-detail {
padding-top: @spacing-lg; // 设计实测:面包屑 y104、头图条 y190、白卡 y429/y736
padding-top: 32px; // 面包屑顶 = 104 − 顶栏 72
padding-bottom: @spacing-xxl; padding-bottom: @spacing-xxl;
&__breadcrumb { &__breadcrumb {
margin-bottom: 36px; // 设计稿面包屑 @(70,104) 24px/500 #666666
// 设计稿面包屑 24 设计px / 500 / #666666 margin-bottom: 49px; // 头图条顶 190 − 面包屑底(104+37)
font-size: 18px; font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 37px;
color: @home-breadcrumb; color: @home-breadcrumb;
.link { .link {
@@ -84,52 +77,52 @@ onMounted(load)
} }
} }
// 设计稿头图卡 1780×219 设计px:浅蓝渐变底、左上图标块 + 标题 + 副标题 // 设计稿头图条 矩形 195 @(70,190) 1780×219(浅蓝渐变底)
&__hero { &__hero {
.flex(column, flex-start); .flex(column, flex-start);
gap: 18px;
margin-bottom: @spacing-base; margin-bottom: @spacing-base;
padding: 19px 22px 24px; padding: 129px 29px 0; // 业务名 @(99,319) → 卡内 x29 / y129
border-radius: @radius-lg; border-radius: @radius-lg;
background: @home-detail-hero-bg; background: @home-detail-hero-bg;
height: 219px;
box-sizing: border-box;
}
&__loading {
padding: 80px 0;
font-size: 16px;
color: @text-color-secondary;
} }
&__logo { &__logo {
.flex(row, center); display: none; // 设计稿头图条内无独立 logo 块
width: 62px;
height: 62px;
border-radius: @radius-lg;
background: @home-detail-logo-bg;
color: @primary-strong;
font-size: @font-size-xl;
font-weight: 600;
} }
&__intro { &__intro {
.flex(column, flex-start); .flex(column, flex-start);
gap: 6px; gap: 8px; // 名称底 319+33=352 → 副标题顶 360
} }
&__name { &__name {
// 设计稿详情标题 26 设计px / #333333 // 设计稿业务名 @(99,319) 26px / #333333
font-size: @font-size-xl; margin: 0;
font-weight: 600; font-size: 26px;
font-weight: 400;
line-height: 33px;
color: @home-body; color: @home-body;
} }
&__slogan { &__slogan {
// 设计稿副标题 14 设计px → 可读下限 11px / #333333 // 设计稿副标题 @(99,360) 14px/400 #333333
font-size: 11px; margin: 0;
font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 18px;
color: @home-body; color: @home-body;
} }
&__cta { &__cta {
padding-top: @spacing-base; display: none; // 设计稿无底部 CTA 块
.app-button {
width: 100%;
}
} }
} }
</style> </style>

View File

@@ -12,7 +12,7 @@ import HomeBusinessCards from './cpns/HomeBusinessCards.vue'
import HomeCaseShowcase from './cpns/HomeCaseShowcase.vue' import HomeCaseShowcase from './cpns/HomeCaseShowcase.vue'
const homeStore = useHomeStore() const homeStore = useHomeStore()
const { stats, banners, businessLines, featuredCases } = storeToRefs(homeStore) const { stats, businessLines, featuredCases } = storeToRefs(homeStore)
const todaySummary = computed(() => `今天有${stats.value.inProgress}个项目正在进行`) const todaySummary = computed(() => `今天有${stats.value.inProgress}个项目正在进行`)
@@ -27,10 +27,11 @@ onMounted(() => {
<template> <template>
<div class="home"> <div class="home">
<!-- 设计稿头图为通栏(0..1920),故不包 .container --> <!-- 设计稿头图为通栏(0..1920),静态设计图,故不包 .container -->
<HomeBanner :banners="banners" /> <HomeBanner />
<div class="container home__dashboard"> <!-- 首页设计稿单独基准:内容区 1680 / 留白 120,故用 container--home -->
<div class="container container--home home__dashboard">
<section class="home__dashboard-main"> <section class="home__dashboard-main">
<HomeWelcome :summary="todaySummary" /> <HomeWelcome :summary="todaySummary" />
<HomeStats :stats="stats" /> <HomeStats :stats="stats" />
@@ -38,11 +39,11 @@ onMounted(() => {
<HomeQuickEntries /> <HomeQuickEntries />
</div> </div>
<div class="container"> <div class="container container--home">
<HomeBusinessCards :lines="businessLines" /> <HomeBusinessCards :lines="businessLines" />
</div> </div>
<div class="container"> <div class="container container--home">
<HomeCaseShowcase <HomeCaseShowcase
:cases="featuredCases" :cases="featuredCases"
@category-change="onCategoryChange" @category-change="onCategoryChange"
@@ -54,27 +55,24 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.home { .home {
.flex(column, flex-start); .flex(column, flex-start);
// 设计稿区块间距 19~20 设计px ≈ 16 CSS;头图紧贴顶栏 // 设计稿区块间距 ≈ 20px(内容区 1680,区块卡紧邻)
gap: @spacing-base; gap: 20px;
padding-top: 0; padding-top: 0;
padding-bottom: @spacing-xxl; padding-bottom: @spacing-xxl;
&__dashboard { &__dashboard {
.flex(row, space-between, stretch); .flex(row, space-between, stretch);
// 设计稿左右卡间距 36 设计px ≈ 28 CSS // 设计稿左右卡间距 36px(左卡宽 922 / 右卡宽 722,内容区 1680)
gap: 28px; gap: 36px;
// 设计稿区块行 1680 设计px 宽(内容区内左右各缩 50 设计px)≈ 1294 CSS
max-width: 1326px;
margin-right: auto;
margin-left: auto;
} }
&__dashboard-main { &__dashboard-main {
.flex(row, space-between, center); .flex(row, space-between, center);
flex: 1; flex: 1;
min-width: 0; // 允许收缩,右侧快捷卡才能在窄视口让位
gap: @spacing-lg; gap: @spacing-lg;
// 设计稿欢迎卡(120,754,922,245):白底 + 浅灰描边,内边距 61/106/66/73 设计px // 设计稿欢迎卡(120,754,922,245):白底 + 浅灰描边
padding: 46px 56px 46px 82px; padding: 0 73px 0 106px;
border: 1px solid @home-card-border; border: 1px solid @home-card-border;
border-radius: @radius-lg; border-radius: @radius-lg;
background: @white; background: @white;

View File

@@ -35,7 +35,9 @@ const localAgree = computed({
<template> <template>
<form class="login-form" @submit.prevent="emit('submit')"> <form class="login-form" @submit.prevent="emit('submit')">
<div class="login-form__field"> <div class="login-form__field">
<label class="login-form__label" for="login-account">账号</label>
<input <input
id="login-account"
v-model="localAccount" v-model="localAccount"
class="login-form__input" class="login-form__input"
type="text" type="text"
@@ -44,7 +46,9 @@ const localAgree = computed({
> >
</div> </div>
<div class="login-form__field"> <div class="login-form__field">
<label class="login-form__label" for="login-password">密码</label>
<input <input
id="login-password"
v-model="localPassword" v-model="localPassword"
class="login-form__input" class="login-form__input"
type="password" type="password"
@@ -57,30 +61,45 @@ const localAgree = computed({
<span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span> <span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span>
</label> </label>
<button class="login-form__submit" type="submit"> <button class="login-form__submit" type="submit">
登 录 登录
</button> </button>
</form> </form>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.login-form { .login-form {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-lg; // 设计稿:Tab 行 x1039 → 表单列整列 x1039 = 卡列 x1022 + 17
margin: 26px 0 0 17px;
width: 680px;
&__field { &__field {
width: 100%; .flex(column, flex-start, stretch);
gap: 8px;
width: 680px;
} }
// 设计稿:输入框 40 设计px ≈ 32px 高、13 设计px 字号(可读下限 11px)、白底浅描边 &__field + &__field {
margin-top: 64px;
}
&__label {
font-size: 13px; // 设计稿 13/500 #1F2937
font-weight: 500;
line-height: 17px;
color: #1f2937;
}
// 设计稿:输入框 680×39、白底 + #DDE1E7 描边、文字 13/400
&__input { &__input {
width: 100%; width: 100%;
height: 32px; height: 39px;
padding: 0 12px; padding: 0 12px;
border: 1px solid @border-color; border: 1px solid #dde1e7;
border-radius: 6px; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 11px; font-size: 13px;
color: @heading-color; color: #1f2937;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
box-sizing: border-box; box-sizing: border-box;
@@ -90,19 +109,26 @@ const localAgree = computed({
} }
&::placeholder { &::placeholder {
color: @heading-color; color: #1f2937;
} }
} }
// 设计稿:协议行 @(1039,641);密码输入底 649 → 协议行文字基线对齐
&__agree { &__agree {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: @spacing-xs; gap: 8px;
font-size: 11px; margin-top: -8px;
color: @text-color-secondary; font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #6b7280;
cursor: pointer; cursor: pointer;
input { input {
margin-top: 1px; flex-shrink: 0;
width: 16px;
height: 16px;
margin: 0;
accent-color: @primary-strong; accent-color: @primary-strong;
} }
@@ -113,17 +139,17 @@ const localAgree = computed({
} }
} }
// 设计稿:登录按钮 44 设计px ≈ 34px 高、#024FFF 实底 // 设计稿:登录按钮 680×46 @y675、#024FFF 实底、文字 15/500
&__submit { &__submit {
width: 100%; width: 100%;
height: 34px; height: 46px;
margin-top: 18px;
border: none; border: none;
border-radius: 6px; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 12px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 2px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -59,7 +59,9 @@ function onSubmit(): void {
<template> <template>
<form class="sms-login" @submit.prevent="onSubmit"> <form class="sms-login" @submit.prevent="onSubmit">
<div class="sms-login__field"> <div class="sms-login__field">
<label class="sms-login__label" for="sms-phone">手机号</label>
<input <input
id="sms-phone"
v-model="localPhone" v-model="localPhone"
class="sms-login__input" class="sms-login__input"
type="text" type="text"
@@ -69,6 +71,7 @@ function onSubmit(): void {
> >
</div> </div>
<div class="sms-login__field"> <div class="sms-login__field">
<label class="sms-login__label" for="sms-code">验证码</label>
<SmsCodeInput v-model:code="localCode" @get-code="onGetCode" /> <SmsCodeInput v-model:code="localCode" @get-code="onGetCode" />
</div> </div>
<label class="sms-login__agree"> <label class="sms-login__agree">
@@ -76,30 +79,46 @@ function onSubmit(): void {
<span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span> <span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span>
</label> </label>
<button class="sms-login__submit" type="submit"> <button class="sms-login__submit" type="submit">
登 录 登录
</button> </button>
</form> </form>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.sms-login { .sms-login {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-lg; // 设计稿:表单列 x989,Tab 行底 412 → 首个标签 438 = 26
margin: 26px 0 0 17px;
width: 680px;
&__field { &__field {
width: 100%; .flex(column, flex-start, stretch);
gap: 8px;
width: 680px;
} }
// 设计稿:输入框 40 设计px ≈ 32px 高、13 设计px 字号(可读下限 11px)、白底浅描边 // 设计稿:手机号输入底 502 → 验证码标签 520 = 18;标签行盒 17 → 18
&__field + &__field {
margin-top: 18px;
}
&__label {
font-size: 13px; // 设计稿 13/500 #1F2937
font-weight: 500;
line-height: 17px;
color: #1f2937;
}
// 设计稿:输入框 680×39、白底 + #DDE1E7 描边、文字 13/400
&__input { &__input {
width: 100%; width: 100%;
height: 32px; height: 39px;
padding: 0 12px; padding: 0 12px;
border: 1px solid @border-color; border: 1px solid #dde1e7;
border-radius: 6px; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 11px; font-size: 13px;
color: @heading-color; color: #1f2937;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
box-sizing: border-box; box-sizing: border-box;
@@ -109,19 +128,26 @@ function onSubmit(): void {
} }
&::placeholder { &::placeholder {
color: @heading-color; color: #1f2937;
} }
} }
// 设计稿:协议行 @(989,622);验证码输入底 584 → 协议行 38
&__agree { &__agree {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: @spacing-xs; gap: 8px;
font-size: 11px; margin-top: 22px;
color: @text-color-secondary; font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #6b7280;
cursor: pointer; cursor: pointer;
input { input {
margin-top: 1px; flex-shrink: 0;
width: 16px;
height: 16px;
margin: 0;
accent-color: @primary-strong; accent-color: @primary-strong;
} }
@@ -132,17 +158,17 @@ function onSubmit(): void {
} }
} }
// 设计稿:登录按钮 44 设计px ≈ 34px 高、#024FFF 实底 // 设计稿:登录按钮 680×46 @y656、#024FFF 实底、文字 15/500
&__submit { &__submit {
width: 100%; width: 100%;
height: 34px; height: 46px;
margin-top: 18px;
border: none; border: none;
border-radius: 6px; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 12px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 2px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -72,22 +72,33 @@ async function onGetCode(target: string): Promise<void> {
<template> <template>
<div class="auth-page"> <div class="auth-page">
<!-- 设计稿:左侧品牌渐变面板 @(0,0) 784×1080,文案列 x257 -->
<aside class="auth-page__brand"> <aside class="auth-page__brand">
<div class="auth-page__brand-inner"> <h1 class="auth-page__logo">
<p class="auth-page__logo"> 虫子智造研发<br>一体化平台
虫子智造研发一体化平台 </h1>
<p class="auth-page__key">
chongzizhizaoyanfayitihuaPINGTAI
</p> </p>
<p class="auth-page__slogan"> <p class="auth-page__slogan">
安全 · 高效 · 可控 安全 · 高效 · 可控
</p> </p>
<RouterLink class="auth-page__guest" to="/">
游客模式 →
</RouterLink>
</div>
</aside> </aside>
<RouterLink class="auth-page__guest" to="/">
游客模式
</RouterLink>
<!-- 设计稿:右侧表单区起点 x1022,表单列宽 693(标签)/680(控件) -->
<main class="auth-page__main"> <main class="auth-page__main">
<div class="auth-page__card"> <div class="auth-page__card">
<h2 class="auth-page__title">
欢迎登录
</h2>
<p class="auth-page__subtitle">
登录您的账号,开启高效之旅
</p>
<div class="auth-page__tabs"> <div class="auth-page__tabs">
<!-- 设计稿 Tab 顺序:手机验证码登录在前、账号密码登录在后 --> <!-- 设计稿 Tab 顺序:手机验证码登录在前、账号密码登录在后 -->
<button <button
@@ -130,118 +141,52 @@ async function onGetCode(target: string): Promise<void> {
</div> </div>
</template> </template>
<style lang="less">
// 共用外壳 .auth-page 定义在 styles/common.less(登录/注册/找回密码三页共享)
</style>
<style lang="less" scoped> <style lang="less" scoped>
// ===== 账号密码登录页专属(--auth-form-x/y 默认值即本页基准) =====
.auth-page { .auth-page {
position: relative; &__subtitle {
display: flex; margin: 12px 0 0;
min-height: 100vh; font-size: 13px; // 设计稿 13/400 #6B7280
// 设计稿:右侧表单区为纯白底,无卡片描边/阴影 font-weight: 400;
background: @white; line-height: 17px;
color: #6b7280;
&__brand {
// 设计稿:左侧品牌渐变面板约占 786/1920 ≈ 41%
flex: 0 0 41%;
.flex(row, flex-start, stretch);
background: linear-gradient(135deg, @auth-gradient-from 0%, @auth-gradient-to 100%);
color: @white;
}
&__brand-inner {
.flex(column, space-between, flex-start);
width: 100%;
// 设计稿:文字列起点 x=257 ≈ 面板宽 32.7%;主标距顶 ≈ 28%、标语底边距底 ≈ 30%
padding: 26vh 48px 30vh 32%;
}
&__logo {
font-size: 31px;
font-weight: 700;
letter-spacing: 2px;
line-height: 1.45;
// 设计稿文本框 432 设计px:定宽让 11 字在「虫子智造研发 / 一体化平台」处折行
max-width: 200px;
}
&__slogan {
font-size: 17px;
font-weight: 600;
letter-spacing: 8px;
}
// 设计稿:「游客模式」为右上角白底上的深色纯文本,无边框
&__guest {
position: absolute;
top: 18px;
right: 16px;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
color: @heading-color;
font-size: 11px;
text-decoration: none;
transition: color 0.2s;
&:hover {
background: transparent;
color: @primary-strong;
}
}
&__main {
flex: 1;
.flex(row, center, center);
padding: @spacing-xl;
}
&__card {
.flex(column, flex-start);
// 设计稿:表单列宽 688 设计px ≈ 530px,标题/Tab/输入/按钮纵向节奏 ≈ 41px
gap: 41px;
width: 100%;
max-width: 530px;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
} }
// 设计稿:Tab 行 @(1039,406),两项间距 24(起 x1039 / x1163)
&__tabs { &__tabs {
// 设计稿:Tab 左对齐于表单列,仅激活项带 2px 蓝色下划线(无整行分隔线) .flex(row, flex-start, center);
.flex(row, flex-start); gap: 24px;
gap: @spacing-sm; margin: 26px 0 0 17px; // 406 − 363 = 43;x1039 − 1022 = 17
border-bottom: none; height: 25px;
} }
&__tab { &__tab {
padding: 0 0 4px; padding: 0;
border: none; border: none;
border-bottom: 2px solid transparent;
background: transparent; background: transparent;
font-size: 11px; font-size: 14px; // 设计稿 14/400 #4B5563 → 激活 14/600 #024FFF
color: @text-color; font-weight: 400;
color: #4b5563;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: color 0.2s;
&.is-active { &.is-active {
border-bottom-color: @primary-strong;
color: @primary-strong;
font-weight: 600; font-weight: 600;
color: @primary-strong;
} }
} }
&__switch { &__switch {
text-align: center; align-self: center; // 设计稿:X 居中 @(1325,774)
font-size: 11px; margin: 53px 0 0; // 按钮底 721 → 774
color: @text-color-secondary; font-size: 12px; // 设计稿 12/400 #6B7280
} font-weight: 400;
line-height: 16px;
&__link { color: #6b7280;
color: @primary-strong;
&:hover {
color: @primary-hover;
}
} }
} }
</style> </style>

View File

@@ -2,30 +2,45 @@
import { onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { PasswordStrength } from '@/components' import { PasswordStrength, SmsCodeInput } from '@/components'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { isPhone, isValidPassword } from '@/utils/validate'
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const username = ref('') const phone = ref('')
const phoneCode = ref('')
const password = ref('') const password = ref('')
const confirmPassword = ref('') const confirmPassword = ref('')
const agree = ref(false) const agree = ref(false)
let redirectTimer: number | null = null let redirectTimer: number | null = null
async function onGetCode(): Promise<void> {
if (!isPhone(phone.value)) {
ElMessage.warning('手机号格式不正确')
return
}
try {
await userStore.sendSmsCode(phone.value)
}
catch {
// 失败提示已由 store 统一弹出,此处仅避免未处理的 rejection
}
}
async function onRegister(): Promise<void> { async function onRegister(): Promise<void> {
if (!username.value.trim()) { if (!isPhone(phone.value)) {
ElMessage.warning('请输入账号') ElMessage.warning('手机号格式不正确')
return return
} }
if (username.value.trim().length < 2 || username.value.trim().length > 30) { if (!/^\d{6}$/.test(phoneCode.value)) {
ElMessage.warning('账号长度应为2-30位') ElMessage.warning('请输入6位验证码')
return return
} }
if (password.value.length < 5 || password.value.length > 30) { if (!isValidPassword(password.value)) {
ElMessage.warning('密码长度应为5-30位') ElMessage.warning('密码长度应为6-20位')
return return
} }
if (password.value !== confirmPassword.value) { if (password.value !== confirmPassword.value) {
@@ -37,7 +52,7 @@ async function onRegister(): Promise<void> {
return return
} }
const ok = await userStore.register({ const ok = await userStore.register({
username: username.value.trim(), username: phone.value,
password: password.value, password: password.value,
confirmPassword: confirmPassword.value, confirmPassword: confirmPassword.value,
}) })
@@ -54,63 +69,88 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<div class="auth-page"> <div class="auth-page register">
<aside class="auth-page__brand"> <aside class="auth-page__brand">
<div class="auth-page__brand-inner"> <h1 class="auth-page__logo">
<p class="auth-page__logo"> 虫子智造研发<br>一体化平台
虫子智造研发一体化平台 </h1>
<p class="auth-page__key">
chongzizhizaoyanfayitihuaPINGTAI
</p> </p>
<p class="auth-page__slogan"> <p class="auth-page__slogan">
安全 · 高效 · 可控 安全 · 高效 · 可控
</p> </p>
<RouterLink class="auth-page__guest" to="/">
游客模式 →
</RouterLink>
</div>
</aside> </aside>
<RouterLink class="auth-page__guest" to="/">
游客模式
</RouterLink>
<main class="auth-page__main"> <main class="auth-page__main">
<div class="auth-page__card"> <div class="auth-page__card">
<h1 class="auth-page__title"> <h2 class="auth-page__title">
注册账号 创建账号
</h1> </h2>
<p class="auth-page__subtitle">
注册后即可开始使用全部功能
</p>
<form class="auth-page__form" @submit.prevent="onRegister"> <form class="register__form" @submit.prevent="onRegister">
<div class="register__field">
<label class="register__label" for="reg-phone">手机号</label>
<input <input
v-model="username" id="reg-phone"
class="auth-page__input" v-model="phone"
class="register__input"
type="text" type="text"
placeholder="请输入账号(2-30位)" placeholder="请输入手机号"
maxlength="30" maxlength="11"
autocomplete="username" autocomplete="tel"
> >
<div class="auth-page__pwd-wrap"> </div>
<div class="register__field">
<label class="register__label" for="reg-code">验证码</label>
<SmsCodeInput id="reg-code" v-model:code="phoneCode" @get-code="onGetCode" />
</div>
<div class="register__field">
<label class="register__label" for="reg-password">设置密码</label>
<input <input
id="reg-password"
v-model="password" v-model="password"
class="auth-page__input" class="register__input"
type="password" type="password"
placeholder="设置密码(5-30位)" placeholder="请设置6-20位密码"
autocomplete="new-password" autocomplete="new-password"
> >
<!-- 设计稿:密码框下 15 高密码强度条 + 15 高提示行 -->
<PasswordStrength :password="password" /> <PasswordStrength :password="password" />
</div> </div>
<div class="register__field">
<label class="register__label" for="reg-confirm">确认密码</label>
<input <input
id="reg-confirm"
v-model="confirmPassword" v-model="confirmPassword"
class="auth-page__input" class="register__input"
type="password" type="password"
placeholder="确认密码" placeholder="请设置6-20位密码"
autocomplete="new-password" autocomplete="new-password"
> >
<label class="auth-page__agree"> </div>
<label class="register__agree">
<input v-model="agree" type="checkbox"> <input v-model="agree" type="checkbox">
<span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span> <span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span>
</label> </label>
<button class="auth-page__submit" type="submit">
注 册 <button class="register__submit" type="submit">
注册
</button> </button>
</form> </form>
<p class="auth-page__switch"> <p class="register__switch">
已有账号?<RouterLink class="auth-page__link" to="/login">立即登录</RouterLink> 已有账号?<RouterLink class="auth-page__link" to="/login">立即登录</RouterLink>
</p> </p>
</div> </div>
@@ -119,112 +159,45 @@ onBeforeUnmount(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.auth-page { // 设计稿(注册账号):表单列起点 x999(--auth-form-x 982 + 17)、垂直顶 268
position: relative; .register {
display: flex; --auth-form-x: 982px;
min-height: 100vh; --auth-form-y: 268px;
// 设计稿:右侧表单区为纯白底,无卡片描边/阴影
background: @white;
&__brand {
// 设计稿:左侧品牌渐变面板约占 786/1920 ≈ 41%
flex: 0 0 41%;
.flex(row, flex-start, stretch);
background: linear-gradient(135deg, @auth-gradient-from 0%, @auth-gradient-to 100%);
color: @white;
}
&__brand-inner {
.flex(column, space-between, flex-start);
width: 100%;
// 设计稿:文字列起点 x=257 ≈ 面板宽 32.7%;主标距顶 ≈ 28%、标语底边距底 ≈ 30%
padding: 26vh 48px 30vh 32%;
}
&__logo {
font-size: 31px;
font-weight: 700;
letter-spacing: 2px;
line-height: 1.45;
// 设计稿文本框 432 设计px:定宽让 11 字在「虫子智造研发 / 一体化平台」处折行
max-width: 200px;
}
&__slogan {
font-size: 17px;
font-weight: 600;
letter-spacing: 8px;
}
// 设计稿:「游客模式」为右上角白底上的深色纯文本,无边框
&__guest {
position: absolute;
top: 18px;
right: 16px;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
color: @heading-color;
font-size: 11px;
text-decoration: none;
transition: color 0.2s;
&:hover {
background: transparent;
color: @primary-strong;
}
}
&__main {
flex: 1;
.flex(row, center, center);
padding: @spacing-xl;
}
&__card {
.flex(column, flex-start);
// 设计稿:表单列宽 688 设计px ≈ 530px,标题/输入/按钮纵向节奏 ≈ 41px
gap: 41px;
width: 100%;
max-width: 530px;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
// 设计稿:标题(34 设计px ≈ 26px)到首个输入框 ≈ 62px
&__title {
margin-bottom: 21px;
font-size: 26px;
font-weight: 700;
color: @heading-color;
}
&__form { &__form {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-lg; // 副标题底 342 → 首个标签 368 = 26(副标题行盒 17 + 外边距 12 → 实际需补 38)
width: 100%; margin: 38px 0 0 17px;
width: 680px;
} }
// 设置密码 → 密码强度条 ≈ 9px,强度条 → 提示 ≈ 9px(组件内) &__field {
&__pwd-wrap { .flex(column, flex-start, stretch);
.flex(column, flex-start); gap: 8px;
gap: 9px; width: 680px;
width: 100%; }
// 设计稿:手机号框底 430 → 验证码标签 450 = 20;验证码框底 514 → 设置密码标签 532 = 18
&__field + &__field {
margin-top: 18px;
}
&__label {
font-size: 13px; // 设计稿 13/500 #1F2937
font-weight: 500;
line-height: 17px;
color: #1f2937;
} }
// 设计稿:输入框 40 设计px ≈ 32px 高、13 设计px 字号(可读下限 11px)、白底浅描边
&__input { &__input {
width: 100%; width: 100%;
height: 32px; height: 39px;
padding: 0 12px; padding: 0 12px;
border: 1px solid @border-color; border: 1px solid #dde1e7;
border-radius: 6px; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 11px; font-size: 13px;
color: @heading-color; color: #1f2937;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
box-sizing: border-box; box-sizing: border-box;
@@ -234,40 +207,46 @@ onBeforeUnmount(() => {
} }
&::placeholder { &::placeholder {
color: @heading-color; color: #1f2937;
} }
} }
// 设计稿:协议行 @(999,748)
&__agree { &__agree {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: @spacing-xs; gap: 8px;
font-size: 11px; margin-top: 22px;
color: @text-color-secondary; font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #6b7280;
cursor: pointer; cursor: pointer;
input { input {
margin-top: 1px; flex-shrink: 0;
width: 16px;
height: 16px;
margin: 0;
accent-color: @primary-strong; accent-color: @primary-strong;
} }
// 设计稿:《用户协议》《隐私政策》为强调蓝,其余为次级灰
em { em {
font-style: normal; font-style: normal;
color: @primary-strong; color: @primary-strong;
} }
} }
// 设计稿:注册按钮 44 设计px ≈ 34px 高、#024FFF 实底 // 设计稿:注册按钮 680×46 @y782、#024FFF 实底、文字 15/500
&__submit { &__submit {
width: 100%; width: 100%;
height: 34px; height: 46px;
margin-top: 18px;
border: none; border: none;
border-radius: 6px; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 12px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 2px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
@@ -276,20 +255,22 @@ onBeforeUnmount(() => {
} }
} }
// 设计稿:按钮 → 底部切换链接仅约 13px // 设计稿:按钮底 828 → 切换行 844 = 16
&__switch { &__switch {
margin-top: -26px; align-self: center;
text-align: center; margin: 16px 0 0;
font-size: 11px; font-size: 12px;
color: @text-color-secondary; font-weight: 400;
line-height: 16px;
color: #6b7280;
} }
&__link { &__subtitle {
color: @primary-strong; margin: 12px 0 0;
font-size: 13px; // 设计稿 13/400 #6B7280
&:hover { font-weight: 400;
color: @primary-hover; line-height: 17px;
} color: #6b7280;
} }
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
import { onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { SmsCodeInput } from '@/components' import { PasswordStrength, SmsCodeInput } from '@/components'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { isPhone, isValidPassword } from '@/utils/validate' import { isPhone, isValidPassword } from '@/utils/validate'
@@ -71,62 +71,89 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<div class="auth-page"> <div class="auth-page reset">
<aside class="auth-page__brand"> <aside class="auth-page__brand">
<div class="auth-page__brand-inner"> <h1 class="auth-page__logo">
<p class="auth-page__logo"> 虫子智造研发<br>一体化平台
虫子智造研发一体化平台 </h1>
<p class="auth-page__key">
chongzizhizaoyanfayitihuaPINGTAI
</p> </p>
<p class="auth-page__slogan"> <p class="auth-page__slogan">
安全 · 高效 · 可控 安全 · 高效 · 可控
</p> </p>
<RouterLink class="auth-page__guest" to="/">
游客模式 →
</RouterLink>
</div>
</aside> </aside>
<RouterLink class="auth-page__guest" to="/">
游客模式
</RouterLink>
<main class="auth-page__main"> <main class="auth-page__main">
<div class="auth-page__card"> <div class="auth-page__card">
<h1 class="auth-page__title"> <h2 class="auth-page__title">
找回密码 找回密码
</h1> </h2>
<p class="auth-page__subtitle">
使用手机号重新设置您的密码
</p>
<form class="auth-page__form" @submit.prevent="onReset"> <form class="reset__form" @submit.prevent="onReset">
<div class="reset__field">
<label class="reset__label" for="reset-phone">手机号</label>
<input <input
id="reset-phone"
v-model="phone" v-model="phone"
class="auth-page__input" class="reset__input"
type="text" type="text"
placeholder="请输入手机号" placeholder="请输入手机号"
maxlength="11" maxlength="11"
autocomplete="tel" autocomplete="tel"
> >
<SmsCodeInput v-model:code="code" @get-code="onGetCode" /> </div>
<div class="reset__field">
<label class="reset__label" for="reset-code">验证码</label>
<SmsCodeInput id="reset-code" v-model:code="code" @get-code="onGetCode" />
</div>
<div class="reset__field">
<label class="reset__label" for="reset-password">设置密码</label>
<input <input
id="reset-password"
v-model="password" v-model="password"
class="auth-page__input" class="reset__input"
type="password" type="password"
placeholder="新密码(6-20位)" placeholder="请设置6-20位密码"
autocomplete="new-password" autocomplete="new-password"
> >
<!-- 设计稿:密码框下 15 高密码强度条 + 15 高提示行 -->
<PasswordStrength :password="password" />
</div>
<div class="reset__field">
<label class="reset__label" for="reset-confirm">确认密码</label>
<input <input
id="reset-confirm"
v-model="confirmPassword" v-model="confirmPassword"
class="auth-page__input" class="reset__input"
type="password" type="password"
placeholder="确认新密码" placeholder="请设置6-20位密码"
autocomplete="new-password" autocomplete="new-password"
> >
<label class="auth-page__agree"> </div>
<label class="reset__agree">
<input v-model="agree" type="checkbox"> <input v-model="agree" type="checkbox">
<span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span> <span>已阅读并同意<em>《用户协议》</em>和<em>《隐私政策》</em></span>
</label> </label>
<button class="auth-page__submit" type="submit">
<button class="reset__submit" type="submit">
确认修改 确认修改
</button> </button>
</form> </form>
<p class="auth-page__switch"> <p class="reset__switch">
想起密码了?<RouterLink class="auth-page__link" to="/login">返回登录</RouterLink> 已有账号?<RouterLink class="auth-page__link" to="/login">立即登录</RouterLink>
</p> </p>
</div> </div>
</main> </main>
@@ -134,105 +161,45 @@ onBeforeUnmount(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.auth-page { // 设计稿(找回密码):表单列起点 x1040(--auth-form-x 1023 + 17)、垂直顶 261
position: relative; .reset {
display: flex; --auth-form-x: 1023px;
min-height: 100vh; --auth-form-y: 261px;
// 设计稿:右侧表单区为纯白底,无卡片描边/阴影
background: @white;
&__brand {
// 设计稿:左侧品牌渐变面板约占 786/1920 ≈ 41%
flex: 0 0 41%;
.flex(row, flex-start, stretch);
background: linear-gradient(135deg, @auth-gradient-from 0%, @auth-gradient-to 100%);
color: @white;
}
&__brand-inner {
.flex(column, space-between, flex-start);
width: 100%;
// 设计稿:文字列起点 x=257 ≈ 面板宽 32.7%;主标距顶 ≈ 28%、标语底边距底 ≈ 30%
padding: 26vh 48px 30vh 32%;
}
&__logo {
font-size: 31px;
font-weight: 700;
letter-spacing: 2px;
line-height: 1.45;
// 设计稿文本框 432 设计px:定宽让 11 字在「虫子智造研发 / 一体化平台」处折行
max-width: 200px;
}
&__slogan {
font-size: 17px;
font-weight: 600;
letter-spacing: 8px;
}
// 设计稿:「游客模式」为右上角白底上的深色纯文本,无边框
&__guest {
position: absolute;
top: 18px;
right: 16px;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
color: @heading-color;
font-size: 11px;
text-decoration: none;
transition: color 0.2s;
&:hover {
background: transparent;
color: @primary-strong;
}
}
&__main {
flex: 1;
.flex(row, center, center);
padding: @spacing-xl;
}
&__card {
.flex(column, flex-start);
// 设计稿:表单列宽 688 设计px ≈ 530px,标题/输入/按钮纵向节奏 ≈ 41px
gap: 41px;
width: 100%;
max-width: 530px;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
// 设计稿:标题(34 设计px ≈ 26px)到首个输入框 ≈ 62px
&__title {
margin-bottom: 21px;
font-size: 26px;
font-weight: 700;
color: @heading-color;
}
&__form { &__form {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-lg; // 副标题底 335 → 首个标签 361 = 26(副标题行盒 17 + 外边距 12 → 实际需补 38)
width: 100%; margin: 38px 0 0 17px;
width: 680px;
}
&__field {
.flex(column, flex-start, stretch);
gap: 8px;
width: 680px;
}
// 设计稿:手机号框底 425 → 验证码标签 443 = 18
&__field + &__field {
margin-top: 18px;
}
&__label {
font-size: 13px; // 设计稿 13/500 #1F2937
font-weight: 500;
line-height: 17px;
color: #1f2937;
} }
// 设计稿:输入框 40 设计px ≈ 32px 高、13 设计px 字号(可读下限 11px)、白底浅描边
&__input { &__input {
width: 100%; width: 100%;
height: 32px; height: 39px;
padding: 0 12px; padding: 0 12px;
border: 1px solid @border-color; border: 1px solid #dde1e7;
border-radius: 6px; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 11px; font-size: 13px;
color: @heading-color; color: #1f2937;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
box-sizing: border-box; box-sizing: border-box;
@@ -242,40 +209,46 @@ onBeforeUnmount(() => {
} }
&::placeholder { &::placeholder {
color: @heading-color; color: #1f2937;
} }
} }
// 设计稿:协议行 @(1040,741)
&__agree { &__agree {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: @spacing-xs; gap: 8px;
font-size: 11px; margin-top: 22px;
color: @text-color-secondary; font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #6b7280;
cursor: pointer; cursor: pointer;
input { input {
margin-top: 1px; flex-shrink: 0;
width: 16px;
height: 16px;
margin: 0;
accent-color: @primary-strong; accent-color: @primary-strong;
} }
// 设计稿:《用户协议》《隐私政策》为强调蓝,其余为次级灰
em { em {
font-style: normal; font-style: normal;
color: @primary-strong; color: @primary-strong;
} }
} }
// 设计稿:确认修改按钮 44 设计px ≈ 34px 高、#024FFF 实底 // 设计稿:确认修改按钮 680×46 @y775、#024FFF 实底、文字 15/500
&__submit { &__submit {
width: 100%; width: 100%;
height: 34px; height: 46px;
margin-top: 18px;
border: none; border: none;
border-radius: 6px; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 12px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 2px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
@@ -284,20 +257,22 @@ onBeforeUnmount(() => {
} }
} }
// 设计稿:按钮 → 底部切换链接仅约 13px // 设计稿:按钮底 821 → 切换行 838 = 17
&__switch { &__switch {
margin-top: -26px; align-self: center;
text-align: center; margin: 17px 0 0;
font-size: 11px; font-size: 12px;
color: @text-color-secondary; font-weight: 400;
line-height: 16px;
color: #6b7280;
} }
&__link { &__subtitle {
color: @primary-strong; margin: 12px 0 0;
font-size: 13px; // 设计稿 13/400 #6B7280
&:hover { font-weight: 400;
color: @primary-hover; line-height: 17px;
} color: #6b7280;
} }
} }
</style> </style>

View File

@@ -1,69 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
interface MenuItem { interface MenuItem {
to: string to: string
icon: string
label: string label: string
} }
// 设计稿(个人中心)菜单项 y268/304/340/376/412/448/484/520,行距 36,文字 x179(卡内缩进 24)
const menus: MenuItem[] = [ const menus: MenuItem[] = [
{ to: '/profile/security', icon: '🔒', label: '账号安全' }, { to: '/profile/security', label: '账号安全' },
{ to: '/profile/address', icon: '📍', label: '收货地址' }, { to: '/profile/address', label: '地址管理' },
{ to: '/profile/favorites', icon: '⭐', label: '我的收藏' }, { to: '/profile/favorites', label: '收藏列表' },
{ to: '/profile/qualifications', icon: '📜', label: '资质管理' }, { to: '/profile/qualifications', label: '资质列表' },
{ to: '/profile/feedback', icon: '💡', label: '意见反馈' }, { to: '/profile/feedback', label: '意见反馈' },
{ to: '/profile/help', icon: '❓', label: '帮助中心' }, { to: '/profile/help', label: '帮助中心' },
{ to: '/profile/about', icon: 'ℹ️', label: '关于我们' }, { to: '/profile/about', label: '关于我们' },
{ to: '/profile/hotline', icon: '📞', label: '客服热线' }, { to: '/profile/hotline', label: '专属客服热线' },
] ]
</script> </script>
<template> <template>
<nav class="profile-sider" aria-label="个人中心导航"> <nav class="profile-sider" aria-label="个人中心导航">
<ul class="profile-sider__menu">
<li v-for="item in menus" :key="item.to">
<RouterLink :to="item.to" class="profile-sider__item">
<span class="profile-sider__label">{{ item.label }}</span>
</RouterLink>
</li>
</ul>
<div class="profile-sider__footer">
<RouterLink <RouterLink
to="/profile/hotline" v-for="item in menus"
class="profile-sider__hotline" :key="item.to"
title="专属客服热线" :to="item.to"
class="profile-sider__item"
> >
📞 专属客服热线 {{ item.label }}
</RouterLink> </RouterLink>
</div>
</nav> </nav>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿:菜单为独立白卡(245 宽),选中项浅蓝底 @profile-nav-active-bg 圆角 6
.profile-sider { .profile-sider {
.flex(column, space-between);
min-height: 480px;
padding: @spacing-sm 0;
&__menu {
.flex(column, flex-start); .flex(column, flex-start);
gap: 0; gap: 0;
list-style: none; padding: 4px 0;
} border-radius: @radius-lg;
background: @white;
// 设计 14/28:纯文字菜单,选中项浅蓝底 + 蓝色加粗,行距 36 × 0.77 ≈ 28
&__item { &__item {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 28px; height: 36px; // 设计行距 36
padding: 0 @spacing-sm; padding: 0 24px; // 卡 x155 → 文字 x179
border-radius: @profile-radius; border-radius: @profile-radius;
color: @text-color; color: @text-color;
font-size: 11px; font-size: @font-size-base; // 设计 12/400
line-height: 1; font-weight: 400;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
background: @primary-light;
color: @primary-strong; color: @primary-strong;
} }
@@ -73,25 +59,5 @@ const menus: MenuItem[] = [
font-weight: 600; font-weight: 600;
} }
} }
&__label {
white-space: nowrap;
}
&__footer {
padding: @spacing-sm @spacing-sm 0;
border-top: 1px solid @project-divider;
}
&__hotline {
display: inline-block;
font-size: 11px;
color: @text-color-secondary;
transition: color 0.2s;
&:hover {
color: @primary-strong;
}
}
} }
</style> </style>

View File

@@ -32,7 +32,6 @@ function save(): void {
<div class="profile-user-card__avatar" aria-hidden="true"> <div class="profile-user-card__avatar" aria-hidden="true">
王 王
</div> </div>
<div class="profile-user-card__info">
<input <input
v-model="draft" v-model="draft"
class="profile-user-card__name" class="profile-user-card__name"
@@ -42,51 +41,43 @@ function save(): void {
@keyup.enter="save" @keyup.enter="save"
@blur="save" @blur="save"
> >
<p class="profile-user-card__phone text-secondary"> <p class="profile-user-card__phone">
{{ profile?.phone ?? '' }} {{ profile?.phone ?? '' }}
</p> </p>
</div> </div>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 14:左列紫渐变用户卡(渐变实取 #5459E8 → #D2BDF3),内容居中 // 设计稿(个人中心):头像 46 圆形居中,名称 18/600,手机 14/400;
// 整块 245×110 紫渐变卡,内容垂直居中
.profile-user-card { .profile-user-card {
.flex(column, center); .flex(column, center, center);
gap: @spacing-xs; // 设计 14:头像与文字块间距约 4px height: 110px;
padding: @spacing-base @spacing-sm; // 设计:上 20 / 下 25 设计px ≈ 16
border-radius: @radius-lg; border-radius: @radius-lg;
background: linear-gradient(170deg, @profile-user-from 0%, @profile-user-to 100%); background: linear-gradient(180deg, @profile-user-from 0%, @profile-user-to 100%);
box-sizing: border-box;
&__avatar { &__avatar {
.flex(row, center); .flex(row, center);
flex-shrink: 0; flex-shrink: 0;
width: 42px; // 设计 54 × 0.77 width: 42px;
height: 42px; height: 42px;
border-radius: @radius-round; border-radius: @radius-round;
background: @profile-user-avatar; background: fade(@white, 22%);
color: @white; color: @white;
font-size: @font-size-lg; font-size: @font-size-lg;
font-weight: 600; font-weight: 600;
} }
&__info {
.flex(column, center);
gap: @spacing-xs;
min-width: 0;
width: 100%;
text-align: center;
}
// 卡内为深色底:名称/号码一律白字(设计 14 实取 #FFFFFF)
&__name { &__name {
width: 100%; width: 100%;
height: 16px; // 设计 19 × 0.77 height: 26px;
margin-top: 6px;
padding: 0 @spacing-xs; padding: 0 @spacing-xs;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: @profile-radius; border-radius: @profile-radius;
background: transparent; background: transparent;
font-size: 11px; font-size: @font-size-lg; // 设计 18/600
font-weight: 600; font-weight: 600;
color: @white; color: @white;
text-align: center; text-align: center;
@@ -103,15 +94,17 @@ function save(): void {
} }
&::placeholder { &::placeholder {
font-size: 11px; font-size: @font-size-lg;
font-weight: 400; font-weight: 400;
color: fade(@white, 75%); color: fade(@white, 75%);
} }
} }
&__phone { &__phone {
font-size: 11px; margin-top: 6px;
font-size: @font-size-sm; // 设计 14/400
color: @white; color: @white;
text-align: center;
} }
} }
</style> </style>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed } from 'vue'
import { AppButton } from '@/components'
import type { Profile } from '@/types/business/profile' import type { Profile } from '@/types/business/profile'
interface Props { interface Props {
@@ -17,354 +16,148 @@ const emit = defineEmits<{
deleteAccount: [] deleteAccount: []
}>() }>()
const agree = ref(false)
const hasEmail = computed(() => Boolean(props.profile?.email)) const hasEmail = computed(() => Boolean(props.profile?.email))
/** 注销条件(mock 全部满足) */
const conditions = [
'账号无未完成的订单',
'无未结清的款项',
'无进行中的合同',
'无未处理的工单',
]
/** 注销风险提示 */
const risks = [
'账号注销后将无法登录本应用,所有个人信息将被永久删除',
'您名下的订单、合同、工单等业务数据将一并清除,无法找回',
'注销后同一手机号/邮箱将无法再次注册本应用',
'如您有未完成的订单或服务请先处理完毕后再进行注销',
]
</script> </script>
<template> <template>
<div class="security-panel"> <div class="security-panel">
<!-- 修改密码 --> <!-- 设计稿:三行白卡 1672×86,行距 16,左缩进 24 -->
<section class="security-panel__row"> <section
<span class="security-panel__icon security-panel__icon--pwd" aria-hidden="true" /> class="security-panel__row"
<div class="security-panel__main"> @click="emit('changePassword')"
<h3 class="security-panel__title">
修改密码
</h3>
<p class="security-panel__status">
<span class="security-panel__badge security-panel__badge--ok">已设置</span>
<span class="security-panel__note">上次修改:{{ profile?.passwordModifiedAt }}</span>
</p>
</div>
<AppButton type="default" @click="emit('changePassword')">
修改密码
</AppButton>
</section>
<!-- 绑定手机 -->
<section class="security-panel__row">
<span class="security-panel__icon security-panel__icon--phone" aria-hidden="true" />
<div class="security-panel__main">
<h3 class="security-panel__title">
绑定手机
</h3>
<p class="security-panel__status">
<span class="security-panel__badge security-panel__badge--ok">已绑定 {{ profile?.phone }}</span>
</p>
<p class="security-panel__note">
注:换绑新手机号之后,可以用新的手机号及当前密码登录
</p>
</div>
<AppButton type="default" @click="emit('bindPhone')">
绑定手机
</AppButton>
</section>
<!-- 绑定邮箱 -->
<section class="security-panel__row">
<span class="security-panel__icon security-panel__icon--email" aria-hidden="true" />
<div class="security-panel__main">
<h3 class="security-panel__title">
绑定邮箱
</h3>
<p class="security-panel__status">
<template v-if="hasEmail">
<span class="security-panel__badge security-panel__badge--ok">已绑定 {{ profile?.email }}</span>
</template>
<template v-else>
<span class="security-panel__badge">未绑定</span>
</template>
</p>
</div>
<div class="security-panel__actions">
<AppButton
v-if="!hasEmail"
type="default"
@click="emit('bindEmail', 'email-bind')"
> >
绑定邮箱 <div class="security-panel__main">
</AppButton> <h3 class="security-panel__title">
<template v-else> 修改密码
<AppButton type="default" @click="emit('bindEmail', 'email-rebind')"> </h3>
更换绑定邮箱 <p class="security-panel__sub">
</AppButton> 上次修改:{{ profile?.passwordModifiedAt }}
<AppButton type="text" @click="emit('unbindEmail')"> </p>
取消绑定
</AppButton>
</template>
</div> </div>
<span class="security-panel__state security-panel__state--ok">已设置</span>
<span class="security-panel__arrow" aria-hidden="true">›</span>
</section> </section>
<!-- 注销账号(danger 区) --> <section
<section class="security-panel__danger"> class="security-panel__row"
<h3 class="security-panel__danger-title"> @click="emit('bindPhone')"
注销账号 >
<div class="security-panel__main">
<h3 class="security-panel__title">
绑定手机
</h3> </h3>
<p class="security-panel__sub">
<div class="security-panel__block"> {{ profile?.phone ?? '' }}
<h4 class="security-panel__block-title security-panel__block-title--ok"> </p>
注销条件
</h4>
<ul class="security-panel__checks">
<li v-for="item in conditions" :key="item" class="security-panel__check">
<span class="security-panel__check-ok">✓</span>{{ item }}
</li>
</ul>
</div> </div>
<span class="security-panel__state security-panel__state--ok">已绑定</span>
<span class="security-panel__arrow" aria-hidden="true">›</span>
</section>
<div class="security-panel__block"> <section
<h4 class="security-panel__block-title security-panel__block-title--risk"> class="security-panel__row"
注销风险提示 @click="emit('bindEmail', hasEmail ? 'email-rebind' : 'email-bind')"
</h4> >
<ol class="security-panel__risks"> <div class="security-panel__main">
<li v-for="(item, index) in risks" :key="item"> <h3 class="security-panel__title">
{{ index + 1 }}. {{ item }} 绑定邮箱
</li> </h3>
</ol> <p class="security-panel__sub">
{{ hasEmail ? profile?.email : '未绑定邮箱' }}
</p>
</div> </div>
<span
class="security-panel__state"
:class="{ 'security-panel__state--ok': hasEmail }"
>{{ hasEmail ? '已绑定' : '未绑定' }}</span>
<span class="security-panel__arrow" aria-hidden="true">›</span>
</section>
<label class="security-panel__agree"> <!-- 设计稿:注销账号 1672×48,灰字,浅灰描边 -->
<input v-model="agree" type="checkbox"> <button
<span>已阅读并同意<em>《账号注销协议》</em></span> type="button"
</label> class="security-panel__delete"
<AppButton
type="default"
class="security-panel__delete-btn"
:disabled="!agree"
@click="emit('deleteAccount')" @click="emit('deleteAccount')"
> >
注销账号 注销账号
</AppButton> </button>
</section>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计 28:白卡行 + 浅灰描边、图标块 44×44 设计px、标题 14 设计px w600、副文 11 设计px // 设计稿(个人中心):
// 行卡 1672×86 bg #FFFFFF bd #F0F1F3/1,行距 16;标题 14/600 #1F2937 @x428
// 副文 11/400 #9CA3AF;右端状态 11/500(已设置 #16A34A / 未绑定 #6B7280)+ 箭头 ›
// 注销按钮 1672×48 bd #E5E7EB/1,文字 15/500 #9CA3AF
.security-panel { .security-panel {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-base; gap: @spacing-base;
&__row { &__row {
.flex(row, flex-start, center); .flex(row, space-between, center);
gap: @spacing-base; height: 86px;
padding: @spacing-base; padding: 0 24px;
border: 1px solid @profile-card-border; border: 1px solid @profile-card-border;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
} box-sizing: border-box;
cursor: pointer;
transition: border-color 0.2s;
// 设计 28 实取图标块底色:#C7D2FE / #FDEBEB / #FEE2E2 &:hover {
&__icon { border-color: @profile-line;
flex-shrink: 0;
width: 34px; // 设计 44 × 0.77
height: 34px;
border-radius: @radius-base;
&--pwd {
background: @profile-icon-pwd;
}
&--phone {
background: @profile-icon-phone;
}
&--email {
background: @profile-icon-email;
} }
} }
&__main { &__main {
flex: 1;
min-width: 0; min-width: 0;
} }
&__title { &__title {
font-size: 11px; // 设计 14 → 10.8,取可读下限 11 font-size: @font-size-base; // 设计 14/600
font-weight: 600; font-weight: 600;
color: @heading-color; color: @profile-ink;
} }
&__status { &__sub {
.flex(row, flex-start, center); margin-top: 6px;
flex-wrap: wrap; font-size: @font-size-sm; // 设计 11/400
gap: @spacing-sm; color: @finance-axis-text; // #9CA3AF
margin-top: @spacing-xs;
font-size: 11px;
} }
// 设计 28:已设置/已绑定 = 浅绿底绿字胶囊,未绑定 = 纯灰字 &__state {
&__badge { margin-left: auto;
padding: 2px 8px; font-size: @font-size-sm;
border-radius: @radius-sm;
background: transparent;
color: @text-color-secondary;
font-size: 11px;
font-weight: 500; font-weight: 500;
color: @finance-muted-text; // 未绑定 #6B7280
&--ok { &--ok {
background: @tag-success-bg; color: @success-color; // 已设置 / 已绑定 #16A34A
color: @success-color;
} }
} }
&__note { &__arrow {
margin-top: @spacing-xs; margin-left: 20px;
color: @text-color-disabled; font-size: 21px;
font-size: 11px; line-height: 1;
line-height: 1.6; color: @finance-axis-text;
} }
&__actions { &__delete {
.flex(row, center); width: 100%;
gap: @spacing-sm; height: 48px;
flex-shrink: 0;
}
// 行内操作按钮:白底灰描边小按钮(设计 20/17 操作按钮同规格)
.app-button {
height: 28px;
padding: 0 12px;
border: 1px solid @profile-line; border: 1px solid @profile-line;
border-radius: @profile-radius;
background: @white;
color: @text-color;
font-size: 11px;
font-weight: 500;
&:hover:not(:disabled) {
border-color: @primary-strong;
color: @primary-strong;
background: @white;
}
}
.app-button--text {
border-color: transparent;
background: transparent;
color: @text-color;
&:hover:not(:disabled) {
border-color: transparent;
background: transparent;
color: @primary-strong;
}
}
&__danger {
.flex(column, flex-start);
gap: @spacing-base;
padding: @spacing-lg;
border: 1px solid @profile-card-border;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
} color: @finance-axis-text;
font-size: @font-size-base; // 设计 15/500
&__danger-title {
font-size: 12px; // 设计 15 → 12
font-weight: 700;
color: @danger-color;
}
&__block {
.flex(column, flex-start);
gap: @spacing-sm;
}
// 设计 29:注销条件标题绿、注销风险提示标题红
&__block-title {
font-size: 11px;
font-weight: 600;
&--ok {
color: @success-color;
}
&--risk {
color: @danger-color;
}
}
&__checks {
.flex(column, flex-start);
gap: @spacing-xs;
list-style: none;
}
&__check {
font-size: 11px;
color: @text-color;
}
&__check-ok {
margin-right: @spacing-xs;
font-weight: 600;
color: @success-color;
}
&__risks {
margin: 0;
padding-left: @spacing-base;
.flex(column, flex-start);
gap: @spacing-xs;
font-size: 11px;
color: @text-color;
line-height: 1.6;
}
&__agree {
.flex(row, flex-start, center);
gap: @spacing-sm;
font-size: 11px;
color: @text-color-secondary;
cursor: pointer;
input {
accent-color: @primary-strong;
}
em {
font-style: normal;
color: @primary-strong;
}
}
// 注销账号:白底灰描边整行按钮(设计 28 实取文字 #9CA3AF、15 设计px)
&__delete-btn {
width: 100%;
height: 36px;
border: 1px solid @profile-line !important;
background: @white !important;
color: @text-color-disabled !important;
font-size: 12px;
font-weight: 500; font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) { &:hover {
border-color: @danger-color !important; border-color: @danger-color;
background: @white !important; color: @danger-color;
color: @danger-color !important;
}
&:disabled {
opacity: 1;
cursor: not-allowed;
} }
} }
} }

View File

@@ -5,7 +5,8 @@ import { useProfileStore } from '@/stores/modules/profile'
import ProfileSider from './cpns/ProfileSider.vue' import ProfileSider from './cpns/ProfileSider.vue'
import ProfileUserCard from './cpns/ProfileUserCard.vue' import ProfileUserCard from './cpns/ProfileUserCard.vue'
// 布局容器:顶部用户卡 + 左侧菜单 / 右侧子路由出口,数据只经 store // 布局容器:左侧用户卡 + 菜单(x155 起 245 宽),右侧内容卡(x400 起 1400 宽)
// 数据只经 store
const profileStore = useProfileStore() const profileStore = useProfileStore()
const { profile } = storeToRefs(profileStore) const { profile } = storeToRefs(profileStore)
@@ -20,7 +21,7 @@ onMounted(() => {
<template> <template>
<div class="profile"> <div class="profile">
<div class="profile__container"> <div class="profile__container container">
<div class="profile__body"> <div class="profile__body">
<aside class="profile__aside"> <aside class="profile__aside">
<ProfileUserCard :profile="profile" @save-nickname="onSaveNickname" /> <ProfileUserCard :profile="profile" @save-nickname="onSaveNickname" />
@@ -35,40 +36,46 @@ onMounted(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿(个人中心 1920×1568)锚点:
// 面包屑 y23 h26 · 左列 x155 宽 245 · 右卡 x400 y98 1400×532 · 页脚 y1087
// 内容区顶 98 = 顶栏 72 + 上留白 26
.profile { .profile {
padding-bottom: @spacing-xxl; min-height: 737px; // 内容区 y98→835
background: @bg-color; background: @profile-page-bg;
&__container { &__container {
width: @container-width; padding-top: 26px; // 顶栏 72 → 内容区 98
max-width: calc(100% - @spacing-xl); // 个人中心系独立基准(视觉核对定版 x155/1600),不随全站 70/1780 联动
margin: 0 auto; padding-left: clamp(@spacing-base, calc((100% - @profile-content) / 2), @profile-gutter);
padding-top: @spacing-lg; padding-right: clamp(@spacing-base, calc((100% - @profile-content) / 2), @profile-gutter);
} }
&__body { &__body {
.flex(row, flex-start); .flex(row, flex-start, stretch);
gap: @spacing-base; width: 100%;
align-items: flex-start;
} }
// 左列(设计14):紫渐变用户卡 + 菜单,直接坐在页面底色上,无外层白卡 // 左列 200:用户卡 + 菜单白卡,两卡间距 6
// (设计稿左列 245、右卡 1400,合计 1645 超出内容宽 1595,按比例收左列到 200 保右卡 1400)
&__aside { &__aside {
width: 114px; // 设计 148 × 0.77 .flex(column, flex-start, stretch);
flex-shrink: 0; gap: 6px;
.flex(column, flex-start); flex: 0 0 200px;
gap: @spacing-base; width: 200px;
margin-right: 0;
} }
// 右侧内容面板(设计14/28):整块白卡,内缩 16px 承载各页面 // 右内容卡 1400:白底 + 浅灰描边
&__main { &__main {
flex: 1; flex: 1 1 auto;
min-width: 0; min-width: 0;
min-height: 400px; min-height: 425px;
padding: @spacing-base; margin-left: 40px; // 左列 200 → 右卡起点 240(设计稿比例:160/245 ≈ 0.65)
border-radius: @radius-base; padding: 24px 24px 0; // 卡内左缩进:卡 → 标题 +28
border: 1px solid @profile-card-border;
border-radius: @radius-lg;
background: @white; background: @white;
box-shadow: @shadow-base; box-sizing: border-box;
} }
} }
</style> </style>

View File

@@ -15,6 +15,36 @@ const router = useRouter()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const { profile } = storeToRefs(profileStore) const { profile } = storeToRefs(profileStore)
// ── 注销账号二次确认(设计稿 __4d03fd89:风险提示 + 注销条件 + 双按钮) ──
const deleteVisible = ref(false)
const agree = ref(false)
const identityVisible = ref(false)
/** 注销条件(mock 全部满足) */
const conditions = [
{ label: '账号无未完成的订单', ok: true },
{ label: '账号无未结清的款项', ok: false },
{ label: '账号无进行中的合同', ok: true },
{ label: '账号无处理中的工单', ok: true },
]
/** 注销风险提示(设计稿 __4d03fd89 原文四条) */
const risks = [
'账号注销后,您将无法登录本应用,所有个人信息将被永久删除。',
'您名下的订单、合同、工单等业务数据将一并清除,无法找回。',
'注销后,同一手机号/邮箱将无法再次注册本应用。',
'如有未完成的订单或服务,请先处理完毕后再进行注销。',
]
function onDeleteConfirm(): void {
if (!agree.value) {
ElMessage.warning('请先阅读并同意《账号注销协议》')
return
}
// 安全身份验证 → 验证通过后注销
identityVisible.value = true
}
// ── 修改密码 ── // ── 修改密码 ──
const pwdVisible = ref(false) const pwdVisible = ref(false)
@@ -66,14 +96,6 @@ function onUnbindConfirm(): void {
} }
// ── 注销账号 ── // ── 注销账号 ──
const deleteVisible = ref(false)
const identityVisible = ref(false)
function onDeleteConfirm(): void {
// 确认注销 → 进入安全身份验证
identityVisible.value = true
}
function onIdentitySubmit(payload: { contact: string; code: string }): void { function onIdentitySubmit(payload: { contact: string; code: string }): void {
void (async () => { void (async () => {
await profileStore.verifyIdentity(payload.contact, payload.code) await profileStore.verifyIdentity(payload.contact, payload.code)
@@ -96,6 +118,59 @@ function onIdentitySubmit(payload: { contact: string; code: string }): void {
@delete-account="deleteVisible = true" @delete-account="deleteVisible = true"
/> />
<!-- 注销账号 · 二次确认(设计稿 __4d03fd89) -->
<ConfirmDialog
v-model:visible="deleteVisible"
title="注销账号"
confirm-text="注销账号"
cancel-text="暂不注销"
danger
@cancel="agree = false"
@confirm="onDeleteConfirm"
>
<div class="delete-confirm">
<span class="delete-confirm__icon" aria-hidden="true">i</span>
<h3 class="delete-confirm__title">
确定要注销账号吗?
</h3>
<div class="delete-confirm__block">
<h4 class="delete-confirm__block-title delete-confirm__block-title--risk">
⚠ 注销风险提示
</h4>
<ul class="delete-confirm__risks">
<li v-for="(item, index) in risks" :key="item">
{{ index + 1 }}、{{ item }}
</li>
</ul>
</div>
<div class="delete-confirm__block">
<h4 class="delete-confirm__block-title delete-confirm__block-title--ok">
✓ 注销条件
</h4>
<ul class="delete-confirm__conditions">
<li
v-for="item in conditions"
:key="item.label"
class="delete-confirm__condition"
>
<span>{{ item.label }}</span>
<span
class="delete-confirm__mark"
:class="item.ok ? 'is-ok' : 'is-fail'"
>{{ item.ok ? '✓' : '✕' }}</span>
</li>
</ul>
</div>
<label class="delete-confirm__agree">
<input v-model="agree" type="checkbox">
<span>已阅读并同意<em>《账号注销协议》</em></span>
</label>
</div>
</ConfirmDialog>
<ChangePasswordDialog <ChangePasswordDialog
v-model:visible="pwdVisible" v-model:visible="pwdVisible"
:phone="profile?.phone ?? ''" :phone="profile?.phone ?? ''"
@@ -126,22 +201,128 @@ function onIdentitySubmit(payload: { contact: string; code: string }): void {
danger danger
@confirm="onUnbindConfirm" @confirm="onUnbindConfirm"
/> />
<ConfirmDialog
v-model:visible="deleteVisible"
title="注销账号"
message="确定要注销账号吗?"
confirm-text="注销账号"
cancel-text="暂不注销"
danger
@confirm="onDeleteConfirm"
/>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.security-page { .security-page {
.flex(column, flex-start); .flex(column, flex-start, stretch);
gap: @spacing-base; gap: @spacing-base;
} }
// 设计稿(注销账号 __4d03fd89):
// 弹层内容列 x660→1420 宽 760;感叹号 30/700 #EF4444;主问 19/700 #1F2937
// 风险标题 13/600 #EF4444;正文 13/400 #4B5563 行距 26
// 条件行 760×32 底 #FFFFFF bd #DDE1E7;勾选标记 14 圆底
// 协议勾选框 16;取消按钮 760×46 底 #D8DBDF;注销按钮 760×46 底 #EF4444
.delete-confirm {
.flex(column, flex-start);
align-items: center;
padding: 8px 0;
&__icon {
.flex(row, center);
width: 34px;
height: 34px;
border-radius: @radius-round;
background: fade(@danger-color, 12%);
color: @danger-color;
font-size: 22px;
font-weight: 700;
line-height: 1;
}
&__title {
margin-top: 10px;
font-size: 19px;
font-weight: 700;
color: @profile-ink;
}
&__block {
width: 100%;
margin-top: 32px;
}
&__block-title {
font-size: 13px;
font-weight: 600;
&--risk {
color: @danger-color;
}
&--ok {
color: @success-color;
}
}
&__risks {
margin-top: 12px;
.flex(column, flex-start);
gap: 9px;
list-style: none;
font-size: 13px;
color: @finance-entry-text;
}
&__conditions {
margin-top: 12px;
.flex(column, flex-start);
gap: 10px;
list-style: none;
}
&__condition {
.flex(row, space-between, center);
width: 100%;
height: 32px;
padding: 0 16px;
border: 1px solid @border-color;
border-radius: @radius-sm;
background: @white;
font-size: 13px;
color: @finance-entry-text;
box-sizing: border-box;
}
&__mark {
.flex(row, center);
width: 14px;
height: 14px;
border-radius: @radius-round;
color: @white;
font-size: 10px;
line-height: 1;
&.is-ok {
background: @success-color;
}
&.is-fail {
background: @danger-color;
}
}
&__agree {
.flex(row, flex-start, center);
gap: 8px;
width: 100%;
margin-top: 22px;
font-size: @font-size-sm;
color: @finance-muted-text;
cursor: pointer;
input {
width: 16px;
height: 16px;
accent-color: @primary-strong;
}
em {
font-style: normal;
color: @primary-strong;
}
}
}
</style> </style>

View File

@@ -342,65 +342,80 @@ onMounted(() => {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1,规格 项目详情__4ed7159e):
// 面包屑 (70,116) 24/500 #666666;主标题 (70,196) 22/700 #1F2937;副标题 (66,233) 13/400 #6B7280;
// 步骤标题 (126,299) 15/700,序号圆 (94,294) 21×21 #024FFF 12/700 白字;
// 字段标签 (87.5,336) 13/500、输入框 (94,361) 1732×39;文本域 (94,623) 1732×130;
// 附件区 (70,456) 1780×499 白底、文件行 (94,813) 1732×55 #F7F8FA;
// 底栏 (0,1238) 1920×80 + 顶部分隔线 (0,1238) 1px #F0F1F3;按钮 (1409,1258) 254×38 底 #075CFF。
.after-sales { .after-sales {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base; padding-bottom: 104px; // 固定底栏 80 + 呼吸 24
padding-top: @spacing-lg;
padding-bottom: @spacing-xxl;
// 设计 12:整页白底(蓝湖实测页面底 #FFFFFF)
background: @white; background: @white;
// 面包屑(设计 24px × 0.77 ≈ 18px,#666666,字重 500) // 设计 ink 116(页头 72 + 44);行高 27 → 盒顶 ≈113
&__breadcrumb { &__breadcrumb {
font-size: 18px; padding-top: 41px;
font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 27px;
color: @project-breadcrumb; color: @project-breadcrumb;
} }
// 面包屑盒底 140 → 主标题盒顶 192
&__header { &__header {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 6px; padding-top: 52px;
padding: 16px 0 0;
} }
// 设计 12:主标题 22 设计px × 0.77 ≈ 17px / 700 // 主标题 22/700,行高 29
&__title { &__title {
font-size: 17px; margin: 0;
font-size: 22px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 29px;
color: @project-ink;
} }
// 设计 12:副标题 13 设计px × 0.77 ≈ 11px、整行居中 // 副标题 13/400,行高 17(顶 233 = 标题底 225 + 8)
&__subtitle { &__subtitle {
font-size: 11px; margin-top: 8px;
text-align: center; font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-label;
} }
// ── 三段卡片(设计 12:白底 + 1px #F0F1F3 描边 + 圆角,无阴影) ── // 卡内上下留白:卡1 顶 276 → 序号圆 298(22);select 底 400 → 卡1 底 440(40)
&__card { &__card {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base; margin-top: 30px;
padding: 20px 24px; padding: 22px 0 40px;
border: 1px solid @project-divider;
border-radius: @radius-lg;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }
// 设计 12:卡片标题 15 设计px × 0.77 ≈ 12px / 700 + 圆形序号 // 卡1 底 440 → 卡2 顶 456(16);卡2 底 955 → 卡3 顶 971(16)
&__card-title { &__card + &__card {
.flex(row, flex-start, center); margin-top: 16px;
gap: 12px; }
font-size: 12px;
font-weight: 700; // 卡片标题:(126,299) 15/700 + 序号圆 (94,294) 21×21
color: @heading-color; &__card-title {
.flex(row, flex-start, center);
gap: 11px; // 126 − (94+21) = 11
padding-left: 24px; // 序号落 x94 = 内容区 70 + 24(01 段)
font-size: 15px;
font-weight: 700;
line-height: 20px;
color: @project-ink;
} }
// 设计 12:序号圆 ≈26 设计px × 0.77、蓝底白字 12px / 700
&__step { &__step {
.flex(row, center); .flex(row, center, center);
width: 20px; flex-shrink: 0;
height: 20px; width: 21px;
height: 21px;
border-radius: @radius-round; border-radius: @radius-round;
background: @project-accent; background: @project-accent;
color: @white; color: @white;
@@ -409,112 +424,129 @@ onMounted(() => {
line-height: 1; line-height: 1;
} }
// 01 段:标题底 319 → 首个字段标签 336(+17);02/03 段:标题在 (126,479)/(118.5,994)
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 4px;
position: relative; position: relative;
gap: 8px; // 标签底 353 → 框顶 361
}
&__field:first-of-type {
margin-top: 17px;
}
&__field + &__field {
margin-top: 19px; // 框底 400 → 下一标签 419(02 段)
}
// 02 段「项目名称」框底 580 → 「问题描述」标签 598
&__field + &__field:has(+ &__field) {
margin-top: 19px;
} }
// 设计 12:标签与提示同行,间距 14 设计px × 0.77
&__field-head { &__field-head {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 11px; gap: 14px;
} }
// 设计 12:提示 11 设计px #9CA3AF
&__field-hint { &__field-hint {
font-size: 11px; font-size: 11px;
color: @text-color-disabled; line-height: 15px;
color: @project-muted;
} }
// 「上传文件」推到行尾(设计 12:浅灰底按钮) // 「上传文件」推到行尾
&__upload-btn { &__upload-btn {
margin-left: auto; margin-left: auto;
} }
&__field-head :deep(.after-sales__upload-btn) { &__field-head :deep(.after-sales__upload-btn) {
height: 28px; height: 21px;
padding: 0 7px;
border: none; border: none;
border-radius: 6px; border-radius: 4px;
background: @project-quiet-btn; background: @project-field-bg;
color: @text-color; color: @project-cell;
font-size: 12px; font-size: 12px;
font-weight: 500;
line-height: 21px;
&:hover { &:hover {
border-color: transparent; border-color: transparent;
background: @project-quiet-btn; background: @project-field-bg;
color: @text-color; color: @project-cell;
} }
} }
// 设计 12:字段标签整行**居中**,13 设计px × 0.77 ≈ 11px / 500 // 字段标签 13/500,整行铺满(落到内容区 x70)
&__label { &__label {
font-size: 11px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: @heading-color; line-height: 17px;
text-align: center; color: @project-ink;
text-align: left;
} }
// 设计 12:星号与标签同色同字重(标注层整串同色)
&__required { &__required {
margin-left: 2px; margin-left: 2px;
color: inherit; color: inherit;
font-weight: inherit; font-weight: inherit;
} }
// 设计 12:「售后类型」白底描边,「项目名称」灰底只读态(--filled) // 输入框 / 下拉:设计 1732×39、白底描边 #DDE1E7、圆角 8
&__select { &__select {
width: 100%; width: 100%;
height: 36px; height: 39px;
padding: 0 36px 0 12px; padding: 0 13px;
border: 1px solid @border-color; border: 1px solid @project-input-border;
border-radius: 6px; border-radius: 8px;
background-color: @white; background-color: @white;
color: @text-color; color: @project-ink;
font-size: 11px; font-size: 13px;
line-height: 17px;
appearance: none; appearance: none;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
// 自定义下拉箭头(原生 select 不支持 ::after) box-sizing: border-box;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.5 6 8.5l4-4' fill='none' stroke='%238c8c8c' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.5 6 8.5l4-4' fill='none' stroke='%238c8c8c' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 12px center; background-position: right 13px center;
background-size: 12px 12px; background-size: 12px 12px;
transition: border-color 0.2s; transition: border-color 0.2s;
&::placeholder { &::placeholder {
color: @text-color-disabled; color: @project-muted;
} }
&:focus { &:focus {
border-color: @primary-color; border-color: @primary-color;
} }
// 设计 12:项目名称 = 灰底填充(#F3F4F6),无描边 // 设计:项目名称 = 灰底填充(#F3F4F6),无描边
&--filled { &--filled {
border-color: transparent; border-color: transparent;
background-color: @input-bg; background-color: @project-field-bg;
} }
} }
// 「项目名称」与「售后类型」共用同一个下拉样式(见上方 &__select) // 文本域:设计 1732×130、白底描边 #DDE1E7、圆角 8
&__textarea { &__textarea {
width: 100%; width: 100%;
height: 104px; height: 130px;
padding: 10px 12px; padding: 10px 13px;
border: 1px solid @border-color; border: 1px solid @project-input-border;
border-radius: 6px; border-radius: 8px;
background: @white; background: @white;
color: @text-color; color: @project-ink;
font-size: 11px; font-size: 13px;
line-height: 1.6; line-height: 20px;
resize: vertical; resize: vertical;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
transition: border-color 0.2s; transition: border-color 0.2s;
&::placeholder { &::placeholder {
color: @text-color-disabled; color: @project-muted;
} }
&:focus { &:focus {
@@ -522,77 +554,87 @@ onMounted(() => {
} }
} }
// 设计 12:字数统计在文本域**内部右下角** // 字数统计在文本域内部右下角(设计 (1770.5,726) 11/400)
&__count { &__count {
position: absolute; position: absolute;
right: 12px; right: 13px;
bottom: 8px; bottom: 8px;
font-size: 11px; font-size: 11px;
color: @text-color-disabled; line-height: 15px;
color: @project-muted;
} }
&__file-input { &__file-input {
display: none; display: none;
} }
// ── 底部固定操作条(设计 12:顶部分隔线 #F0F1F3、白底、左提示右三键) ── // ── 底部固定操作条(设计 1920×80、白底、顶部分隔线 #F0F1F3) ──
&__footer { &__footer {
position: sticky; position: fixed;
right: 0;
bottom: 0; bottom: 0;
left: 0;
z-index: 10; z-index: 10;
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
margin-top: 8px; height: 80px;
padding: 12px 0; box-sizing: border-box;
padding: 0 121px 0 170px; // 提示 x170、按钮右缘 x1791
border-top: 1px solid @project-divider; border-top: 1px solid @project-divider;
background: @white; background: @white;
} }
// 底栏提示:(93.99,1266) 12/400 #4B5563 + i 图标 (76.66,1267)
&__footer-tip { &__footer-tip {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 6px; gap: 8px;
font-size: 12px; font-size: 12px;
color: @text-color; font-weight: 400;
line-height: 16px;
color: @project-cell;
} }
&__footer-icon { &__footer-icon {
.flex(row, center); .flex(row, center, center);
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 14px;
height: 16px; height: 14px;
border: 1px solid currentColor; border: 1px solid currentColor;
border-radius: @radius-round; border-radius: @radius-round;
font-size: 11px; font-size: 10px;
font-style: normal; font-style: normal;
line-height: 1; line-height: 1;
} }
&__footer-actions { &__footer-actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 12px; gap: 8px;
flex-shrink: 0; flex-shrink: 0;
// 设计 12:取消 / 重置 = 浅灰底灰字,申请售后 = #024FFF 蓝底白字 // 设计:三键同高 38、圆角 8、字号 13/500
:deep(.app-button) { :deep(.app-button) {
height: 32px; height: 38px;
border-radius: 6px; padding: 0 18px;
font-size: 11px; border-radius: 8px;
font-size: 13px;
font-weight: 500;
line-height: 18px;
} }
:deep(.app-button--default) { :deep(.app-button--default) {
border: none; border: none;
background: @project-quiet-btn; background: @project-quiet-btn;
color: @text-color; color: @project-cell;
&:hover { &:hover {
border-color: transparent; border-color: transparent;
background: @project-quiet-btn; background: @project-quiet-btn;
color: @text-color; color: @project-cell;
} }
} }
:deep(.app-button--primary) { :deep(.app-button--primary) {
background: @project-accent; background: @demand-secondary-bg;
&:hover { &:hover {
background: @primary-color; background: @primary-color;
@@ -600,36 +642,39 @@ onMounted(() => {
} }
} }
// 设计 12:附件行 = #F7F8FA 圆角行、灰字操作(公共 FileList,仅本页覆写外观) // 附件行:设计 (94,813) 1732×55、底 #F7F8FA、圆角 8
:deep(.file-list) { :deep(.file-list) {
gap: 12px; gap: 10px;
} }
:deep(.file-list__item) { :deep(.file-list__item) {
padding: 6px 14px; height: 55px;
box-sizing: border-box;
padding: 0 5.7px 0 16px;
border: none; border: none;
border-radius: 6px; border-radius: 8px;
background: @finance-row-bg; background: @finance-row-bg;
} }
:deep(.file-list__name) { :deep(.file-list__name) {
font-size: 11px; font-size: 13px;
line-height: 15px; line-height: 17px;
} }
:deep(.file-list__meta) { :deep(.file-list__meta) {
font-size: 11px; font-size: 11px;
line-height: 15px; line-height: 15px;
color: @text-color-disabled; color: @project-muted;
} }
:deep(.file-list__actions) { :deep(.file-list__actions) {
gap: 12px; gap: 14px;
} }
:deep(.file-list__btn) { :deep(.file-list__btn) {
font-size: 12px; font-size: 12px;
color: @text-color-secondary; line-height: 16px;
color: @project-label;
} }
:deep(.file-list__btn--danger) { :deep(.file-list__btn--danger) {

View File

@@ -153,94 +153,133 @@ function onDeleteFile(): void {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1):横幅 (70,342) 1780×96 底 #FDEEEE;
// 标题 (96,367.5) 17/700 #D93025;说明 (96,395.5) 13/400 #4B5563;
// 区块标题 (82,468) 15/700;字段标签 (63.5,508) 13/500 #1F2937(整行铺满 → 实际落 x70);
// 只读框 (70,533) 1780×39 底 #F3F4F6 描边 #DDE1E7;文本域 (70,615) 1780×130;
// 附件标题 (70,768.5) 13/500 + 提示 (149,769.5) 11/400 #9CA3AF + 「上传文档」(1801.4,769) 12/500;
// 文件行 (70,805) 1780×55 #F7F8FA;操作 (1628,968)/(1754,968) 15/500 白字 —— 底部按钮。
.project-after-sales { .project-after-sales {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base;
// 设计 10:售后信息区块直接落在白底页面上,无外层卡片
padding: 0; padding: 0;
// ── 顶部工单横幅:浅红底、无图标块(设计 10 实取 #FDEEEE) ── // ── 顶部工单横幅 ──
// 页签分割线 317 → 横幅顶 342(25)
&__banner { &__banner {
.flex(row, flex-start, center); .flex(row, flex-start, center);
margin-bottom: 16px; height: 96px;
padding: 12px 20px; box-sizing: border-box;
border-radius: 6px; margin-top: 25px;
background: @project-banner-bg; padding-left: 26px; // 文字 x96 = 内容区 x70 + 26
border-radius: 0;
background: @project-banner-soft;
} }
&__banner-text { &__banner-text {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 6px;
min-width: 0; min-width: 0;
} }
// 设计 10:标题 17 设计px × 0.77 ≈ 13px / 700 红 // 横幅标题 17/700,行高 22(顶 367.5 = 横幅 342 + 25.5)
&__banner-title { &__banner-title {
font-size: 13px; margin: 0;
padding-top: 0;
font-size: 17px;
font-weight: 700; font-weight: 700;
line-height: 22px;
color: @project-banner-title; color: @project-banner-title;
line-height: 1.3;
} }
// 设计 10:说明 13 设计px × 0.77 ≈ 11px #4B5563 // 说明 13/400(顶 395.5 = 标题底 389.5 + 6)
&__banner-desc { &__banner-desc {
font-size: 11px; margin-top: 6px;
color: @text-color; font-size: 13px;
font-weight: 400;
line-height: 17px;
color: @project-cell;
} }
// ── 「售后信息」区块标题:竖条 + 缩进(与其它页签一致) ── // ── 「售后信息」区块标题 ──
&__title { &__title {
.section-title(); .section-title();
margin-top: 0; margin-top: 0;
padding-left: 9px; padding-left: 12px; // 文字落 x82
font-size: 12px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px;
&::before { &::before {
width: 3px; width: 4px;
height: 15px; height: 20px;
} }
} }
// 横幅底 438 → 标题 468;标题底 488 → 字段。
// 后代表达式:紧邻横幅的标题额外上抬 —— 用统一节距表达(见下方 padding)
&__banner + &__title {
margin-top: 30px;
}
// 标题盒底 484.5 → 首标签盒顶 505(20.5)
&__title + &__field {
margin-top: 20.5px;
}
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 6px; gap: 5px; // 标签盒底 528 → 只读框顶 533
} }
// 设计 10:标签与提示同行,间距 14 设计px × 0.77 // 框底 → 下一标签盒顶:ro1 底 572 → 标签2 586.5(14.5);ro2 底 745 → 标签3 765(20)
&__field + &__field {
margin-top: 14.5px;
}
// 第三个字段(附件)上距略大:设计 ro2 底 745 → 标签3 768.5
// 注意:字段在 section 里是第 3/4/5 个子元素(前面还有 banner、title),所以是 nth-child(5)
&__field:nth-child(5) {
margin-top: 23px;
gap: 20px; // 标签行盒底 785 → 首行附件 805
}
// 设计:标签与提示同行
&__field-head { &__field-head {
.flex(row, flex-start, baseline); .flex(row, flex-start, center);
gap: 11px; gap: 14px; // 149 − (70+65) = 14
} }
// 设计 10:提示 11 设计px #9CA3AF
&__field-hint { &__field-hint {
font-size: 11px; font-size: 11px;
color: @text-color-disabled; line-height: 15px;
color: @project-muted;
} }
// 设计 10:字段标签 13 设计px × 0.77 ≈ 11px / 500 // 字段标签 13/500
&__label { &__label {
font-size: 11px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: @heading-color; line-height: 20px;
color: @project-ink;
} }
// 只读展示(设计 10:灰底圆角实底框,无描边) // 只读展示:设计 1780×39、底 #F3F4F6、描边 1px #DDE1E7、圆角 8
&__readonly { &__readonly {
min-height: 36px; .flex(row, flex-start, center);
padding: 0 12px; min-height: 39px;
border: none; padding: 0 13px; // 值 x83 = 内容区 70 + 13
border-radius: 6px; border: 1px solid @project-input-border;
background: @input-bg; border-radius: 8px;
color: @text-color; background: @project-field-bg;
font-size: 11px; color: @project-cell;
line-height: 36px; font-size: 13px;
line-height: 17px;
box-sizing: border-box;
&--block { &--block {
min-height: 104px; display: block;
padding: 12px; min-height: 130px;
line-height: 1.7; padding: 10px 13px;
line-height: 20px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
@@ -248,7 +287,7 @@ function onDeleteFile(): void {
&__none { &__none {
padding: @spacing-sm 0; padding: @spacing-sm 0;
font-size: @font-size-sm; font-size: 13px;
} }
&__blank { &__blank {
@@ -257,36 +296,36 @@ function onDeleteFile(): void {
} }
&__blank-text { &__blank-text {
font-size: @font-size-base; font-size: @font-size-body;
} }
// 设计 10:按钮组贴右,间距 34 设计px × 0.77 // 末行附件 925(870+55)→ 按钮盒顶 955(30)
&__actions { &__actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 24px; gap: 42px; // 1754 − (1628+84) = 42
margin-top: 8px; margin-top: 30px;
} }
// 设计 10:高 ≈36 设计px × 0.77、圆角 ≈8 设计px、文字 15 设计px × 0.77
&__btn { &__btn {
height: 28px; height: 38px;
padding: 0 14px; padding: 0 18px;
border-radius: 6px; border-radius: 8px;
font-size: 12px; font-size: 15px;
font-weight: 500;
line-height: 20px;
cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
// 「取消售后」= 品牌蓝(设计 10 实取 #1F67EF)
&--primary { &--primary {
border: none; border: none;
background: @primary-color; background: @project-accent;
color: @white; color: @white;
&:hover { &:hover {
background: @primary-hover; background: @primary-color;
} }
} }
// 「申请售后」= 红(设计 10 实取 #E3464B)
&--apply { &--apply {
border: none; border: none;
background: @project-apply; background: @project-apply;
@@ -298,36 +337,39 @@ function onDeleteFile(): void {
} }
} }
// 设计 10:附件行 = #F7F8FA 圆角行、灰字操作(公共 FileList,仅本页覆写外观) // 附件行:公共 FileList 外观覆写(设计 1780×55、底 #F7F8FA、圆角 8)
:deep(.file-list) { :deep(.file-list) {
gap: 12px; gap: 10px;
} }
:deep(.file-list__item) { :deep(.file-list__item) {
padding: 6px 14px; height: 55px;
box-sizing: border-box;
padding: 0 5.7px 0 16px;
border: none; border: none;
border-radius: 6px; border-radius: 8px;
background: @finance-row-bg; background: @finance-row-bg;
} }
:deep(.file-list__name) { :deep(.file-list__name) {
font-size: 11px; font-size: 13px;
line-height: 15px; line-height: 17px;
} }
:deep(.file-list__meta) { :deep(.file-list__meta) {
font-size: 11px; font-size: 11px;
line-height: 15px; line-height: 15px;
color: @text-color-disabled; color: @project-muted;
} }
:deep(.file-list__actions) { :deep(.file-list__actions) {
gap: 12px; gap: 14px;
} }
:deep(.file-list__btn) { :deep(.file-list__btn) {
font-size: 12px; font-size: 12px;
color: @text-color-secondary; line-height: 16px;
color: @project-label;
} }
:deep(.file-list__btn--danger) { :deep(.file-list__btn--danger) {

View File

@@ -18,6 +18,8 @@ const emit = defineEmits<{
<template> <template>
<section class="project-files"> <section class="project-files">
<!-- 设计稿(785b3eb8)本页签**没有区块标题**,文件行直接起于分割线下方 24px;
标题只保留给读屏,视觉隐藏。 -->
<h2 class="project-files__title"> <h2 class="project-files__title">
项目文件 项目文件
</h2> </h2>
@@ -57,72 +59,73 @@ const emit = defineEmits<{
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1):无区块标题,文件行直接起于 (70,342);
// 行 1780×55 底 #F7F8FA、接缝 10;类型徽标 (87,352) 33×21;文件名 (130,352) 13px;
// 副行 (130,372) 11px #9CA3AF;操作 (1748,361) 12px #6B7280(预览 / 下载),右缘 x1845。
.project-files { .project-files {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base;
// 设计 11:项目文件区块直接落在白底页面上,无卡片
padding: 0; padding: 0;
// 设计稿无可见标题:仅读屏可读(不占位)
&__title { &__title {
.section-title(); position: absolute;
// 设计 11:竖条 4×20 设计px、文字 15 设计px/700 width: 1px;
padding-left: 9px; height: 1px;
font-size: 12px; margin: -1px;
font-weight: 700; padding: 0;
overflow: hidden;
&::before { clip-path: inset(50%);
width: 3px; white-space: nowrap;
height: 15px;
}
} }
&__list { &__list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 设计 11:行间距 15 设计px × 0.77 gap: 10px;
gap: 12px; margin-top: 24px; // 分割线 317 → 首行顶 342(785b3eb8)
} }
// 设计 11:整行通铺 #F7F8FA 浅底、圆角 ≈8 设计px,无描边 // 整行通铺 #F7F8FA、圆角 8,行高 55(内容 35 + 上下 10)
&__item { &__item {
.flex(row, space-between); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
padding: 6px 16px; height: 55px;
border: none; box-sizing: border-box;
border-radius: 6px; padding: 0 5.6px 0 17px; // 徽标左缘 87 = 内容区 70 + 17;操作右缘 1845 = 1850 − 5
border-radius: 8px;
background: @finance-row-bg; background: @finance-row-bg;
} }
&__info { &__info {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 2px; gap: 3px;
min-width: 0; min-width: 0;
} }
// 设计 11:文件名 13 设计px、副行 11 设计px #9CA3AF
&__name { &__name {
font-size: 11px; max-width: 1410px;
line-height: 15px; font-size: 13px;
color: @heading-color; line-height: 17px;
color: @project-ink;
.ellipsis(); .ellipsis();
} }
&__meta { &__meta {
font-size: 11px; font-size: 11px;
line-height: 15px; line-height: 15px;
color: @text-color-disabled; color: @project-muted;
} }
&__actions { &__actions {
.flex(row, center); .flex(row, flex-end, center);
// 设计 11:「预览 / 下载」间距 14 设计px × 0.77 gap: 14px; // 1748.39 → 1805.33 实测节距 57/40 → 用文字宽 + 14
gap: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
// 设计 11:行内操作是灰色纯文字(非蓝色),删除保持警示红
&__btn { &__btn {
flex-shrink: 0;
font-size: 12px; font-size: 12px;
color: @text-color-secondary; line-height: 16px;
color: @project-label;
transition: opacity 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
@@ -135,7 +138,7 @@ const emit = defineEmits<{
} }
&__empty { &__empty {
font-size: @font-size-base; font-size: @font-size-body;
} }
} }
</style> </style>

View File

@@ -165,54 +165,84 @@ function fileMeta(file: FileInfo): string {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1):标题 (82,342) 15/700;计划卡 (70,380) 372×139 / (70,533) 372×144 /
// (70,691) 372×116,描边 1px #F0F1F3、圆角 13;期次名 (89,398) 14/700;状态 (378,400) 11/500;
// 字段行 (89,426) 11/400 #6B7280;金额 (341.33,453) 20/700;「立即支付」通栏 372×34 #F59E0B;
// 发票记录标题 (82,841)、合同文件标题 (82,1033);文件行 1780×55 #F7F8FA。
.project-finance { .project-finance {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base;
// 设计 09:合同财务区块直接落在白底页面上,无外层卡片
padding: 0; padding: 0;
// 区块标题:与「项目进度/项目文件/售后信息」同款(12px/700 + 左侧蓝色竖条) // 分割线 317 → 「付款计划」盒 339(ink 342,843103b2)
&__title { &__title {
.section-title(); .section-title();
padding-left: 9px; margin-top: 22px;
font-size: 12px; padding-left: 12px;
font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px;
&::before { &::before {
width: 3px; width: 4px;
height: 15px; height: 20px;
} }
} }
// 只读回落态(后端未重启、响应无 bills)的说明行 // 首区块标题盒底 359 → 首张卡顶 380
&__note { &__title:first-child + &__plans {
font-size: @font-size-sm; margin-top: 21px;
}
// 区块间距:付款计划卡底 807 → 「发票记录」盒 838(31);发票行底 999 → 「合同文件」盒 1030(31)
&__plans + &__title,
&__files + &__title {
margin-top: 31px;
}
// 区块标题盒底 → 首行顶:838+20 → 879(21);1030+20 → 1071(21)
&__title + &__files,
&__title + &__file {
margin-top: 21px;
} }
// ---------- 付款计划 ---------- // ---------- 付款计划 ----------
// 设计稿:卡片**竖排单列、定宽左对齐**(不是横排多列网格) // 卡片竖排单列、定宽 372 左对齐;卡间距 14(380→533→691 实测 153/158,扣卡高)
&__plans { &__plans {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
// 设计 09:卡片间距 13 设计px × 0.77 gap: 14px;
gap: 10px;
} }
// 设计 09:白底 + 1px #F0F1F3 描边、圆角 ≈13 设计px,定宽竖排 // 白底 + 1px #F0F1F3 描边、圆角 13,定宽 372;内容内缩 19/17
&__plan { &__plan {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-sm; position: relative;
width: @finance-card-width; width: 372px;
padding: 14px 15px; padding: 17px 19px 17px;
border: 1px solid @project-divider; border: 1px solid @project-divider;
border-radius: @finance-card-radius; border-radius: 13px;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
gap: 0; // 段间距全部走显式 margin,避免 gap 与 margin 叠加
// 带「立即支付」时按钮贴卡片下缘:底部不留内边距
&:has(.project-finance__pay) {
padding-bottom: 0;
}
// 两条字段、无支付按钮(卡3):底距 17(设计卡高 116 = 2+17+19+8+53+17)
&:not(:has(.project-finance__field:nth-child(3))):not(:has(.project-finance__pay)) {
padding-bottom: 17px;
}
// 三条字段、无支付按钮(卡1):底距 15(设计卡高 139 = 2+17+19+8+78+15)
&:has(.project-finance__field:nth-child(3)):not(:has(.project-finance__pay)) {
padding-bottom: 15px;
}
// 三态配色(浅底徽标 + 同色系金额) // 三态配色(浅底徽标 + 同色系金额)
&.is-paid { &.is-paid {
.project-finance__badge { .project-finance__badge {
background: @tag-success-bg;
color: @finance-paid; color: @finance-paid;
} }
@@ -223,7 +253,6 @@ function fileMeta(file: FileInfo): string {
&.is-pending { &.is-pending {
.project-finance__badge { .project-finance__badge {
background: @tag-warning-bg;
color: @tag-warning-text; color: @tag-warning-text;
} }
@@ -234,8 +263,7 @@ function fileMeta(file: FileInfo): string {
&.is-idle { &.is-idle {
.project-finance__badge { .project-finance__badge {
background: @tag-info-bg; color: @project-label;
color: @text-color-secondary;
} }
.project-finance__amount { .project-finance__amount {
@@ -249,61 +277,90 @@ function fileMeta(file: FileInfo): string {
gap: @spacing-sm; gap: @spacing-sm;
} }
// 设计 09:期次标题 14 设计px × 0.77 ≈ 11px / 700 // 期次标题 14/700,行高 19(顶 398 = 卡内 380+18)
&__plan-name { &__plan-name {
font-size: 11px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: @heading-color; line-height: 19px;
color: @project-ink;
.ellipsis(); .ellipsis();
} }
// 状态徽标:浅底彩色字(设计 09:约 57×22 设计px、圆角 4 设计px、11px 字) // 状态纯文字(设计 11/500)
&__badge { &__badge {
.flex(row, center, center); .flex(row, center, center);
flex-shrink: 0; flex-shrink: 0;
padding: 3px 10px; margin-top: 2px;
border-radius: @radius-sm;
font-size: 11px; font-size: 11px;
line-height: 16px; line-height: 15px;
white-space: nowrap; white-space: nowrap;
} }
// 金额是**文本覆盖层**,不参与卡高:设计里它压在字段区右侧、与首字段行齐平
// (ink 453 = 卡顶 380 + 73;字段盒顶 424 + 29)。必须 absolute,否则 26 高的
// 行盒会把 __plan-body 撑高,三张卡分别虚高 8/8/0。
&__plan-body { &__plan-body {
.flex(row, space-between, center); .flex(row, space-between, flex-start);
position: relative;
margin-top: 8px; // 期次行盒底 416 → 字段块顶 424
gap: @spacing-base; gap: @spacing-base;
} }
// 字段块按内容定高(3 行 78 / 2 行 53),行距由绝对偏移给,避免 margin 叠加。
// 设计:卡1 424→502(3 行 0/23/61),卡2 564→617、卡3 729→782(2 行 0/23)。
&__fields { &__fields {
.flex(column, flex-start, stretch); position: relative;
gap: 4px; flex: 0 0 auto;
min-width: 0; min-width: 0;
// 3 行时块高 78,2 行时 53
height: 53px;
&:has(> :nth-child(3)) {
height: 78px;
}
} }
&__field { &__field {
position: absolute;
left: 0;
right: 0;
font-size: 11px; font-size: 11px;
line-height: 16px; line-height: 15px;
color: @finance-meta-color; color: @project-label;
.ellipsis(); .ellipsis();
// 字段行距:426 → 449 → 487(相对块顶 0 / 23 / 61)
&:nth-child(1) { top: 0; }
&:nth-child(2) { top: 23px; }
&:nth-child(3) { top: 61px; }
} }
// 金额:设计稿在卡片**右侧**、字号最大且随状态变色(20 设计px × 0.77 ≈ 15px) // 金额 20/700:设计 ink 453(卡顶 380 + 73),相对首字段行盒顶(424)再下移 29
&__amount { &__amount {
position: absolute;
top: 29px;
right: 0;
flex-shrink: 0; flex-shrink: 0;
font-size: 15px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 22px; line-height: 26px;
white-space: nowrap; white-space: nowrap;
} }
// 「立即支付」:设计稿是卡内**满宽**金色按钮,通铺到卡片左右下缘 // 「立即支付」:设计卡内通铺 370×34、贴卡片下缘。
// 卡2:字段块底 617 → 按钮顶 642(间距 25),按钮底 676,卡底 677。
&__pay { &__pay {
height: 28px; height: 34px;
margin: 0 -15px -14px; margin: 11px -19px 0;
border: none; border: none;
border-radius: 0 0 @finance-card-radius @finance-card-radius; border-radius: 0 0 13px 13px;
background: @finance-pending; background: @finance-pending;
color: @white; color: @white;
font-size: 12px; font-size: 12px;
line-height: 34px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
@@ -314,31 +371,34 @@ function fileMeta(file: FileInfo): string {
// ---------- 发票 / 合同文件 ---------- // ---------- 发票 / 合同文件 ----------
// 发票行间距 10(879→944 / 1071→1136)
&__files { &__files {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 12px; gap: 10px;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
// 设计稿:整行通铺 #F7F8FA 浅底、圆角 ≈8 设计px(区别于卡片) // 整行通铺 #F7F8FA、圆角 8,行高 55
&__file { &__file {
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-sm; gap: @spacing-base;
padding: 6px 14px; height: 55px;
border-radius: 6px; box-sizing: border-box;
padding: 0 5.7px 0 16px; // 徽标左缘 86 = 内容区 70 + 16;操作右缘 x1845
border-radius: 8px;
background: @finance-row-bg; background: @finance-row-bg;
} }
// 文件类型标识:按后缀着色的小方块(与需求提报页附件行同一套色板) // 文件类型方块(设计 ≈33×21)
&__file-badge { &__file-badge {
.flex(row, center, center); .flex(row, center, center);
flex-shrink: 0; flex-shrink: 0;
width: 34px; width: 33px;
height: 20px; height: 21px;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 11px; font-size: 9px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
@@ -346,35 +406,35 @@ function fileMeta(file: FileInfo): string {
&__file-info { &__file-info {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
gap: 2px; gap: 3px;
min-width: 0; min-width: 0;
} }
&__file-name { &__file-name {
font-size: 11px; max-width: 1410px;
line-height: 15px; font-size: 13px;
color: @heading-color; line-height: 17px;
color: @project-ink;
.ellipsis(); .ellipsis();
} }
&__file-meta { &__file-meta {
font-size: 11px; font-size: 11px;
line-height: 15px; line-height: 15px;
color: @text-color-disabled; color: @project-muted;
} }
&__file-actions { &__file-actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 12px; gap: 14px;
flex-shrink: 0; flex-shrink: 0;
} }
// 设计 09:行内操作是灰色纯文字(非蓝色)
&__act { &__act {
.flex(row, center, center); flex-shrink: 0;
gap: 4px;
font-size: 12px; font-size: 12px;
color: @text-color-secondary; line-height: 16px;
color: @project-label;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
@@ -384,7 +444,7 @@ function fileMeta(file: FileInfo): string {
} }
&__empty { &__empty {
font-size: @font-size-base; font-size: @font-size-body;
} }
} }
</style> </style>

View File

@@ -65,60 +65,58 @@ function businessTypeText(project: Project): string {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1):logo 方块 (70,196) 64×64、圆角 16;
// 标题 ink (142,212) 19/700 #1F2937;字段行 ink (142,245) 11px,标签 #6B7280 / 值 #1F2937;
// 状态 ink (1802,225.5) 16/700 #024FFF。整块 195..262(67 高),其后页签行 278 起(+16)。
.project-info-card { .project-info-card {
.flex(row, flex-start, center); .flex(row, flex-start, flex-start);
gap: 14px; gap: 8px; // 142 − (70 + 64) = 8
// 设计 09:标识 + 文字 + 右侧状态直接排在白底页面上,无卡片底/阴影/内边距 height: 67px;
padding: 0; box-sizing: border-box;
&__logo { &__logo {
.flex(row, center, center); .flex(row, center, center);
flex-shrink: 0; flex: 0 0 64px;
// 设计 09:方块 ≈56 设计px × 0.77,浅灰底、圆角 ≈12 设计px × 0.77 width: 64px;
width: 44px; height: 64px;
height: 44px; margin-top: 1px; // 196 − 195
border-radius: 12px; border-radius: 16px;
background: @project-logo-bg; background: @project-logo-bg;
} }
// 设计 09:方块内只有蓝色「</>」文字,无描边徽标框 // 方块内只有蓝色「</>」文字(设计 20/400 #024FFF)
&__logo-badge { &__logo-badge {
.flex(row, center, center);
width: auto;
height: auto;
border: none;
border-radius: 0;
color: @project-accent; color: @project-accent;
font-family: inherit; font-size: 20px;
font-size: 15px;
font-weight: 400; font-weight: 400;
letter-spacing: 0; line-height: 26px;
} }
&__main { &__main {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 6px;
} }
// 设计 09:主标题 19 设计px × 0.77 ≈ 15px / 700 // 标题 19/700、行盒 25:ink 212 → box 209(块顶 195 + 14)
&__name { &__name {
font-size: 15px; margin: 14px 0 0;
font-size: 19px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 25px;
color: @heading-color; color: @project-ink;
.ellipsis(); .ellipsis();
} }
// 设计稿:三个字段是**单行**「标签:值」,等距排布(11px 小字号不跟缩) // 字段行 11/15:ink 245 → box 243(标题盒底 234 + 9)
&__meta { &__meta {
.flex(row, flex-start, center); .flex(row, flex-start, center);
flex-wrap: wrap;
gap: 20px; gap: 20px;
margin-top: 9px;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
color: @heading-color; line-height: 15px;
color: @project-value;
} }
&__meta-item { &__meta-item {
@@ -127,16 +125,18 @@ function businessTypeText(project: Project): string {
} }
&__label { &__label {
color: @text-color-secondary; color: @project-label;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
} }
// 设计 09:右侧状态是纯文字,16 设计px × 0.77 ≈ 12px / 700 // 状态纯文字 16/700:ink 225.5 → box 222.5(块顶 195 + 27.5);右缘超出内容区 21.6
&__status { &__status {
flex-shrink: 0; flex-shrink: 0;
font-size: 12px; margin: 27.5px -21.6px 0 0;
font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 21px;
&.is-active { &.is-active {
color: @project-accent; color: @project-accent;
@@ -151,7 +151,7 @@ function businessTypeText(project: Project): string {
} }
&.is-idle { &.is-idle {
color: @text-color-secondary; color: @project-muted;
} }
} }
} }

View File

@@ -61,33 +61,36 @@ function linkDone(index: number): boolean {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 节点列宽 = 设计稿相邻节点中心距(356 设计px × 0.77 ≈ 274px) // 设计稿实测(1920,1:1;5865c6d9 −14 基准 ⇒ 统一后 +14):
@milestone-node-width: 274px; // 标题 ink 471 15/700;首列中心 x261(步距 356)、已完成实心点 26(x235..261,顶 y509);
@milestone-dot-size: 18px; // 连接线 356×3(y520);名称 ink 541 12;说明 ink 561 10;待开始线 #E5E7EB、已完成 #22C55E。
@milestone-dot: 26px;
@milestone-line-h: 3px;
.project-milestones { .project-milestones {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 设计 13:里程碑区块直接落在白底页面上,无卡片
padding: 0; padding: 0;
&__title { &__title {
.section-title(); .section-title();
// 设计 13:竖条 4×20 设计px、文字 15 设计px/700 margin-top: 35px; // 起止时间盒底 433 → 标题盒 468
padding-left: 9px; padding-left: 12px;
font-size: 12px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px;
&::before { &::before {
width: 3px; width: 4px;
height: 15px; height: 20px;
} }
} }
// 设计稿:轨迹从容器左缘起、**固定节距**左对齐(不铺满整宽) // 起止时间盒底 433 → 「项目进度(里程碑)」盒 468(ink 471)→ 圆点顶 509
&__track { &__track {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
margin-top: 16px; margin-top: 0;
padding-bottom: @spacing-xs; padding-top: 21px; // 标题盒底 488 → 圆点顶 509
padding-bottom: 0;
overflow-x: auto; overflow-x: auto;
.scrollbar(); .scrollbar();
} }
@@ -95,18 +98,18 @@ function linkDone(index: number): boolean {
&__node { &__node {
position: relative; position: relative;
.flex(column, flex-start, center); .flex(column, flex-start, center);
flex: 0 0 @milestone-node-width; flex: 0 0 @milestone-node;
min-width: 0; min-width: 0;
// 圆点之间的连接线:从本节点圆点右缘连到下一节点圆点左缘 // 连接线:y506(圆点顶 495 起第 11px),x248 → 604(下一节点 x426 起 178 宽)
&:not(:last-child)::after { &:not(:last-child)::after {
content: ''; content: '';
position: absolute; position: absolute;
top: (@milestone-dot-size / 2 - 1px); top: (@milestone-dot / 2 - @milestone-line-h / 2); // 13 − 1.5 = 11.5
left: calc(50% + @milestone-dot-size / 2); left: calc(50% + 13px); // 261 → 248 再 +13 = 当前中心 +13
width: calc(100% - @milestone-dot-size); width: @milestone-node; // 覆盖到下一节点左缘
height: 2px; height: @milestone-line-h;
background: @milestone-line; background: @milestone-ring;
} }
&.is-link-done:not(:last-child)::after { &.is-link-done:not(:last-child)::after {
@@ -118,17 +121,15 @@ function linkDone(index: number): boolean {
.flex(row, center, center); .flex(row, center, center);
position: relative; position: relative;
z-index: 1; z-index: 1;
width: @milestone-dot-size; width: @milestone-dot;
height: @milestone-dot-size; height: @milestone-dot;
border: 3px solid @milestone-idle; border: 3px solid @milestone-idle;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
// 设计 13:已完成圆点略小(20 设计px)、实心绿 // 已完成:实心绿点(设计 (235,495) 26×26,勾白 11)
.is-done & { .is-done & {
width: 16px;
height: 16px;
border-color: @milestone-done; border-color: @milestone-done;
background: @milestone-done; background: @milestone-done;
} }
@@ -140,17 +141,19 @@ function linkDone(index: number): boolean {
} }
&__check { &__check {
width: 10px; width: 11px;
height: 10px; height: 11px;
} }
// 圆点底 521 → 名称顶 527(12px 字 line-height 16)
&__name { &__name {
margin-top: @spacing-sm; margin-top: 6px;
max-width: 100%; max-width: calc(@milestone-node - 16px);
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
line-height: 16px;
text-align: center; text-align: center;
color: @text-color; color: @project-cell;
.multi-ellipsis(2); .multi-ellipsis(2);
.is-done & { .is-done & {
@@ -163,18 +166,20 @@ function linkDone(index: number): boolean {
} }
} }
// 名称底 543 → 说明顶 547(10px 字 line-height 14)
&__sub { &__sub {
margin-top: @spacing-xs; margin-top: 4px;
max-width: 100%; max-width: calc(@milestone-node - 16px);
font-size: 11px; font-size: 10px;
line-height: 14px;
text-align: center; text-align: center;
color: @text-color-disabled; color: @milestone-idle-text;
.multi-ellipsis(2); .multi-ellipsis(2);
} }
&__empty { &__empty {
margin-top: @spacing-base; margin-top: @spacing-base;
font-size: @font-size-base; font-size: @font-size-body;
} }
} }
</style> </style>

View File

@@ -44,45 +44,54 @@ function clampPercent(value: number): number {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920,1:1;5865c6d9 为 −14 基准,此处统一到主流基准 ⇒ 全部 +14):
// 「开发进度」ink 324 15/700;「总体进度」ink 372 12/400 #6B7280;「65%」ink 364 20/700 #024FFF;
// 进度条 (70,402) 1780×6、填充 1157 #024FFF、轨道 #EDEFF2;起止时间 ink 420 11/400 #9CA3AF。
.project-progress { .project-progress {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 设计 13:开发进度区块直接落在白底页面上,无卡片
padding: 0; padding: 0;
// 分割线 317 → 标题 box 321
&__title { &__title {
.section-title(); .section-title();
// 设计 13:竖条 4×20 设计px、文字 15 设计px/700,条→文间距 8 设计px margin-top: 4px;
padding-left: 9px; padding-left: 12px; // 文字落 x82 = 内容区 x70 + 12
font-size: 12px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px;
&::before { &::before {
width: 3px; width: 4px;
height: 15px; height: 20px;
} }
} }
// 设计 13:正文与容器左缘对齐(只有标题被竖条顶出一级) // 标题盒 321..341 → 「总体进度」盒 370(12/16)
&__head { &__head {
.flex(row, space-between, center); .flex(row, space-between, baseline);
margin-top: 20px; margin-top: 29px;
} }
&__label { &__label {
font-size: 12px; font-size: 12px;
line-height: 16px;
color: @project-label;
} }
// 设计 13:「65%」20 设计px × 0.77 ≈ 15px / 700 // 「65%」20/700:ink 364 → 行盒 361,相对 head 行顶 370 上移 9
&__percent { &__percent {
font-size: 15px; margin-top: -9px;
font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 26px;
color: @project-accent; color: @project-accent;
} }
// 说明盒 370..386 → 轨道顶 402
&__track { &__track {
position: relative; position: relative;
height: 5px; height: 6px;
margin: 12px 0 0; margin: 14px 0 0;
border-radius: @radius-round; border-radius: @radius-round;
background: @project-track; background: @project-track;
} }
@@ -94,25 +103,13 @@ function clampPercent(value: number): number {
transition: width 0.3s; transition: width 0.3s;
} }
// 设计 13:末端白底蓝环圆点 ≈15 设计px × 0.77 // 轨道底 408 → 起止时间盒 418(11/15,ink 420)
&__knob {
position: absolute;
top: 50%;
width: 12px;
height: 12px;
border: 2px solid @project-accent;
border-radius: @radius-round;
background: @white;
transform: translate(-50%, -50%);
box-sizing: border-box;
}
&__range { &__range {
.flex(row, space-between, center); .flex(row, space-between, center);
margin-top: 10px; margin-top: 10px;
font-size: 11px; font-size: 11px;
// 设计 13:起止时间 (156,163,175) line-height: 15px;
color: @text-color-disabled; color: @project-muted;
} }
} }
</style> </style>

View File

@@ -33,75 +33,80 @@ defineProps<Props>()
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测:头像直径 64 设计px、相邻头像中心距 99 设计px → ×0.77 得 49px / 76px(取整 48 / 间距 28) // 设计稿实测(1920,1:1;5865c6d9 −14 基准 ⇒ 统一后 +14):
@team-avatar-size: 48px; // 标题 ink 617 15/700;头像直径 64、首列左缘 x66.5(中心 98.5)、相邻中心距 99 → 间距 35;
@team-avatar-gap: 28px; // 姓名 ink 734 16/600 #1F2937;角色 ink 761 12/400 #9CA3AF。
.project-team { .project-team {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 设计 13:项目团队区块直接落在白底页面上,无卡片
padding: 0; padding: 0;
&__title { &__title {
.section-title(); .section-title();
// 设计 13:竖条 4×20 设计px、文字 15 设计px/700 margin-top: 42px; // 里程碑说明盒底 575 → 标题盒 614(ink 617)
padding-left: 9px; padding-left: 12px;
font-size: 12px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px;
&::before { &::before {
width: 3px; width: 4px;
height: 15px; height: 20px;
} }
} }
// 标题盒底 634 → 头像顶 670(中心 y702)
&__list { &__list {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
flex-wrap: wrap; flex-wrap: wrap;
gap: @spacing-base @team-avatar-gap; gap: 32px @team-gap;
margin-top: 16px; margin-top: 33px;
margin-left: -3.5px; // 首列左缘 66.5 = 内容区 70 − 3.5
} }
&__item { &__item {
.flex(column, flex-start, center); .flex(column, flex-start, center);
width: @team-avatar-size; width: @team-size;
} }
// 设计 13:头像 = 品牌蓝实底圆 + 白色单字(16 设计px × 0.77 ≈ 12px) // 品牌蓝实底圆 + 白色单字(设计 16/400)
&__avatar { &__avatar {
.flex(row, center, center); .flex(row, center, center);
width: @team-avatar-size; width: @team-size;
height: @team-avatar-size; height: @team-size;
border-radius: @radius-round; border-radius: @radius-round;
background: @team-avatar-bg; background: @team-avatar-bg;
color: @white; color: @white;
font-size: 12px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 22px;
} }
// 设计 13:姓名 16 设计px/600,角色 12 设计px #9CA3AF // 头像底 734 → 姓名 ink 734
&__name { &__name {
margin-top: 16px; margin-top: 0;
max-width: 100%; max-width: 100%;
font-size: 12px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: @heading-color; line-height: 22px;
color: @project-ink;
text-align: center; text-align: center;
.ellipsis(); .ellipsis();
} }
// 姓名盒底 756 → 角色盒 759(12/19,ink 761)
&__role { &__role {
margin-top: @spacing-xs; margin-top: 3px;
max-width: 100%; max-width: 100%;
font-size: 12px; font-size: 12px;
line-height: 19px;
text-align: center; text-align: center;
color: @text-color-disabled; color: @project-muted;
.ellipsis(); .ellipsis();
} }
&__empty { &__empty {
margin-top: @spacing-base; margin-top: @spacing-base;
font-size: @font-size-base; font-size: @font-size-body;
} }
} }
</style> </style>

View File

@@ -131,7 +131,9 @@ onMounted(load)
</nav> </nav>
<template v-if="current"> <template v-if="current">
<div class="project-detail__info">
<ProjectInfoCard :project="current" /> <ProjectInfoCard :project="current" />
</div>
<!-- 本地 Tab 切换(设计稿:文字 + 激活项下划线) --> <!-- 本地 Tab 切换(设计稿:文字 + 激活项下划线) -->
<div class="project-detail__tabs"> <div class="project-detail__tabs">
@@ -204,47 +206,55 @@ onMounted(load)
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿实测(1920 画布,1:1,以 785b3eb8 / 843103b2 / ef0166c6 三张同基准稿为准):
// 面包屑 ink (70,116) 24/500 #666666;信息块 logo (70,196) 64×64、标题 ink (142,212) 19/700、
// 字段行 ink (142,245) 11px;页签行 278..318,文字 ink 278、激活蓝 #024FFF/600、常规 #4B5563/400,
// 节距 86;分割线 (70,317) 1780×1 #F0F1F3;页脚 (0,1811) 1920×481 #0C1321。
.project-detail { .project-detail {
// ⚠️ mixin 签名是 .flex(@direction; @justify; @align) —— 第二参是 justify、**第三参才是 align**。
// 原来只传两参 ⇒ align 取默认的 center ⇒ 纵向容器里所有卡片被压成内容宽度、水平居中(380px 窄条)。
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base; gap: 0;
padding-top: @spacing-lg;
padding-bottom: @spacing-xxl;
// 设计 09/10/11/13:详情内容区是整片白底、无卡片(蓝湖实测页面底 #FFFFFF)
background: @white; background: @white;
// 面包屑(设计 24px × 0.77 ≈ 18px,#666666,字重 500) // 顶栏底 72 → 面包屑 ink 116(24px/27 行盒 → box 111.5,约 +40)
&__breadcrumb { &__breadcrumb {
font-size: 18px; padding-top: 40px;
font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 27px;
color: @project-breadcrumb; color: @project-breadcrumb;
} }
// 设计稿:页签无白底容器,仅一行文字 + 激活项下划线,下缘 1px 通栏分割线; // 面包屑块 72..139 → 信息块顶 195(logo 196)
// 首个页签文字与内容区左缘对齐(间距 = 86 设计px 节距 − 文字宽 ≈ 22px) &__info {
margin-top: 56px;
}
// 信息块底 262 → 页签行顶 276..317(分割线在 317)
&__tabs { &__tabs {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 22px; gap: 16.45px; // 文字宽 69.55,节距 86 → 间距 86 − 69.55
height: 41px;
margin-top: 14px;
box-sizing: border-box;
border-bottom: 1px solid @project-divider; border-bottom: 1px solid @project-divider;
} }
&__tab { &__tab {
position: relative; position: relative;
padding: 0 0 16px; padding: 0 0 24px; // 文字 box 0..17、下划线落 41
border: none; border: none;
background: transparent; background: transparent;
color: @text-color; color: @project-cell;
font-size: 11px; font-size: 13px;
font-weight: 400; font-weight: 400;
line-height: 14px; line-height: 17px;
cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
&:hover { &:hover {
color: @project-accent; color: @project-accent;
} }
// 刷新中:轻微变淡 + 进度光标(本地接口很快,做太重的 loading 反而闪屏)
&.is-loading { &.is-loading {
opacity: 0.6; opacity: 0.6;
cursor: progress; cursor: progress;
@@ -261,7 +271,6 @@ onMounted(load)
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 2px; height: 2px;
border-radius: 1px;
background: @project-accent; background: @project-accent;
} }
} }
@@ -270,7 +279,7 @@ onMounted(load)
&__empty { &__empty {
padding: @spacing-xxl 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;
font-size: @font-size-base; font-size: @font-size-body;
} }
} }
</style> </style>

36
tabs.js Normal file
View File

@@ -0,0 +1,36 @@
// @ts-nocheck
// 逐页签量测:node tabs.js 生成一段 JS,在页面内点第 N 个页签后量测
const TAB = Number(process.argv[2] || 0)
const SELS = process.argv[3]
? process.argv[3].split(',')
: [
'.project-detail__tabs',
'.project-files__list',
'.project-files__item',
'.project-finance__title',
'.project-finance__plans',
'.project-finance__plan',
'.project-finance__files',
'.project-finance__file',
'.project-after-sales__banner',
'.project-after-sales__title',
'.project-after-sales__field',
'.project-after-sales__readonly',
'.project-after-sales__actions',
'.project-after-sales__btn',
]
const js = `(async function(){
const tabs = document.querySelectorAll(".project-detail__tab");
if (${TAB} > 0 && tabs[${TAB} - 1]) { tabs[${TAB} - 1].click(); await new Promise(r => setTimeout(r, 900)); }
const o = {};
for (const s of ${JSON.stringify(SELS)}) {
const list = document.querySelectorAll(s);
o[s] = list.length === 0 ? null : Array.from(list).slice(0, 3).map(e => {
const r = e.getBoundingClientRect();
return [+r.x.toFixed(1), +r.y.toFixed(1), +r.width.toFixed(1), +r.height.toFixed(1)];
});
}
return JSON.stringify(o);
})()`
console.log(js)

110
vite.visual.config.ts Normal file
View File

@@ -0,0 +1,110 @@
// 视觉走查专用:在 vite.config.ts 之上叠加 mock 中间件(仅本地临时使用,不参与构建)
import { fileURLToPath, URL } from 'node:url'
import { readFileSync, existsSync } from 'node:fs'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
const MOCK_DIR = fileURLToPath(new URL('./.mock', import.meta.url))
/** 把 mock 目录下的 json 按路径映射回响应 */
const ROUTES: Array<[RegExp, string]> = [
[/\/web\/requirementPool\/get\/progress\//, 'progress.json'],
[/\/web\/requirementPool\/get\/files\//, 'files.json'],
[/\/web\/requirementPool\/get\/finance\//, 'finance.json'],
[/\/web\/requirementPool\/get\/afterSales\//, 'afterSales.json'],
[/\/web\/bill\/list/, 'bills.json'],
]
/**
* 状态变体覆盖(走查专用)。
*
* <p>同一张设计稿的多个状态(如「售后信息」只看已完成 + 处理中)无法靠一份静态
* json 同时表达,所以支持 `?state=done` 之类的开关在服务端就地改写字段,
* 免得为了看一个状态去反复改 mock 文件。</p>
*
* <p>注意:详情页首屏 4 个接口同帧并发,顶部信息卡的 `status` 取**任意一支**先返回
* 的,所以只改一支会出现「头说进行中、体说已完成」的错位。变体必须 4 支都打。</p>
*/
const VARIANTS: Record<string, Array<[string, unknown]>> = {
// 项目已完成 + 有处理中的售后工单 → 对应设计稿 ef0166c6
done: [['status', 'COMPLETED']],
}
/** 按点号路径写值,创建不存在的中间层 */
function setPath(obj: Record<string, unknown>, path: string, value: unknown): void {
const keys = path.split('.')
let cur = obj
for (let i = 0; i < keys.length - 1; i += 1) {
const k = keys[i]
if (typeof cur[k] !== 'object' || cur[k] === null) cur[k] = {}
cur = cur[k] as Record<string, unknown>
}
cur[keys[keys.length - 1]] = value
}
export default defineConfig({
plugins: [
vue(),
{
name: 'visual-mock',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const url = req.url ?? ''
// 状态开关从 **Referer** 取(= 当前页面 URL),而不是 req.url:
// 业务代码发的接口地址里没有页面 query,走查期又不想为看一个状态改应用代码。
const referer = String(req.headers.referer ?? '')
const state = referer ? new URL(referer).searchParams.get('state') : null
for (const [re, file] of ROUTES) {
if (!re.test(url)) continue
const path = `${MOCK_DIR}/${file}`
if (!existsSync(path)) break
let body = readFileSync(path, 'utf-8')
const patch = state ? VARIANTS[state] : undefined
if (patch) {
const json = JSON.parse(body)
for (const [p, v] of patch) setPath(json, `data.${p}`, v)
body = JSON.stringify(json)
}
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(body)
return
}
next()
})
},
},
],
server: {
proxy: {
// 走查期不转发后端:未命中的接口直接 404(避免真实后端的 401 干扰观感)
'/api': {
target: 'http://127.0.0.1:1',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
configure: (proxy) => {
proxy.on('error', (_err, _req, res) => {
if (res && !res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
}
res?.end?.(JSON.stringify({ code: 200, msg: 'ok', data: null }))
})
},
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
css: {
preprocessorOptions: {
less: {
additionalData:
'@import "@/styles/variables.less";\n@import "@/styles/mixins.less";',
},
},
},
})