feat 首页

This commit is contained in:
Ironsp
2026-10-08 17:26:33 +08:00
parent a3b04485dd
commit aa440d6bb5
21 changed files with 138 additions and 926 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ RTK.md
.workbuddy .workbuddy
specs2 specs2
.shots .shots
docs
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@@ -1,412 +0,0 @@
# PRD — 虫子智造 研发智造一体化平台(Web 端)
> 来源:`docs/prototype/` 共 10 个墨刀演示原型(无蓝湖设计稿、无接口文档,本文档为唯一需求基线)。
> 项目定位:一站式研发智造一体化服务平台,打通需求提报 → 项目管控 → 合同签署 → 财务结算 → 售后迭代全流程。平台自营,对项目质量全权兜底。
## 1. 全局框架
### 1.1 顶部导航(AppHeader,全站公共)
| 导航项 | 路由 | 说明 |
| --- | --- | --- |
| 首页 | `/` | 门户首页 |
| 案例库 | `/cases` | 成功案例展示 |
| 需求提报 | `/demand` | 需求填报表单 |
| 项目中心 | `/project` | 项目列表/详情 |
| 合同中心 | `/contract` | 合同列表/详情/签署 |
| 财务中心 | `/finance` | 账单/支付/发票 |
| 个人中心 | `/profile` | 账号与资料管理 |
- 右侧:用户头像/昵称、消息铃铛(未读数角标,示例 3)、搜索等。
- 未登录访问受保护路由 → 跳转 `/login`(支持游客模式的部分页面除外)。
### 1.2 全局浮动组件
- **快捷提报**(右侧悬浮按钮):一键跳转 `/demand`。
- **在线客服**(右侧悬浮按钮):一键跳转 `/service`。
### 1.3 全局页脚(AppFooter)
- 版权声明:©2026 虫子智造 研发智造一体化平台,源码/图纸受平台知识产权保护。
- ICP 备案号、公安备案号。
- 联系方式:商务合作 business@chongzizhizao.com、技术支持 support@chongzizhizao.com、客服热线 400-XXX-XXXX、工作时间 周一–周五 9:00–18:00。
- 快速导航:首页 / 案例中心 / 需求提报 / 下载 APP / 帮助中心。
- 核心业务:互联网软件开发 / 硬件电路开发 / 物联网软硬一体化全案 / PCB 制版 & SMT 贴片生产 / 元器件集采与整机装配。
- 平台简介文案(研发智造一体化自营平台…)。
## 2. 页面清单及功能描述
| # | 模块 | 页面 | 原型文件 | 访问权限 |
| --- | --- | --- | --- | --- |
| 1 | home | 首页 | 首页-演示.html | 公开 |
| 2 | home | 业务详情 | 首页-演示.html(业务详情画板) | 公开 |
| 3 | cases | 案例库列表 | 案例库-演示.html | 公开 |
| 4 | cases | 案例详情 | 案例库-演示.html | 公开 |
| 5 | demand | 需求提报 | 需求填报-演示.html | 公开(提交需登录) |
| 6 | login | 登录/注册/找回密码 | 登录-演示.html | 游客 |
| 7 | project | 项目中心列表 | 项目中心-演示.html | 登录 |
| 8 | project | 项目详情 | 项目中心-演示.html | 登录 |
| 9 | project | 申请售后 | 项目中心-演示.html | 登录 |
| 10 | contract | 合同中心列表 | 合同管理-演示.html | 登录 |
| 11 | contract | 合同详情 | 合同管理-演示.html | 登录 |
| 12 | contract | 签署合同 | 合同管理-演示.html | 登录 |
| 13 | finance | 财务中心概览 | 财务中心-演示.html | 登录 |
| 14 | finance | 账单明细 | 财务中心-演示.html | 登录 |
| 15 | finance | 账单详情 | 财务中心-演示.html | 登录 |
| 16 | finance | 扫码支付/支付结果 | 财务中心-演示.html | 登录 |
| 17 | finance | 发票申请 | 财务中心-演示.html | 登录 |
| 18 | finance | 发票抬头管理 | 财务中心-演示.html | 登录 |
| 19 | message | 消息中心 | 消息中心-演示.html | 登录 |
| 20 | message | 系统通知详情 | 消息中心-演示.html | 登录 |
| 21 | service | 在线客服 | 在线客服-演示.html | 登录 |
| 22 | profile | 账号安全 | 个人中心-演示.html | 登录 |
| 23 | profile | 地址管理 | 个人中心-演示.html | 登录 |
| 24 | profile | 收藏列表 | 个人中心-演示.html | 登录 |
| 25 | profile | 资质列表 | 个人中心-演示.html | 登录 |
| 26 | profile | 意见反馈 | 个人中心-演示.html | 登录 |
| 27 | profile | 帮助中心 | 个人中心-演示.html | 登录 |
| 28 | profile | 关于我们 | 个人中心-演示.html | 登录 |
| 29 | profile | 专属客服热线 | 个人中心-演示.html | 登录 |
---
## 3. 各页面交互逻辑
### 3.1 首页(home)
- **欢迎区(登录后)**:问候语「中午好」+ 当日摘要「今天有2个项目正在进行」。
- **数据看板**:待您处理 3 / 进行中 3 / 累计项目 6。
- **快捷入口**:财务台账、合同台账、客服工作台、案例库管理、工单需求池(跳对应模块)。
- **业务服务**:4 张卡片(互联网软件 / 硬件电路 / 物联网全案 / 生产制造),每张含描述 + 「了解详细 >」→ 业务详情页。
- **案例库精选**:按业务类型 Tab(全部/互联网软件/硬件电路/物联网全案/生产制造)筛选案例卡片,「查看更多 >」→ 案例库,「查看详情 >」→ 案例详情。
- **服务区(Tab 切换)**:服务类型(Web应用/小程序/APP/二次开发/桌面工具)与服务内容(五类服务条款富文本 + 视频)。
### 3.2 业务详情(home detail)
- 面包屑「首页 > 业务详情」。
- 业务线介绍(标题 + slogan,如「专业的软件定制开发服务,从需求到上线全流程保障」)。
- 服务内容富文本 + 视频(预览播放)。
- 服务类型子项切换(Web/小程序/APP/二次开发/桌面工具)。
### 3.3 案例库列表(cases)
- 头部:标题「成功案例库」+ 副文案。
- **筛选面板**:
- 业务类型:全部/互联网软件/硬件电路/物联网全案/生产制造(单选,带计数 25/25/25/25/100)。
- 所属行业:全部/电商零售/教育培训/医疗健康/硬件电路/能源环保/房地产(多选按钮组)。
- 项目规模:全部/50万以下/50-100万/100-300万/300-500万/500万以上(多选)。
- 完成时间:全部/近3个月/近半年/近一年/开始时间—结束时间(日期范围)。
- 关键字搜索 + 重置。
- 已选条件回显(「已选条件(4个)」+ 条件 chip × 移除 + 清空)、「确定筛选(100个结果)」。
- **案例卡片**:封面图、业务类型标签、标题、查看详情 >;字段:完成时间/服务团队/规模/所属行业/项目周期/客户类型。
- 分页器(‹ 1 2 3 4 5 ›)。
- **收藏列表**(profile 模块复用):同卡片布局,展示已收藏案例。
### 3.4 案例详情(cases detail)
- 面包屑「案例库 > 案例详情」。
- 项目概览:标题、业务类型、完成时间/服务团队/规模/所属行业/项目周期/客户类型。
- 操作:**收藏**(收藏/已收藏切换)、**分享**。
- 视频区(预览播放)。
- 富文本区块:项目背景、客户痛点、需求描述、交付物清单。
- 底部 CTA:**一键填报类似案例** → 跳转 `/demand` 并预填业务类型。
### 3.5 需求提报(demand)
分步表单(1 选择业务类型 → 2 填写需求信息 → 3 确认信息并补充备注):
1. **选择业务类型***(单选 5 项):互联网软件 / 物联网全案 / 生产制造 / 硬件电路 / 其他(「若没有对应的服务内容请选择该选项」),每项含图标 + 描述。
2. **填写需求信息***:
- 项目名称*(文本)。
- 附件上传:支持图片/文档/压缩包,大小不超过 1G;支持上传按钮 + 文件列表(名称/大小/上传时间/预览/下载/删除)。
- 核心需求描述*(多行,0/500 字计数)。
- 预计开发周期(工作日,数字)。
- 预算范围(按钮单选:1万以下 / 1-5万 / 5-10万 / 10万+)。
3. **确认信息并补充备注**:
- 用户名称(默认当前登录用户「王永志」)。
- 用户手机号(脱敏 1866\*\*\*\*9618,可手填)。
- 补充备注(0/500 字计数)。
- 底部:提示语「提交后我们的专业顾问将在一个工作日内与您联系」。
- 操作按钮:**立即提报** / **保存草稿** / **重置** / 手填。
### 3.6 登录/注册/找回密码(login)
统一视觉:左侧品牌区(logo + 「虫子智造研发一体化平台」+ slogan「安全 · 高效 · 可控」),右侧表单区;顶部「游客模式」入口、「没有账号?立即注册 / 已有账号?立即登录」切换。
- **账号密码登录**:账号、密码、协议勾选(《用户协议》《隐私政策》)、登录按钮。
- **手机验证码登录**:手机号、验证码、「获取验证码 / 60秒后重新获取」倒计时、协议勾选、登录按钮。
- **注册**:手机号、验证码、设置密码(6-20 位,**密码强度指示:弱/中/强**,强度过低提示「密码等级过低,请重新输入」)、确认密码、协议勾选、注册按钮。
- **找回密码**:手机号、验证码、设置密码、确认密码、「确认修改」。
- 状态反馈:「注册成功,请登录」「修改成功,请登录」「登录失败,请先注册账号」,**3 秒后自动跳转**。
### 3.7 项目中心列表(project)
- 标题 + 副文案「查看和管理您的所有项目,实时跟踪项目进度」。
- 筛选:关键字搜索、业务类型(全部/互联网软件/硬件电路/物联网全案/生产制造)、项目状态(全部/待立项/进行中/待验收/已完成/售后中/已取消,带计数 25/25/25/25/100)、提交时间(全部/近3个月/近半年/近一年/起止日期)、已选条件回显、确定筛选/重置。
- 表格列:序号、项目编号、项目名称、业务类型、进度(64%)、状态、操作(详情;已完成时另有**申请售后**)。
- 状态枚举:待立项 / 进行中 / 待验收 / 已完成 / 售后中 / 已取消。
- 分页器。
### 3.8 项目详情(project detail)
- 面包屑「项目中心 > 项目详情」。
- 头部信息卡:项目名称(社区康养智能服务系统)、项目编号、业务类型、提交时间、状态标签。
- 项目团队:项目经理卡片(头像/姓名/角色,张三)。
- **Tab 四页签**:
1. **项目进度**:总体进度 65% + 开发进度滑杆;里程碑时间轴(需求确认 → 原型设计 → 开发实现 → 测试验收 → 项目交付),每节点状态(待开始/进行中/已完成)+ 完成时间;开始/结束时间。
2. **项目文件**:文件列表(项目需求规格说明书.pdf 等,2.4MB · 上传时间),操作:**预览 / 下载 / 删除**;**上传文档**按钮。
3. **合同财务**:付款计划卡片列表(第一期·预付款 比例33% / 第二期·付款 33% / 第三期·尾款 33%,含账单号、关联项目、金额 ¥20100,状态:已支付(含交易流水号)/待支付(**立即支付**按钮)/未到期);发票记录(预览/下载);合同文件(预览/下载)。
4. **售后信息**:售后工单状态卡(「售后工单处理中——工程师正在处理您的修改需求」);**申请售后 / 取消售后**按钮。
### 3.9 申请售后(project → after-sales)
- 分步表单:1 选择业务类型*、2 填写问题描述*、3 补充备注。
- 字段:项目名称*(默认当前项目)、售后类型*(推荐选项 + 手动输入,示例「BUG问题」)、问题描述*(0/500 字,「请详细描述您的问题...」)、附件上传(图片/文档/压缩包 ≤1G,文件列表预览/下载/删除)、补充备注(0/500 字)。
- 操作:**申请售后**(提交,生成售后工单) / **重置** / **取消**。
### 3.10 合同中心列表(contract)
- 标题「合同中心」+ 副文案「项目电子合同在线签署、归档与查询管理」。
- 关键字搜索 + 搜索/重置。
- 表格列:合同编号、合同名称、合同类型、合同金额、关联项目、项目编号、签署状态、生效时间、到期时间、操作(**去签署**(待签署时)/ 详情)。
- 签署状态枚举:履行中 / 已过期 / 待签署。
- 分页器。
### 3.11 合同详情(contract detail)
- 面包屑「合同中心 > 合同详情」。
- 合同信息(只读表单):合同名称、合同类型、合同编号、签订日期、生效日期、到期日期、合同状态。
- 关联项目:项目编号、项目名称、「查看详情」→ 项目详情。
- 合同文件:文件卡(预览/下载)。
- 操作:**返回**。
### 3.12 签署合同(contract sign)
- 面包屑「合同中心 > 合同详情 > 签署合同」。
- 左侧二维码(**04:32 倒计时失效**,过期可点击刷新)。
- 右侧「扫码验证签署操作步骤」四步引导:
1. 打开手机支付宝(右上角 + → 扫一扫)。
2. 验证身份信息(实名验证,确保本人操作)。
3. 确认合同内容并签署(指定位置签署)。
4. 完成签署(页面自动跳转至合同详情页)。
### 3.13 财务中心概览(finance)
- 标题「财务中心」+ 副文案「账单、支付、发票统一财务管理中心」。
- **累计消费总额**卡片(¥1,726,823,900)+ 月份选择(2026年1月 SELECT)+ 时间维度切换(本季度/本年)。
- **业务类型分布**图表(生产智造/物联网全案/硬件电路/互联网软件 饼图)。
- 待支付角标(3)。
- **快捷入口**:发票申请、账单明细、报表导出。
### 3.14 账单明细(finance bills)
- 面包屑「财务中心 > 账单明细」。
- 状态 Tab:全部 / 待支付 / 已支付 / 未到期(带计数)。
- 关键字搜索 + 搜索/重置。
- 表格列:多选框、序号、账单名称(第一期·预付款 比例33%)、账单号、交易流水号、项目名称、订单金额、状态、操作(**去支付**(待支付)/ 详情)。
- 汇总条:总笔数 10、总金额 123212、待支付 3。
- 多选后:「已选择1笔订单 · 合计:¥20000」+ **立即支付**。
- 分页器。
### 3.15 账单详情(finance bill detail)
- 面包屑「财务中心 > 账单明细 > 账单详情」。
- 订单信息卡:账单名称、账单号、关联项目、金额。
- 待支付订单态:**立即支付**入口。
- 已支付订单态:支付信息(付款金额、支付方式、支付时间、交易流水号)。
- 未到期态:状态提示。操作:**返回**。
### 3.16 扫码支付 + 支付结果(finance pay)
- 面包屑「财务中心 > 账单明细 > 账单详情 > 扫码支付」。
- 左侧支付二维码(**04:32 倒计时**,过期可刷新),安全提示「本页面已通过安全验证,请勿向他人泄露付款码」。
- 支付方式:微信支付 / 支付宝支付(**切换支付方式**弹窗:单选,「推荐使用」「其他用户选择」标签)。
- 右侧:支付金额(¥20000)+ 订单明细列表(账单号/关联项目/期数/金额)。
- 「扫码支付操作步骤」三步引导(微信/支付宝各自文案)。
- **支付结果页**:支付成功(✓、付款金额、共N个订单、支付信息:支付方式/支付时间/交易流水号、已支付订单列表)。
### 3.17 发票申请(finance invoices)
- 面包屑「财务中心 > 发票申请」。
- **发票抬头卡**:默认抬头(企业名称 + 税号 + 「企业」标签 + 「默认」标签)、「管理抬头 >」。
- 状态 Tab:全部 / 已开票 / 开票中 / 未开票。
- 表格列:多选框、序号、账单名称、账单号、交易流水号、项目名称、订单金额、状态、申请时间、操作:
- 已开票:**预览 / 下载 / 发送邮箱**;
- 开票中:无操作(进行中);
- 未开票:**申请开票**。
- 关键字搜索 + 分页器。
### 3.18 发票抬头管理(finance invoice-titles)
- 列表卡:抬头类型(企业单位/非企业单位)、发票抬头、税号、默认标签。
- 操作:**+ 新增抬头**、编辑、删除、设为默认 / 取消默认。
- 弹窗(新增/编辑):抬头类型(单选:企业单位/非企业单位)、发票抬头*、税号*。
- 删除二次确认弹窗:「删除后无法恢复,是否继续?」(取消/确认删除)。
### 3.19 消息中心(message)
- 标题「消息中心」+ 副文案「统一接收项目进度、售后工单、账单待办等站内消息,重要业务动态实时提醒」。
- 分类 Tab:全部 / 在线客服 / 状态通知 / 系统通知(未读数角标 3/3/3/9)。
- 关键字搜索 + 搜索/重置 + **全部已读**按钮。
- 消息条目:类型图标、类型标签(系统通知/合同生效/在线客服)、摘要、「刚刚」相对时间、未读标记。
- 分页器。
### 3.20 系统通知详情(message detail)
- 面包屑「消息中心 > 系统通知详情」。
- 标题 + 发布时间 + 富文本正文(功能亮点/使用场景/入口位置等)。
- 附件(PDF 2.5MB,预览/下载)。
- 操作:**返回**。
### 3.21 在线客服(service)
- 标题「在线客服」+ 副文案「在线客服为您提供需求咨询、项目进度查询、业务答疑…」。
- 聊天窗口:客服头像/名称(专属客服)、欢迎语、**历史消息分割线**、「未读」标记。
- 消息类型:文本、文件(物联网全案服务介绍.pdf 2.5MB)、时间戳(今天 10:23)。
- 输入区:多行输入(placeholder「输入消息,按Enter发送,Shift+Enter换行...」)、**Enter 发送**、**拖拽上传文件**、发送按钮。
### 3.22 个人中心(profile)— 左侧菜单布局
左侧菜单:账号安全 / 地址管理 / 收藏列表 / 资质列表 / 意见反馈 / 帮助中心 / 关于我们 / 专属客服热线。头部用户卡:默认名称 + 头像 + 手机号(脱敏,可编辑昵称)。
1. **账号安全**:
- 修改密码(上次修改:15天前;手机号 + 验证码 + 设置密码 + 确认密码 + 强度提示 + 协议勾选 + 确认修改)。
- 绑定手机(已绑定 186\*\*\*\*618;换绑流程:验证并换绑 → 新手机号 + 验证码,提示「换绑新手机号之后,可以用新的手机号及当前密码登录」)。
- 绑定邮箱(已绑定/未绑定 yongzh\*\*\*\*\*\*4@163.com;绑定/更换绑定邮箱/取消绑定,邮箱 + 邮箱验证码)。
- **注销账号**:注销条件校验(账号无未完成的订单/未结清的款项/进行中的合同/未处理的工单)→ 风险提示 4 条 → 同意《账号注销协议》→ 安全身份验证(发送验证码至 186\*\*\*\*9618)→ 二次确认弹窗「确定要注销账号吗?」(暂不注销/注销账号)→ 「注销成功」3 秒后返回登录页。
2. **地址管理**:地址卡列表(收货人姓名/联系方式/所在地区/详细地址/默认标签);+ 新增地址、编辑、删除(二次确认)、设为默认/取消默认;分页。
3. **收藏列表**:收藏的案例卡片(同 3.3 卡片),筛选面板同案例库。
4. **资质列表**:资质卡(资质类型/证书名称/证书编号/发证单位/有效期/状态:已认证、审核中、未通过);+ 新增资质/提交认证、编辑、删除(二次确认)、重新上传(未通过时);分页。
- 新增/编辑弹窗:资质类型*(SELECT:营业执照/生产许可证/体系认证/检测报告/授权书/其他资质)、证书名称*、证书编号*、发证单位*、有效期*(起止日期)、附件上传(图片/文档/压缩包,添加附件)。
5. **意见反馈**:反馈标题*、详细描述*(0/500 字)、附件上传(≤1G,预览/删除)、**提交反馈**/重置;「提交成功」提示。
6. **帮助中心**:FAQ 折叠组(1.账户与登录、2.功能使用指南、3.系统与权限,各含 Q/A)。
7. **关于我们**:平台简介、我们的使命、核心优势(4 条)、联系信息(客服热线/工作时间/平台官网链接 www.chongzizhizao.com/公司地址 山东省日照市)、版本信息(V1.0.0,更新时间 2026年09月)。
8. **专属客服热线**:电话 000-000-0000 + 工作时间(工作日 8:00-12:00 14:00-18:00)。
---
## 4. 数据字段定义(核心实体)
### 4.1 Case(案例)— `types/business/cases.d.ts`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | string | 案例 ID |
| title | string | 项目名称 |
| category | BusinessType | 业务类型 |
| cover | string | 封面图 |
| finishDate | string | 完成时间 |
| teamSize | number | 服务团队(人) |
| scale | ScaleRange | 规模区间 |
| industry | string | 所属行业 |
| duration | string | 项目周期 |
| clientType | string | 客户类型 |
| background / painPoints / requirements / deliverables | string | 详情富文本 |
| video | string | 视频地址 |
| isFavorite | boolean | 是否收藏 |
### 4.2 Demand(需求)— `types/business/demand.d.ts`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| businessType | BusinessType | 必填,单选(含「其他」) |
| projectName | string | 必填 |
| attachments | FileInfo[] | ≤1G |
| coreDescription | string | 必填,≤500 字 |
| expectedDuration | number | 预计开发周期(工作日) |
| budgetRange | 'lt1w' \| '1to5w' \| '5to10w' \| 'gt10w' | 预算范围 |
| userName / userPhone | string | 联系人 |
| remark | string | ≤500 字 |
| status | 'draft' \| 'submitted' | 草稿/已提交 |
### 4.3 Project(项目)— `types/business/project.d.ts`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| projectNo | string | 项目编号 |
| projectName | string | 项目名称 |
| businessType | BusinessType | 业务类型 |
| progress | number | 进度 % |
| status | ProjectStatus | 待立项/进行中/待验收/已完成/售后中/已取消 |
| submitTime / startTime / endTime | string | 时间 |
| milestones | Milestone[] | 需求确认/原型设计/开发实现/测试验收/项目交付(status + time) |
| team | TeamMember[] | 项目经理等 |
| files | FileInfo[] | 项目文件 |
| paymentPlans | Bill[] | 付款计划 |
| invoiceRecords | Invoice[] | 发票记录 |
| contractFile | FileInfo | 合同文件 |
| afterSales | AfterSales | 售后工单 |
### 4.4 Contract(合同)— `types/business/contract.d.ts`
contractNo、contractName、contractType、contractAmount、relatedProject({ id, name })、projectNo、signStatus(履行中/已过期/待签署)、signDate、effectiveDate、expiryDate、files: FileInfo[]。
### 4.5 Bill / Payment / Invoice / InvoiceTitle — `types/business/finance.d.ts`
- **Bill**:billName(含期数与比例)、billNo、tradeNo、projectName、amount、status(待支付/已支付/未到期)、createTime。
- **Payment**:method(wechat/alipay)、amount、payTime、tradeNo、qrCode、expiresAt(倒计时)。
- **Invoice**:bill(关联账单)、status(已开票/开票中/未开票)、applyTime、file。
- **InvoiceTitle**:type(企业单位/非企业单位)、title、taxNo、isDefault。
### 4.6 Message(消息)— `types/business/message.d.ts`
id、type(在线客服/状态通知/系统通知)、title、summary、content(富文本)、publishTime、isRead、attachments: FileInfo[]。
### 4.7 用户与账号 — `types/business/user.d.ts` / `types/business/profile.d.ts`
- **User**:id、nickname、avatar、phone(脱敏展示)、email(脱敏展示)、passwordModifiedAt。
- **Address**:id、receiverName、phone、region、detailAddress、isDefault。
- **Qualification**:id、type(营业执照/生产许可证/体系认证/检测报告/授权书/其他)、certName、certNo、issuer、validFrom、validTo、status(已认证/审核中/未通过)、attachments: FileInfo[]。
### 4.8 通用 — `types/api.d.ts`
- `ApiResponse<T> { code, msg, data }`、`PageResult<T> { list, total }`、`FileInfo { name, size, uploadTime, url }`。
---
## 5. 组件拆分建议
### 5.1 公共组件(`src/components/`,≥2 页面使用)
| 组件 | 使用页面 |
| --- | --- |
| AppHeader.vue | 全站(导航 + 消息铃铛 + 用户入口) |
| AppFooter.vue | 全站 |
| AppButton.vue | 全站按钮统一 |
| AppSwiper.vue | 首页 banner、业务服务卡片轮播 |
| CaseCard.vue | 案例库、首页精选、收藏列表 |
| FilterPanel.vue | 案例库、项目中心、收藏列表(筛选 + 已选条件回显) |
| SearchBar.vue | 案例库/项目/合同/账单/发票/消息列表 |
| PaginationBar.vue | 所有列表页 |
| FileList.vue | 需求填报、项目文件、售后、资质、意见反馈(文件卡 + 预览/下载/删除) |
| UploadArea.vue | 同上(拖拽/点击上传 + 1G 限制提示) |
| SmsCodeInput.vue | 登录/注册/找回密码/账号安全 |
| PasswordStrength.vue | 注册/修改密码(弱/中/强指示) |
| QrCodePanel.vue | 合同签署、扫码支付(倒计时 + 刷新) |
| StatusTag.vue | 项目状态/签署状态/账单状态/发票状态/资质状态 |
| ConfirmDialog.vue | 删除二次确认、注销确认 |
| StepForm.vue | 需求提报、申请售后(分步表单骨架) |
### 5.2 页面独有组件(`views/<page>/cpns/`)
| 页面 | 组件 |
| --- | --- |
| home | HomeWelcome.vue、HomeStats.vue、HomeQuickEntries.vue、HomeBusinessCards.vue、HomeCaseShowcase.vue、HomeServiceTabs.vue、BusinessDetail.vue |
| cases | CasesFilter.vue、CasesGrid.vue、CaseDetailHeader.vue、CaseRichText.vue |
| demand | DemandStep1Business.vue、DemandStep2Info.vue、DemandStep3Confirm.vue |
| login | LoginForm.vue、SmsLoginForm.vue、RegisterForm.vue、ResetForm.vue |
| project | ProjectFilter.vue、ProjectTable.vue、ProjectInfoCard.vue、ProjectTeam.vue、ProjectMilestones.vue、ProjectFiles.vue、ProjectFinance.vue、ProjectAfterSales.vue、AfterSalesForm.vue |
| contract | ContractTable.vue、ContractInfoCard.vue、ContractSignPanel.vue、SignSteps.vue |
| finance | FinanceOverview.vue、FinanceChart.vue、BillTable.vue、BillSummaryBar.vue、BillPayPanel.vue、PayMethodDialog.vue、PayResult.vue、InvoiceTable.vue、InvoiceTitleCard.vue、InvoiceTitleDialog.vue |
| message | MessageTabs.vue、MessageList.vue、MessageItem.vue、NoticeDetail.vue |
| service | ChatWindow.vue、ChatMessageItem.vue、ChatFileCard.vue、ChatInput.vue |
| profile | ProfileSider.vue、ProfileUserCard.vue、SecurityPanel.vue、ChangePasswordDialog.vue、UnbindAccountDialog.vue、AddressCard.vue、AddressDialog.vue、FavoritesGrid.vue、QualificationCard.vue、QualificationDialog.vue、FeedbackForm.vue、HelpFaq.vue、AboutPanel.vue、HotlinePanel.vue |
---
## 6. 待决事项(接口文档/蓝湖到位后补充)
1. 接口路径/入参出参以接口文档为准;当前仅按本 PRD 定义前端实体类型。
2. 蓝湖视觉参数(色值/字号/间距/圆角/阴影)待设计稿提供,暂以原型提取值近似。
3. 支付(微信/支付宝)与电子签章(支付宝扫码)对接方式待后端确认。
4. 「下载 APP」「报表导出」「发送邮箱」的交互细节待确认。

View File

@@ -1,172 +0,0 @@
# 开发规划 — cz-web(auto-dev 全流程跟踪表)
## 使用说明(工作规则)
1. **状态标记**:`[x]` = 开发完成并验证通过;`[~]` = 开发中;`[ ]` = 未开始;`[!]` = 阻塞(注明原因)。
2. **每完成一项立即打 `[x]`**(开发结束标记),不得批量补标。
3. **之后只做未完成(`[ ]`/`[~]`/`[!]`)的部分**,已完成项不再重做,除非回归失败。
4. 每个模块收尾必须过 `pnpm build`(vue-tsc 零错误)才算可打 `[x]`。
5. 当前约束:**无接口文档** → 接口细节 `[!]` 阻塞中;页面代码按 PRD + 原型降级开发(结构/交互先行,视觉参数用 `styles/variables.less` 近似值,接口用 store 层 mock 临时实现并集中标注 `// TODO(api)`,文档到位后仅替换 api 层)。
6. 蓝湖设计稿已到位(2026-09-30):pid `b0052b6c-4eaa-4709-b2d1-32cf4c1fe53b`,共 55 张设计图(因 UI 失误全部命名为「首页」,已按标注图层文字 + 像素取证完成「设计图 → 路由」映射);阶段二与「蓝湖视觉补齐」已完成。
## 阶段总览
| 阶段 | 内容 | 状态 |
| --- | --- | --- |
| 阶段一 | PRD 生成(`docs/PRD.md`) | [x] |
| 阶段二 | 蓝湖 MCP 设计稿读取(标注参数/切图) | [x](55 张标注已落盘 `specs/*.ann.txt`;`lanhu_get_design_overview/export_design_assets` 因 `UnsupportedCoordinates` 不可用,切图改由 2× 封面按标注坐标裁剪) |
| 阶段三 | 页面结构规划(`docs/page-architecture.md`) | [x] |
| 阶段四 | 页面代码生成 + 组件拆分 | [x] |
| 阶段五 | API + Store 逻辑层 | [x] |
| 阶段六 | 公共组件提取 | [x] |
| 阶段七 | 自动验证与修正(`pnpm build` 零错误) | [x] |
| 补齐 | 接口文档落地后替换 mock api 层 | [!] 无接口文档,阻塞 |
| 补齐 | 蓝湖到位后校准视觉(色值/间距/字号/切图) | [x] |
## P0 基础工程(全模块前置)
- [x] vite.config.ts:配置 `@/` 别名、`css.preprocessorOptions.less.additionalData` 注入 variables.less、安装 less
- [x] src/styles/:index.less + variables.less(色/字/间距/圆角近似值)+ mixins.less + reset.less + common.less
- [x] src/types/api.d.ts:ApiResponse\<T\>、PageResult\<T\>、FileInfo
- [x] src/types/router.d.ts:RouteMeta 扩展(title、requiresAuth)
- [x] src/types/global.d.ts + src/env.d.ts
- [x] src/utils/:request.ts(唯一 axios 实例 + 拦截器)、storage.ts、validate.ts、format.ts
- [x] src/composables/:useCountDown.ts、useLoading.ts、useScroll.ts、useIntersection.ts
- [x] src/directives/:lazy.ts、debounce.ts、throttle.ts、index.ts(setupDirectives)
- [x] src/stores/index.ts(pinia 实例)+ stores/modules/site.ts(全局 loading/站点配置)
- [x] src/router/index.ts + routes.ts + guard.ts(Home + 404 先行注册,其余路由随 M1–M10 各自注册步骤接入)
- [x] src/layout/index.vue(AppHeader + router-view + AppFooter 骨架)
- [x] src/components/:AppHeader.vue、AppFooter.vue、AppButton.vue、index.ts
- [x] src/main.ts:注册 pinia/router/指令、引入 styles/index.less
- [x] 验证:`pnpm build` 零错误(vue-tsc + vite build 通过;Playwright 冒烟:header/footer/占位页渲染正常,控制台 0 错误)
## 模块开发清单(每模块七步,完成即打 [x])
### M1 home(首页 + 业务详情)
- [x] types/business/home.d.ts(含 cases.d.ts 案例实体与 api.d.ts BusinessType 共享枚举,首页案例卡提前复用)
- [x] api/home.ts(getHomeStats、getBusinessLines、getFeaturedCases、getBusinessDetail)`// TODO(api)`
- [x] stores/modules/home.ts(useHomeStore)
- [x] views/home/index.vue + index.ts + cpns/(HomeWelcome、HomeStats、HomeQuickEntries、HomeBusinessCards、HomeCaseShowcase、HomeServiceTabs)
- [x] views/home/detail.vue(业务详情)
- [x] router 注册 `/` 与 `/business/:type`
- [x] 复用检查 + `pnpm build` 通过(Playwright QA:首页六区块渲染、Tab 筛选生效、业务详情页内容完整、控制台 0 错误)
### M2 cases(案例库)
- [x] types/business/cases.d.ts(M1 已建,M2 直接复用)
- [x] api/cases.ts(getCaseList、getCaseDetail、toggleFavorite、getCaseCounts)`// TODO(api)`(8 条 mock + 筛选/分页)
- [x] stores/modules/cases.ts(useCasesStore:列表/筛选/分页/收藏)
- [x] views/cases/index.vue + detail.vue + index.ts + cpns/(CasesFilter、CasesGrid、CaseDetailHeader、CaseRichText)
- [x] 公共组件 CaseCard(M1 已建)、FilterPanel、PaginationBar 上移 components/
- [x] router 注册 `/cases`、`/cases/:id`
- [x] 复用检查 + `pnpm build` 通过(Playwright QA:4 组筛选 + 计数 8/4/2/1、筛选 8→4 卡 + chip 回显、详情四段富文本、收藏切换 + toast、控制台 0 错误)
### M3 demand(需求提报)
- [x] types/business/demand.d.ts
- [x] api/demand.ts(submitDemand、saveDraft、uploadAttachment)`// TODO(api)`
- [x] stores/modules/demand.ts(useDemandStore:表单态/草稿/提交)
- [x] views/demand/index.vue + index.ts + cpns/(DemandStep1Business、DemandStep2Info、DemandStep3Confirm)
- [x] 公共组件 StepForm、FileList、UploadArea、CaseCard 复用接入
- [x] router 注册 `/demand`
- [x] 复用检查 + `pnpm build` 通过
### M4 login(登录/注册/找回密码)
- [x] types/business/user.d.ts
- [x] api/auth.ts(loginByAccount、loginBySms、sendSmsCode、register、resetPassword)`// TODO(api)`
- [x] stores/modules/user.ts(useUserStore:token/用户信息/登录态)
- [x] views/login/index.vue(登录)+ register.vue + reset.vue + index.ts + cpns/(LoginForm、SmsLoginForm、RegisterForm、ResetForm)
- [x] 公共组件 SmsCodeInput、PasswordStrength 上移
- [x] router 注册 `/login`、`/register`、`/reset-password` + guard.ts 登录守卫
- [x] 复用检查 + `pnpm build` 通过
### M5 project(项目中心)
- [x] types/business/project.d.ts
- [x] api/project.ts(getProjectList、getProjectDetail、uploadProjectFile、deleteProjectFile、submitAfterSales、cancelAfterSales)`// TODO(api)`
- [x] stores/modules/project.ts(useProjectStore:列表/筛选/详情/文件/售后)
- [x] views/project/index.vue + detail.vue + after-sales.vue + index.ts + cpns/(ProjectFilter、ProjectTable、ProjectInfoCard、ProjectTeam、ProjectMilestones、ProjectFiles、ProjectFinance、ProjectAfterSales、AfterSalesForm)
- [x] 公共组件 StatusTag、FileList、StepForm 复用接入
- [x] router 注册 `/project`、`/project/:id`、`/project/:id/after-sales`
- [x] 复用检查 + `pnpm build` 通过
### M6 contract(合同中心)
- [x] types/business/contract.d.ts
- [x] api/contract.ts(getContractList、getContractDetail、getSignQr、refreshSignQr)`// TODO(api)`
- [x] stores/modules/contract.ts(useContractStore:列表/详情/签署二维码倒计时)
- [x] views/contract/index.vue + detail.vue + sign.vue + index.ts + cpns/(ContractTable、ContractInfoCard、ContractSignPanel、SignSteps)
- [x] 公共组件 QrCodePanel、StatusTag 复用接入
- [x] router 注册 `/contract`、`/contract/:id`、`/contract/:id/sign`
- [x] 复用检查 + `pnpm build` 通过
### M7 finance(财务中心)
- [x] types/business/finance.d.ts
- [x] api/finance.ts(getFinanceStats、getBillList、getBillDetail、createPayment、getPayResult、getInvoiceList、applyInvoice、sendInvoiceEmail、getInvoiceTitles、createInvoiceTitle、updateInvoiceTitle、deleteInvoiceTitle、setDefaultTitle、exportReport)`// TODO(api)`
- [x] stores/modules/finance.ts(useFinanceStore:概览统计/账单/支付/发票/抬头)
- [x] views/finance/index.vue(概览)+ bills.vue + bill-detail.vue + pay.vue + pay-result.vue + invoices.vue + invoice-titles.vue + index.ts + cpns/(FinanceOverview、FinanceChart、BillTable、BillSummaryBar、BillPayPanel、PayMethodDialog、PayResult、InvoiceTable、InvoiceTitleCard、InvoiceTitleDialog)
- [x] 公共组件 QrCodePanel、StatusTag、ConfirmDialog、PaginationBar 复用接入
- [x] router 注册 `/finance` 及 6 个子路由
- [x] 复用检查 + `pnpm build` 通过
### M8 message(消息中心)
- [x] types/business/message.d.ts
- [x] api/message.ts(getMessageList、getMessageDetail、markAllRead、markRead)`// TODO(api)`
- [x] stores/modules/message.ts(useMessageStore:列表/分类未读数/详情)
- [x] views/message/index.vue + detail.vue + index.ts + cpns/(MessageTabs、MessageList、MessageItem、NoticeDetail)
- [x] AppHeader 消息铃铛角标接入 message store
- [x] router 注册 `/message`、`/message/:id`
- [x] 复用检查 + `pnpm build` 通过
### M9 service(在线客服)
- [x] types/business/service.d.ts
- [x] api/service.ts(getChatHistory、sendChatMessage、uploadChatFile)`// TODO(api)`
- [x] stores/modules/service.ts(useServiceStore:会话/消息/发送)
- [x] views/service/index.vue + index.ts + cpns/(ChatWindow、ChatMessageItem、ChatFileCard、ChatInput)
- [x] 全局浮动「在线客服」入口接入
- [x] router 注册 `/service`
- [x] 复用检查 + `pnpm build` 通过
### M10 profile(个人中心)
- [x] types/business/profile.d.ts
- [x] api/profile.ts(updateNickname、changePassword、bindPhone、unbindPhone、bindEmail、unbindEmail、deleteAccount、verifyIdentity、getAddresses、createAddress、updateAddress、deleteAddress、setDefaultAddress、getFavorites、getQualifications、createQualification、updateQualification、deleteQualification、submitFeedback)`// TODO(api)`
- [x] stores/modules/profile.ts(useProfileStore:账号/地址/收藏/资质/反馈)
- [x] views/profile/index.vue(左菜单布局)+ security.vue + address.vue + favorites.vue + qualifications.vue + feedback.vue + help.vue + about.vue + hotline.vue + index.ts + cpns/(ProfileSider、ProfileUserCard、SecurityPanel、ChangePasswordDialog、UnbindAccountDialog、AddressCard、AddressDialog、FavoritesGrid、QualificationCard、QualificationDialog、FeedbackForm、HelpFaq、AboutPanel、HotlinePanel)
- [x] 公共组件 SmsCodeInput、PasswordStrength、ConfirmDialog、FileList 复用接入
- [x] router 注册 `/profile` 嵌套 8 子路由
- [x] 复用检查 + `pnpm build` 通过
## 收尾阶段
- [x] 阶段六:全局扫描,跨页面重复 UI 上移 components/,更新 components/index.ts barrel
- [x] 阶段七:逐页对照 PRD/原型自检(布局/交互/字段/组件拆分/规范合规:无 as any、页面不直接 import api)
- [x] `pnpm build`(vue-tsc)全量零错误
- [x] 蓝湖到位后:校准 variables.less 色值/字号/间距,替换切图,逐页对照设计稿修正(主色 `#276fff→#1f67ef`、内容强调 `#024fff`、灰阶对齐 `#1f2937/#4b5563/#6b7280/#9ca3af`、危险 `#ef4444`、成功 `#16a34a`、顶栏 `#f4f5f9`+`#d8d8d8`、页脚 `#0c1321`;AppHeader/AppFooter 按设计重写;10 个模块页面视觉按标注补齐,`<script>` 逻辑块零改动;26 条路由 QA 无横向溢出/无同色不可见文字;`pnpm build` 零错误)
- [ ] 仍无设计稿的 5 个路由(保持现状,待补设计后再次校准):`/project` 列表、`/cases/:id` 案例详情、`/finance/invoices` 发票申请、`/profile/favorites` 收藏列表、`/profile/hotline` 专属客服热线
- [ ] 接口文档到位后:逐 api 文件替换 `// TODO(api)` 为真实路径/入参出参,走真实联调 → 补打 [x]
## 完成度速查
| 单元 | 状态 |
| --- | --- |
| P0 基础工程 | [x] |
| M1 home | [x] |
| M2 cases | [x] |
| M3 demand | [x] |
| M4 login | [x] |
| M5 project | [x] |
| M6 contract | [x] |
| M7 finance | [x] |
| M8 message | [x] |
| M9 service | [x] |
| M10 profile | [x] |
| 阶段六 公共组件提取 | [x] |
| 阶段七 验证修正 | [x] |
| 蓝湖视觉补齐 | [x] |
| 接口联调补齐 | [!] |

View File

@@ -1,206 +0,0 @@
# 页面架构规划 — cz-web(auto-dev 阶段三产出)
> 依据 `docs/PRD.md` 梳理。规范遵循 vue-project-structure skill:三层数据流(页面 → store → api)、四者同名(views/api/store/types)、less 样式、`@/` 别名。
## 1. 页面清单表(模块 → 页面 → 功能点 → 数据字段)
### 模块 home(首页)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 首页 `/` | 欢迎区、数据看板、快捷入口、业务服务卡片、案例精选、服务 Tab、页脚 | stats{pending,inProgress,total}、quickLinks[]、businessLines[]、cases[] |
| 业务详情 `/business/:type` | 面包屑、业务介绍、服务内容富文本+视频、服务类型子 Tab | businessLine{id,name,slogan,serviceContent,serviceTypes[],video} |
### 模块 cases(案例库)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 案例列表 `/cases` | 四维筛选(业务类型/行业/规模/完成时间)+关键字、已选条件回显、案例卡片网格、分页 | Case(见 PRD 4.1)、CaseQuery{category,industries[],scales[],dateRange,keyword,page} |
| 案例详情 `/cases/:id` | 概览、收藏/分享、视频、富文本四段、一键填报类似案例 | Case 全量、isFavorite |
### 模块 demand(需求提报)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 需求提报 `/demand` | 三步表单:业务类型单选→需求信息+附件→确认备注;立即提报/保存草稿/重置 | Demand(见 PRD 4.2) |
### 模块 login(认证)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 登录 `/login` | 账号密码/手机验证码双模式、游客模式、协议勾选 | account,password,phone,smsCode,agreeTerms |
| 注册 `/register` | 手机+验证码+密码(强度指示)+确认密码、协议勾选 | phone,smsCode,password,confirmPassword |
| 找回密码 `/reset-password` | 手机+验证码+新密码+确认、确认修改 | phone,smsCode,newPassword,confirmPassword |
### 模块 project(项目中心)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 项目列表 `/project` | 三维筛选+关键字、状态 Tab、项目表格、分页 | Project(PRD 4.3)、ProjectQuery{businessType,status,dateRange,keyword,page} |
| 项目详情 `/project/:id` | 信息卡、项目团队、四 Tab(进度里程碑/项目文件/合同财务/售后信息) | Project 全量(milestones,team,files,paymentPlans,invoiceRecords,afterSales) |
| 申请售后 `/project/:id/after-sales` | 三步表单(业务类型/问题描述+附件/备注)、提交生成工单 | AfterSales{type,description,attachments,remark} |
### 模块 contract(合同中心)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 合同列表 `/contract` | 关键字搜索、合同表格、去签署/详情、分页 | Contract(PRD 4.4)、ContractQuery{keyword,page} |
| 合同详情 `/contract/:id` | 合同信息只读表单、关联项目跳转、合同文件、返回 | Contract 全量 |
| 签署合同 `/contract/:id/sign` | 支付宝扫码二维码(04:32 倒计时+刷新)、四步签署指引、完成自动跳转 | signQr{url,expiresAt} |
### 模块 finance(财务中心)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 财务概览 `/finance` | 累计消费总额+月份/季度/年度切换、业务类型分布图、待支付角标、快捷入口 | financeStats{totalExpense,month,distribution[],pendingCount} |
| 账单明细 `/finance/bills` | 状态 Tab、搜索、账单表格(多选)、汇总条、批量立即支付、分页 | Bill(PRD 4.5)、BillQuery{status,keyword,page} |
| 账单详情 `/finance/bills/:id` | 订单信息卡、待支付/已支付/未到期三态展示、返回 | Bill 全量、Payment |
| 扫码支付 `/finance/bills/:id/pay` | 支付二维码(倒计时+刷新)、微信/支付宝切换弹窗、订单明细、三步指引 | Payment{method,qrCode,expiresAt,amount} |
| 支付结果 `/finance/bills/:id/pay/result` | 支付成功卡、支付信息、已支付订单列表 | Payment 结果 |
| 发票申请 `/finance/invoices` | 默认抬头卡、状态 Tab、发票表格、申请开票/预览/下载/发送邮箱、分页 | Invoice(PRD 4.5) |
| 发票抬头管理 `/finance/invoice-titles` | 抬头列表卡、新增/编辑/删除(二次确认)、设为默认 | InvoiceTitle(PRD 4.5) |
### 模块 message(消息中心)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 消息中心 `/message` | 分类 Tab(全部/在线客服/状态通知/系统通知+未读数)、搜索、全部已读、消息列表、分页 | Message(PRD 4.6)、MessageQuery{type,keyword,page} |
| 通知详情 `/message/:id` | 标题/发布时间/富文本正文/附件、返回 | Message 全量 |
### 模块 service(在线客服)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 在线客服 `/service` | 聊天窗、历史消息分割线、未读标记、文本/文件消息、Enter 发送、拖拽上传 | ChatMessage{id,role,content,file?,time} |
### 模块 profile(个人中心,左菜单嵌套布局)
| 页面 | 功能点 | 数据字段 |
| --- | --- | --- |
| 账号安全 `/profile/security` | 修改密码、绑定手机换绑、绑定/解绑邮箱、注销账号(条件校验→风险提示→身份验证→二次确认) | User(PRD 4.7) |
| 地址管理 `/profile/address` | 地址卡列表、新增/编辑/删除(二次确认)、设为默认、分页 | Address(PRD 4.7) |
| 收藏列表 `/profile/favorites` | 收藏案例卡片网格 + 筛选、分页 | Case[](复用 cases 实体) |
| 资质列表 `/profile/qualifications` | 资质卡列表、新增/编辑/删除/重新上传、审核状态、分页 | Qualification(PRD 4.7) |
| 意见反馈 `/profile/feedback` | 标题+描述+附件表单、提交反馈/重置 | Feedback{title,description,attachments} |
| 帮助中心 `/profile/help` | FAQ 折叠组(三类 Q/A) | FaqGroup[](静态) |
| 关于我们 `/profile/about` | 简介/使命/核心优势/联系信息/版本信息 | 静态+site 配置 |
| 专属客服热线 `/profile/hotline` | 热线电话+工作时间 | 静态 |
## 2. 路由规划(`src/router/routes.ts`)
```ts
export const routes = [
// ── 公开 ──
{ path: '/', name: 'Home', component: () => import('@/views/home'), meta: { title: '首页' } },
{ path: '/business/:type', name: 'BusinessDetail', component: () => import('@/views/home/detail.vue'), meta: { title: '业务详情' } },
{ path: '/cases', name: 'Cases', component: () => import('@/views/cases'), meta: { title: '成功案例库' } },
{ path: '/cases/:id', name: 'CaseDetail', component: () => import('@/views/cases/detail.vue'), meta: { title: '案例详情' } },
{ path: '/demand', name: 'Demand', component: () => import('@/views/demand'), meta: { title: '需求提报' } },
// ── 游客区 ──
{ path: '/login', name: 'Login', component: () => import('@/views/login'), meta: { title: '登录' } },
{ path: '/register', name: 'Register', component: () => import('@/views/login/register.vue'), meta: { title: '注册' } },
{ path: '/reset-password', name: 'ResetPassword', component: () => import('@/views/login/reset.vue'), meta: { title: '找回密码' } },
// ── 登录区(需守卫)──
{ path: '/project', name: 'Project', component: () => import('@/views/project'), meta: { title: '项目中心', requiresAuth: true } },
{ path: '/project/:id', name: 'ProjectDetail', component: () => import('@/views/project/detail.vue'), meta: { title: '项目详情', requiresAuth: true } },
{ path: '/project/:id/after-sales', name: 'AfterSales', component: () => import('@/views/project/after-sales.vue'), meta: { title: '申请售后', requiresAuth: true } },
{ path: '/contract', name: 'Contract', component: () => import('@/views/contract'), meta: { title: '合同中心', requiresAuth: true } },
{ path: '/contract/:id', name: 'ContractDetail', component: () => import('@/views/contract/detail.vue'), meta: { title: '合同详情', requiresAuth: true } },
{ path: '/contract/:id/sign', name: 'ContractSign', component: () => import('@/views/contract/sign.vue'), meta: { title: '签署合同', requiresAuth: true } },
{ path: '/finance', name: 'Finance', component: () => import('@/views/finance'), 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/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/invoices', name: 'Invoices', component: () => import('@/views/finance/invoices.vue'), meta: { title: '发票申请', requiresAuth: true } },
{ path: '/finance/invoice-titles', name: 'InvoiceTitles', component: () => import('@/views/finance/invoice-titles.vue'), meta: { title: '发票抬头', requiresAuth: true } },
{ path: '/message', name: 'Message', component: () => import('@/views/message'), meta: { title: '消息中心', requiresAuth: true } },
{ path: '/message/:id', name: 'MessageDetail', component: () => import('@/views/message/detail.vue'), meta: { title: '通知详情', requiresAuth: true } },
{ path: '/service', name: 'Service', component: () => import('@/views/service'), meta: { title: '在线客服', requiresAuth: true } },
// ── 个人中心(左菜单布局嵌套)──
{
path: '/profile', component: () => import('@/views/profile'), meta: { title: '个人中心', requiresAuth: true },
children: [
{ path: '', redirect: '/profile/security' },
{ path: 'security', name: 'ProfileSecurity', component: () => import('@/views/profile/security.vue'), meta: { title: '账号安全', requiresAuth: true } },
{ path: 'address', name: 'ProfileAddress', component: () => import('@/views/profile/address.vue'), meta: { title: '地址管理', requiresAuth: true } },
{ path: 'favorites', name: 'ProfileFavorites', component: () => import('@/views/profile/favorites.vue'), meta: { title: '收藏列表', requiresAuth: true } },
{ path: 'qualifications', name: 'ProfileQualifications', component: () => import('@/views/profile/qualifications.vue'), meta: { title: '资质列表', requiresAuth: true } },
{ path: 'feedback', name: 'ProfileFeedback', component: () => import('@/views/profile/feedback.vue'), meta: { title: '意见反馈', requiresAuth: true } },
{ path: 'help', name: 'ProfileHelp', component: () => import('@/views/profile/help.vue'), meta: { title: '帮助中心', requiresAuth: true } },
{ path: 'about', name: 'ProfileAbout', component: () => import('@/views/profile/about.vue'), meta: { title: '关于我们', requiresAuth: true } },
{ path: 'hotline', name: 'ProfileHotline', component: () => import('@/views/profile/hotline.vue'), meta: { title: '专属客服热线', requiresAuth: true } },
],
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue'), meta: { title: '页面不存在' } },
]
```
> 路由守卫(`router/guard.ts`):`meta.requiresAuth` 未登录 → 重定向 `/login`;登录页已登录 → 重定向 `/`;切换路由时设置 `document.title`。
## 3. 四者同名映射表(模块 → 文件)
| 模块 | views/ | api/ | stores/modules/ | types/business/ | Store 名 |
| --- | --- | --- | --- | --- | --- |
| home | `views/home/`(index.vue + detail.vue) | `api/home.ts` | `stores/modules/home.ts` | `types/business/home.d.ts` | useHomeStore |
| cases | `views/cases/`(index.vue + detail.vue) | `api/cases.ts` | `stores/modules/cases.ts` | `types/business/cases.d.ts` | useCasesStore |
| demand | `views/demand/` | `api/demand.ts` | `stores/modules/demand.ts` | `types/business/demand.d.ts` | useDemandStore |
| login | `views/login/`(index.vue + register.vue + reset.vue) | `api/auth.ts` | `stores/modules/user.ts` | `types/business/user.d.ts` | useUserStore |
| project | `views/project/`(index.vue + detail.vue + after-sales.vue) | `api/project.ts` | `stores/modules/project.ts` | `types/business/project.d.ts` | useProjectStore |
| contract | `views/contract/`(index.vue + detail.vue + sign.vue) | `api/contract.ts` | `stores/modules/contract.ts` | `types/business/contract.d.ts` | useContractStore |
| finance | `views/finance/`(index.vue + bills.vue + bill-detail.vue + pay.vue + pay-result.vue + invoices.vue + invoice-titles.vue) | `api/finance.ts` | `stores/modules/finance.ts` | `types/business/finance.d.ts` | useFinanceStore |
| message | `views/message/`(index.vue + detail.vue) | `api/message.ts` | `stores/modules/message.ts` | `types/business/message.d.ts` | useMessageStore |
| service | `views/service/` | `api/service.ts` | `stores/modules/service.ts` | `types/business/service.d.ts` | useServiceStore |
| profile | `views/profile/`(index.vue 布局 + 8 子页) | `api/profile.ts` | `stores/modules/profile.ts` | `types/business/profile.d.ts` | useProfileStore |
| 站点公共 | — | — | `stores/modules/site.ts` | — | useSiteStore(全局 loading/站点配置) |
## 4. 复用性分析
### 4.1 跨页面复用 → `src/components/`
| 组件 | 复用范围 | 判定依据 |
| --- | --- | --- |
| AppHeader / AppFooter | 全站 | layout 挂载 |
| AppButton / AppSwiper | 全站 | 通用基础件 |
| CaseCard | 首页精选、案例库、收藏列表 | ≥3 页面 |
| FilterPanel(含已选条件回显) | 案例库、项目中心、收藏列表 | ≥2 页面 |
| SearchBar | 案例/项目/合同/账单/发票/消息 | ≥2 页面 |
| PaginationBar | 所有列表页 | ≥2 页面 |
| FileList + UploadArea | 需求填报、项目文件、售后、资质、意见反馈 | ≥2 页面 |
| SmsCodeInput | 登录/注册/找回/换绑手机/换绑邮箱/注销验证 | ≥2 页面 |
| PasswordStrength | 注册、修改密码 | ≥2 页面 |
| QrCodePanel | 合同签署、扫码支付 | ≥2 页面 |
| StatusTag | 项目/合同/账单/发票/资质状态 | ≥2 页面 |
| ConfirmDialog | 删除/注销等二次确认 | ≥2 页面 |
| StepForm 骨架 | 需求提报、申请售后 | ≥2 页面 |
### 4.2 页面独有 → `views/<page>/cpns/`
详见 `docs/PRD.md` 第 5.2 节组件清单。判定:仅 1 个页面使用即入 cpns/,严禁跨页面 import。
### 4.3 逻辑复用
| 逻辑 | 归属 | 使用方 |
| --- | --- | --- |
| useCountDown(验证码倒计时/二维码失效倒计时) | composables/ | SmsCodeInput、QrCodePanel |
| useLoading | composables/ | 各 store 请求态(可选) |
| useScroll(吸顶/回到顶部) | composables/ | 长列表页 |
| useIntersection(懒加载) | composables/ | 案例卡片图片 |
| v-lazy / v-debounce / v-throttle | directives/ | 图片懒加载、搜索防抖 |
| format.ts(金额/日期/千分位) | utils/ | 财务/项目展示 |
| validate.ts(手机号/邮箱) | utils/ | 表单校验 |
| storage.ts(token/草稿缓存) | utils/ | user store、需求草稿 |
## 5. 静态资源规划
```
src/assets/
├── images/ # 案例封面、业务卡片图、banner(原型导出切图后语义化命名)
├── icons/ # 业务类型图标、消息类型图标、状态图标
└── fonts/ # 如需自定义字体
```
> 切图来源:蓝湖到位前使用原型占位图;到位后按蓝湖图层语义化命名替换。

View File

@@ -1,27 +0,0 @@
// @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)

View File

@@ -85,6 +85,11 @@ const coreBusiness = [
<style lang="less" scoped> <style lang="less" scoped>
.app-footer { .app-footer {
// 设计稿:footer 顶 y=1942,总高 481px。各行绝对坐标换算为相对 footer 顶:
// 列标题 54 / 内容首行 115 / legal[0] 332 / legal[1] 371。
min-height: @footer-height;
display: flex;
flex-direction: column;
background: @footer-bg; background: @footer-bg;
color: @white; color: @white;
@@ -95,7 +100,7 @@ const coreBusiness = [
margin: 0 auto; margin: 0 auto;
height: 250px; height: 250px;
box-sizing: border-box; box-sizing: border-box;
padding-top: 30px; padding-top: 54px;
padding-left: clamp( padding-left: clamp(
@spacing-base, @spacing-base,
calc((100% - 1609px) / 2), calc((100% - 1609px) / 2),
@@ -182,6 +187,8 @@ const coreBusiness = [
margin: 0 auto; margin: 0 auto;
padding-bottom: 20px; padding-bottom: 20px;
text-align: left; text-align: left;
// legal[0] 需落在相对 footer 顶 332px 处
margin-top: 62px;
} }
&__legal { &__legal {

View File

@@ -84,25 +84,16 @@ onMounted(() => void messageStore.fetchUnreadCount())
&__inner { &__inner {
.flex(row, flex-start); .flex(row, flex-start);
height: 100%; height: 100%;
// padding-left: clamp(@spacing-base, calc((100% - 1600px) / 2), 155px); // 设计稿 1920 基准:导航首项左 275px,操作区右边界 1800px(右侧留白 120px)。
padding-left: 15%; // 视口窄于 1920 时按留白收缩,避免两端顶死。
// padding-right: clamp(@spacing-base, calc((100% - 1680px) / 2), 120px); padding-left: clamp(@spacing-base, calc((100% - 1370px) / 2), 275px);
padding-right: 15%; padding-right: clamp(@spacing-base, calc((100% - 1680px) / 2), 120px);
} }
&__nav { &__nav {
.flex(row, center); .flex(row, center);
gap: @header-nav-gap; gap: @header-nav-gap;
height: 100%; height: 100%;
margin-left: 0px;
@media (max-width: 1600px) {
gap: 48px;
}
@media (max-width: 1280px) {
gap: 32px;
}
&-item { &-item {
position: relative; position: relative;

View File

@@ -21,11 +21,7 @@ const showFooter = computed(() => !route.meta.noFooter)
<style lang="less" scoped> <style lang="less" scoped>
.layout { .layout {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
min-height: calc(100vh / @page-zoom); min-height: 100vh;
@media (max-width: 1286px) {
min-height: calc(100vh / @page-zoom--sm);
}
&__main { &__main {
flex: 1; flex: 1;

View File

@@ -10,7 +10,8 @@
.container--home { .container--home {
padding: 0 @home-gutter; padding: 0 @home-gutter;
@media (max-width: 1919px) { // 1:1 还原基准为 1920。仅当视口明显窄于 1920 时才收窄留白。
@media (max-width: 1600px) {
padding: 0 @spacing-lg; padding: 0 @spacing-lg;
} }
} }
@@ -18,13 +19,9 @@
.auth-page { .auth-page {
position: relative; position: relative;
display: flex; display: flex;
min-height: calc(100vh / @page-zoom); min-height: 100vh;
background: @white; background: @white;
@media (max-width: 1286px) {
min-height: calc(100vh / @page-zoom--sm);
}
&__brand { &__brand {
.flex(column, flex-start, center); .flex(column, flex-start, center);
flex: 0 0 @login-brand-width; flex: 0 0 @login-brand-width;

View File

@@ -1,10 +1,8 @@
@import './reset.less'; @import './reset.less';
@import './common.less'; @import './common.less';
// 视觉基准:蓝湖设计稿 1920px,1:1 还原,不做整体缩放。
// 如需适配窄屏,请在各页面内部用媒体查询处理,不要在根元素缩放。
html { html {
zoom: @page-zoom; zoom: 1;
}
@media (max-width: 1286px) {
zoom: @page-zoom--sm;
}
}

View File

@@ -56,8 +56,10 @@
@container-width: 1920px; @container-width: 1920px;
@page-zoom: 0.67; // 视觉基准:蓝湖设计稿 1920px,1:1 还原,根元素不做缩放(html { zoom: 1 })。
@page-zoom--sm: 0.66; // @page-zoom 仅作为「设计稿 px 与实际渲染 px」的换算说明保留,实际值为 1。
@page-zoom: 1;
@page-zoom--sm: 1;
@page-content: 1780px; @page-content: 1780px;
@page-gutter: 70px; @page-gutter: 70px;

View File

@@ -241,15 +241,17 @@ function clearChips(): void {
> >
{{ shortcut.label }} {{ shortcut.label }}
</button> </button>
<ElDatePicker <div class="case-filter__date-wrap">
v-model="dateRange" <ElDatePicker
class="case-filter__date" v-model="dateRange"
type="daterange" class="case-filter__date"
range-separator="—" type="daterange"
start-placeholder="开始时间" range-separator="—"
end-placeholder="结束时间" start-placeholder="开始时间"
value-format="YYYY-MM-DD" end-placeholder="结束时间"
/> value-format="YYYY-MM-DD"
/>
</div>
</div> </div>
</section> </section>
@@ -433,16 +435,37 @@ function clearChips(): void {
} }
} }
&__date { // ElDatePicker 有两层元素:外层 div.case-filter__date 与内部 .el-range-editor,
// 真正控制盒子尺寸的是内层。所以尺寸/背景/圆角都要用 :deep() 打到内层,
// 且不能写在 &__date 上——那样只命中外层,视觉上看不到变化。
&__date-wrap {
grid-column: 1 / -1; grid-column: 1 / -1;
display: flex;
justify-content: center;
width: 100%; width: 100%;
:deep(.case-filter__date),
:deep(.case-filter__date .el-range-editor) {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
height: 53px;
padding: 0 @spacing-sm;
border-radius: @radius-base;
background: @input-bg;
box-sizing: border-box;
box-shadow: none;
justify-content: center;
}
}
&__date {
justify-content: center;
min-width: 0; min-width: 0;
height: 53px; height: 53px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @input-bg; background: @input-bg;
box-sizing: border-box;
box-shadow: none; box-shadow: none;
&:hover, &:hover,
@@ -469,6 +492,8 @@ function clearChips(): void {
} }
:deep(.el-range-separator) { :deep(.el-range-separator) {
flex: 0 0 auto;
margin: 0 @spacing-xs;
color: @cases-text; color: @cases-text;
font-size: 20px; font-size: 20px;
line-height: 53px; line-height: 53px;

View File

@@ -17,10 +17,6 @@ const emit = defineEmits<{
<template> <template>
<div class="cases-grid"> <div class="cases-grid">
<p class="cases-grid__total">
{{ total }}
</p>
<div v-if="cases.length > 0" class="cases-grid__list"> <div v-if="cases.length > 0" class="cases-grid__list">
<CaseCard <CaseCard
v-for="item in cases" v-for="item in cases"
@@ -39,17 +35,10 @@ const emit = defineEmits<{
<style lang="less" scoped> <style lang="less" scoped>
.cases-grid { .cases-grid {
&__total {
font-size: 20px;
font-weight: 700;
color: @heading-color;
}
&__list { &__list {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 23px 28px; gap: 23px 28px;
margin-top: 12px;
} }
&__empty { &__empty {

View File

@@ -60,14 +60,18 @@ const keyword = computed<string>({
.case-search { .case-search {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 24px; gap: 24px;
width: 100%;
height: 128px; height: 128px;
box-sizing: border-box; box-sizing: border-box;
padding: 0 36px; // 设计稿 #31:输入框 x=653(栏左 606 + 47),重置 1305,搜索 1429
padding: 0 47px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__field { &__field {
position: relative; position: relative;
// 输入框右边界 1245 → 重置按钮 1305,设计稿间距 60
margin-right: 36px;
flex: 0 0 592px; flex: 0 0 592px;
} }

View File

@@ -119,8 +119,12 @@ onMounted(() => {
background: @white; background: @white;
} }
// 设计稿 #31:hero 标题 x=112、副标题 x=112;
// 主体筛选栏 x=70、右侧操作区右边界 1850。两套留白并存,
// 所以 hero 内容单独内缩,不动全局 .container 的 70px 留白。
&__hero-inner { &__hero-inner {
padding-left: 42px; padding-left: 112px;
padding-right: 112px;
} }
&__title { &__title {
@@ -146,7 +150,10 @@ onMounted(() => {
} }
&__main { &__main {
.flex(column, flex-start); // 设计稿:右侧操作区 x=606 w=1244(右边界 1850,与筛选栏区留白 70 对称)。
// mixin 签名是 .flex(direction; justify; align),第三参才是 align-items。
// 纵向排列时 justify 无意义,align 用 stretch 让子项撑满 1244。
.flex(column, flex-start, stretch);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 24px; gap: 24px;

View File

@@ -12,6 +12,10 @@ const router = useRouter()
const activeIndex = ref(0) const activeIndex = ref(0)
let timer: ReturnType<typeof setInterval> | null = null let timer: ReturnType<typeof setInterval> | null = null
// 后端未下发banner 时退回内置插图。插图在设计稿中并非铺满背景,
// 而是定位在右侧 (928,104) 尺寸 723x598,需按原比例显示。
const hasBanners = computed(() => props.banners.length > 0)
const slides = computed(() => { const slides = computed(() => {
if (props.banners.length > 0) { if (props.banners.length > 0) {
return props.banners.map((item) => item.imageUrl) return props.banners.map((item) => item.imageUrl)
@@ -64,7 +68,12 @@ onBeforeUnmount(stopAutoplay)
class="home-banner__slide" class="home-banner__slide"
:class="{ 'home-banner__slide--active': index === activeIndex }" :class="{ 'home-banner__slide--active': index === activeIndex }"
> >
<img class="home-banner__image" :src="src" alt=""> <img
class="home-banner__image"
:class="{ 'home-banner__image--inline': !hasBanners }"
:src="src"
alt=""
>
</div> </div>
</div> </div>
@@ -128,6 +137,19 @@ onBeforeUnmount(stopAutoplay)
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
// 内置插图:按设计稿定位与尺寸原样显示,不铺满
&--inline {
position: absolute;
top: 31px;
right: 269px;
left: auto;
bottom: auto;
width: 723px;
height: 598px;
margin-left: auto;
object-fit: contain;
}
} }
&__inner { &__inner {
@@ -216,25 +238,25 @@ onBeforeUnmount(stopAutoplay)
&__dots { &__dots {
position: absolute; position: absolute;
right: 80px; left: 929px;
bottom: 40px; top: 594px;
z-index: 1; z-index: 1;
.flex(row, flex-end, center); .flex(row, flex-start, center);
gap: 8px; gap: 13px;
} }
&__dot { &__dot {
width: 8px; width: 5px;
height: 8px; height: 5px;
padding: 0; padding: 0;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
background: fade(@home-banner-dot, 40%); background: @home-banner-dot;
cursor: pointer; cursor: pointer;
&--active { &--active {
width: 24px; width: 44px;
border-radius: 4px; border-radius: 3px;
background: @home-banner-dot; background: @home-banner-dot;
} }
} }

View File

@@ -1,10 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BusinessType } from '@/types/api' 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[]
@@ -12,13 +8,7 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const FALLBACK_ILL: Record<BusinessType, string> = { const FALLBACK_ACTIVE: BusinessType[] = ['internet']
internet: illInternet,
hardware: illHardware,
iot: illIot,
manufacturing: illManufacturing,
other: illIot,
}
</script> </script>
<template> <template>
@@ -31,13 +21,10 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
:key="line.id" :key="line.id"
:to="`/business/${line.id}`" :to="`/business/${line.id}`"
class="home-business__card" class="home-business__card"
:class="`home-business__card--${line.id}`" :class="{
'home-business__card--active': FALLBACK_ACTIVE.includes(line.id),
}"
> >
<img
class="home-business__ill"
:src="line.icon || FALLBACK_ILL[line.id]"
:alt="line.name"
/>
<span class="home-business__check" aria-hidden="true" /> <span class="home-business__check" aria-hidden="true" />
<h4 class="home-business__name"> <h4 class="home-business__name">
{{ line.name }} {{ line.name }}
@@ -76,6 +63,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
min-width: 0; min-width: 0;
height: 206px; height: 206px;
box-sizing: border-box; box-sizing: border-box;
padding: 0 20px;
overflow: hidden; overflow: hidden;
border: 1px solid @home-business-border; border: 1px solid @home-business-border;
border-radius: @radius-base; border-radius: @radius-base;
@@ -90,19 +78,10 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
} }
} }
&__ill {
position: absolute;
top: 4px;
left: 48px;
width: 81px;
height: 111px;
object-fit: contain;
}
&__check { &__check {
position: absolute; position: absolute;
top: 24px; top: 27px;
right: 20px; right: 37px;
flex-shrink: 0; flex-shrink: 0;
width: 31px; width: 31px;
height: 31px; height: 31px;
@@ -113,8 +92,8 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 18px; width: 21px;
height: 18px; height: 21px;
margin: auto; margin: auto;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
@@ -123,7 +102,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
} }
&__name { &__name {
margin: 106px 0 0; margin: 104px 0 0;
font-size: @font-size-title; font-size: @font-size-title;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 41px;
@@ -139,7 +118,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
.multi-ellipsis(1); .multi-ellipsis(1);
} }
&__card--internet { &__card--active {
border-color: @primary-strong; border-color: @primary-strong;
background: @primary-strong; background: @primary-strong;

View File

@@ -173,13 +173,17 @@ function onMore(): void {
position: relative; position: relative;
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex-shrink: 0; flex-shrink: 0;
flex: 0 1 722px; flex: 0 0 529px;
width: 529px;
height: 245px;
box-sizing: border-box;
min-width: 0; min-width: 0;
gap: 0; gap: 0;
padding: 18px 40px 28px; 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;
overflow: hidden;
&__head { &__head {
.flex(row, flex-start); .flex(row, flex-start);

View File

@@ -59,15 +59,17 @@ onMounted(() => {
&__dashboard { &__dashboard {
.flex(row, space-between, stretch); .flex(row, space-between, stretch);
gap: 36px; gap: 60px;
} }
&__dashboard-main { &__dashboard-main {
.flex(row, space-between, center); .flex(row, space-between, center);
flex: 1; flex: 0 0 1091px;
width: 1091px;
min-width: 0; min-width: 0;
box-sizing: border-box;
gap: @spacing-lg; gap: @spacing-lg;
padding: 0 73px 0 106px; padding: 0 59px 0 105px;
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

@@ -177,7 +177,7 @@ async function onGetCode(target: string): Promise<void> {
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
height: calc(2px / @page-zoom); height: 2px;
background: @primary-strong; background: @primary-strong;
} }
} }

View File

@@ -25,9 +25,14 @@ export default defineConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
less: { less: {
// 自动注入变量与混合宏,组件内可直接使用 @primary-color 等 // 自动注入变量与混合宏,组件内可直接使用 @primary-color 等。
additionalData: // 用绝对路径而非 "@/styles/xxx":Less 会把 "@/..." 当成 less 变量声明,
'@import "@/styles/variables.less";\n@import "@/styles/mixins.less";', // 清过 vite 缓存后别名解析失败会导致整条 additionalData 静默失效,
// 表现为所有组件编译报 "Unknown word .flex"。
additionalData: [
`@import "${fileURLToPath(new URL('./src/styles/variables.less', import.meta.url))}";`,
`@import "${fileURLToPath(new URL('./src/styles/mixins.less', import.meta.url))}";`,
].join('\n'),
}, },
}, },
}, },