Compare commits

...

12 Commits

Author SHA1 Message Date
Ironsp
f7722e19df fix skill 2026-10-14 14:19:12 +08:00
Ironsp
0b974bd36a feat 登录页面整改 2026-10-13 16:13:56 +08:00
Ironsp
a067fd8357 feat 在线客服和详细中心整改 2026-10-13 16:08:55 +08:00
Ironsp
367380e44c feat 个人中心_帮助中心整改 2026-10-13 15:56:12 +08:00
Ironsp
77dbabb1f1 feat 个人中心_地址整改 2026-10-13 15:48:57 +08:00
Ironsp
f917062d76 feat 个人中心_地址整改 2026-10-13 15:45:50 +08:00
Ironsp
093cf9acd0 feat 首页_业务服务整改 2026-10-13 15:42:08 +08:00
Ironsp
5fa940e9ba feat 案例库样式整改 2026-10-13 15:34:55 +08:00
Ironsp
f552dd370c feat 公共样式整改 2026-10-13 15:20:51 +08:00
Ironsp
fe9492ab69 feat 个人中心样式整改 2026-10-13 15:16:13 +08:00
Ironsp
d0779fe37c feat 案例样式整改 2026-10-13 15:14:47 +08:00
Ironsp
b0e9fe6450 feat 样式整改 2026-10-13 15:12:51 +08:00
87 changed files with 2464 additions and 2152 deletions

View File

@@ -1,382 +1,335 @@
--- ---
name: vue-project-structure name: vue-project-structure
description: cz-web 项目(Vue 3 + TypeScript + Vite 网页端站点)的目录结构与编码规范强制 skill。当在本项目中新增、创建、移动、重构或修改任何 src/ 下的代码文件(.vue/.ts/.less)、路由、store、API、组件、页面时,必须加载本 skill,先读取项目目录、再按本规范确定文件归属与写法。本项目是**网页端站点(非中后台)**:页面按名称组织(home/news/product…),接口层在 api/、数据层在 Pinia、页面只经 store 取数,样式使用**less**。凡是涉及"新文件放哪"、"页面数据怎么拿"、"页面/公共组件归属"、"命名"、"功能对应哪些文件"的任务也必须使用本 skill。 description: Vue 3 与 uni-app(Vue3) 项目的目录结构与编码规范通用 skill。当在任意 Vue 3 或 uni-app(Vue3) 项目中新增、创建、移动、重构、修改 src/ 下的代码文件(.vue/.ts/.less/.scss)、路由、Pinia store、API、组件、页面、分包时,必须先加载本 skill。核心内容:先探测项目形态(Vue3 网页端/中后台 或 uni-app 小程序),再套用对应目录骨架;页面统一为「index.vue + cpns/」两件套(不使用 index.ts barrel);数据流固定为 页面 → store → api 单向;含 pages.json 注册与分包规范。凡是涉及"新文件放哪""页面数据怎么拿""公共组件 vs 页面私有组件归属""命名""分包""功能对应哪些文件"的任务也必须使用本 skill。本 skill 与具体仓库无关,可在任何 Vue 3 / uni-app 项目中直接复用。
version: 2.0.0 version: 4.0.0
--- ---
# cz-web 项目目录结构规范(强制 · 网页端) # Vue 3项目目录结构规范(通用 · 先探测后套用)
> 完整版调试文档见 `docs/vue-project-guide.md`(其中中后台相关部分已废弃),**本 skill 为唯一现行规范,以其为准**;未覆盖场景参照 Vue 3 官方目录约定。 > 适用范围:**Vue 3**(含 uni-app 的 Vue 3 版本)。不涉及 Vue 2。
> 本 skill 与具体仓库无关:执行顺序永远是 **① 探测现状 → ② 选定骨架变体 → ③ 按规范落位**。项目内已有显式文档(`AGENTS.md`/`CLAUDE.md`、`docs/` 下的结构说明)优先级高于本 skill。
## 第一步:读取项目目录(每次加载必做) ---
动手写任何代码前,先用工具映射当前项目结构,确认正在操作的正确层级: ## 第零步:探测项目形态(每次加载必做)
1. 用 `codegraph_files` 或 `glob` 列出 `src/` 下的现行目录结构(如某目录尚不存在,应在对应职责目录创建,而不是自创新顶层目录)。 ### 0.1 读取证据
2. 读取目标文件及其同级文件,确认现有命名风格与组织方式(页面名、store 名、api 名必须一致)。
3. 只有在目录树确认完毕后,才允许创建/修改文件。
## 核心原则 | 要看什么 | 怎么看 |
| ---------- | --------------------------------------------------------------------------- |
| 依赖清单 | `package.json`:是否有 `@dcloudio/*`、`pinia`、`vue-router` |
| 是否小程序 | 存在 `pages.json`、`manifest.json`;`App.vue` 含 `onLaunch` |
| 构建工具 | `vite.config.*`(Vue 3 必配);`vue.config.*` 为 Vue CLI |
| 目录现状 | 列出 `src/`(uni-app 通常无 src 包裹,直接根目录)的实际结构 |
| 样式方案 | 现有 `.less` → less;`.scss` → scss |
| 语言 | 有 `tsconfig.json` → TypeScript |
| 目标平台 | `manifest.json` 的 `mp-weixin`/`h5`/`app` 等;`pages.json` 的 `subPackages` |
1. **三层数据流(页面 → store → api)**:页面组件只通过 **Pinia store** 获取数据;store 是数据层,负责调用 **api 层**接口取数并维护状态;api 层只发请求。**页面禁止直接 import api**。 ### 0.2 判定形态
2. **按页面/页面名组织(一页一套)**:`views/<page>/`、`api/<page>.ts`、`stores/modules/<page>.ts`、`types/business/<page>.d.ts` 四者同名对应,方便分层查看。
3. **组件两级归属**:`components/` 只放**跨页面复用的公共组件**;`views/<page>/cpns/` 放**该页面独有的组件**。
4. **网页端(非中后台)**:无侧边栏菜单/权限路由/TabsBar 体系;页面以业务(首页、新闻、产品、关于我们…)划分。
5. **样式使用 less**:全局样式为 `.less` 文件,组件内 `<style lang="less" scoped>`。
## 数据流架构(强约束) ```mermaid
graph TD
A[读 package.json + 目录] --> B{含 @dcloudio/* ?}
B -->|是| U[形态 B: uni-app Vue3]
B -->|否| D{有 layout 壳 + 权限路由?}
D -->|是| M[形态 A2: 中后台]
D -->|否| W[形态 A1: 网页端站点]
U --> U1{pages.json 有 subPackages?}
U1 -->|是| U2[大应用: 必须分包]
U1 -->|否| U3[小应用: 主包即可]
```
| 形态 | 判据 | 骨架 |
| ---------------------- | ---------------------------------------------- | ----------------------------------- |
| **A1 Vue3 网页端站点** | 无权限路由,业务按页面名划分 | 骨架 A |
| **A2 Vue3 中后台** | 有 `layout/` 壳、`permission.ts`、表格表单密集 | 骨架 A + 中后台补充 |
| **B uni-app(Vue3)** | 依赖含 `@dcloudio/*`,存在 `pages.json` | 骨架 B(**页面注册用 pages.json**) |
### 0.3 探测结论必须先复述
动手改代码前,先输出:
```
探测结论:形态=B uni-app(Vue3 + Vite + TS + Pinia + scss,含 subPackages 3 个)
页面结构:index.vue + cpns/(无 index.ts)
样式方案:scss(uni.scss 已存在)
```
探测不明确时**先问用户**,不要猜。
---
## 第一步:读取目标区域(探测后必做)
1. 列出将要操作目录的现行结构(目录不存在则新建,**不要自创新顶层目录**)。
2. 读取目标文件 + 同级 2 个文件,确认既有命名/导出/样式风格。
3. 确认该业务域"页面 / api / store / 类型"在本项目中的实际路径。
---
## 核心原则(形态无关)
1. **三层数据流:页面 → store → api**。页面只通过 store 取数;store 调用 api 并维护状态;api 只发请求。**页面禁止直接 import api**。
2. **业务同名单点**:同一业务域的 `页面目录` / `api 文件` / `store 文件` / `类型文件` 同名对应。
3. **页面两件套(无 barrel)**:页面目录 = `index.vue` + `cpns/`。**不建 `index.ts`**,路由直接指向 `@/views/<page>/index.vue`。
4. **组件两级归属**:跨 ≥2 个页面/分包复用 → 全局 `components/`;单页面独有 → 该页面的 `cpns/`。
5. **技术选型跟随现状**:用 Pinia 就不引 Vuex;用 scss 就不写 less;用 TS 就不落 `.js`。**不擅自更换技术栈**。
6. **最小改动**:只改与本次需求相关的行;无关死代码只报告、不删除。
---
## 骨架 A:Vue 3 网页端 / 中后台
```
src/
├── api/<domain>.ts # 接口层:按业务命名,一域一文件
├── assets/ # 参与构建的静态资源(images/ icons/ fonts/)
├── components/ # 跨页面复用的公共组件(禁含具体业务)
│ └── index.ts # barrel 统一导出(仅公共组件目录需要)
├── composables/ # 通用组合式逻辑(useLoading / useScroll / useCountDown)
├── directives/ # 自定义指令(lazy / debounce / throttle)+ index.ts 注册
├── layout/ # 全站骨架(网页端:Header+router-view+Footer)
├── router/ # index.ts(createRouter)、routes.ts、guard.ts
├── stores/
│ ├── index.ts # pinia 实例
│ └── modules/<domain>.ts
├── styles/ # variables / mixins / reset / common + 入口
├── types/ # api.d.ts / router.d.ts / business/<domain>.d.ts
├── utils/ # request.ts(唯一 axios 实例)/ storage / validate / format
└── views/
└── <page>/ # ★ 页面 = index.vue + cpns/
├── index.vue # 页面主组件(路由直接指向它)
├── detail.vue # 同页面模块的次级页面(如列表/详情)
└── cpns/ # 仅本页使用的组件
├── Head.vue
└── Banner.vue
```
路由写法(**不经过 index.ts**):
```ts
{
path: '/news',
name: 'News',
component: () => import('@/views/news/index.vue'), // 直接指向 .vue
meta: { title: '新闻列表' },
}
{
path: '/news/:id',
name: 'NewsDetail',
component: () => import('@/views/news/detail.vue'),
meta: { title: '新闻详情' },
}
```
### A2 中后台补充(在骨架 A 上增加)
```
src/
├── layout/
│ ├── index.vue # 后台外壳:侧边栏 + 顶栏 + tab 缓存 + <router-view/>
│ └── components/ # Sider / Header / Breadcrumb / TagsView
├── router/
│ ├── routes.ts # constantRoutes(无权限)
│ └── modules.ts # asyncRoutes(按角色过滤)
├── permission.ts # 守卫:登录态 → 白名单 → 动态路由注入
├── stores/modules/ # user(token/角色/权限)、permission(动态路由)、app(侧栏/主题)
└── views/<module>/<page>/ # 按模块再分页面,如 views/system/user/index.vue
```
- 权限:`meta: { title, icon, roles: [], perms: [] }`,由 `permission.ts` 统一处理,页面用 `hasPermi('sys:user:add')` 判断。
- 表格页统一结构:`搜索区 + 工具栏 + 表格 + 分页`,分页用公共 `Pagination` 组件。
- 动态路由必须 `router.addRoute` 注入后 `next({ ...to, replace: true })`,避免空白页。
---
## 骨架 B:uni-app(Vue 3)
```
├── pages.json # ★ 唯一页面注册入口(路径、导航栏、分包)
├── manifest.json # 应用配置(appid、平台配置、原生插件)
├── uni.scss # ★ 全局 scss 变量(uni-app 约定入口)
├── App.vue # 全局生命周期 onLaunch/onShow/onHide
├── main.ts # createApp、注册 pinia、引入全局样式
├── api/<domain>.ts # 接口层(uni.request 封装为函数)
├── stores/modules/<domain>.ts # 状态层
├── utils/
│ ├── request.ts # 封装 uni.request 的唯一请求层
│ └── storage.ts # uni.getStorageSync 封装
├── components/ # 跨页面/跨分包复用的自定义组件
├── static/ # 不参与编译的静态资源(不要用 assets)
├── composables/
├── types/
├── pages/ # ★ 主包页面
│ ├── index/
│ │ ├── index.vue
│ │ └── cpns/
│ └── login/
│ ├── index.vue
│ └── cpns/
└── subpackages/ # 分包页面(仅分包项目)
└── news/
├── detail.vue
└── cpns/
```
uni-app 硬性约定:
- 页面**必须**在 `pages.json` 的 `pages` / `subPackages[].pages` 注册,路径不带扩展名、不以 `/` 开头。
- 页面目录同样遵循 **`index.vue` + `cpns/`**,不建 `index.ts`。
- 主包体积敏感:**非首屏必需**页面一律放 `subPackages`;静态大图放 `static/`。
- 跨分包随意 import 受限;公共依赖优先放主包或做分包异步化。
- 平台差异(`#ifdef MP-WEIXIN / H5 / APP-PLUS`)集中封装并注释,禁止散落全项目。
- 用 `uni.request` / `uni.chooseImage` / `uni.navigateTo`,不混用 `wx.*`;tabBar 页用 `switchTab`(不能带参数)。
- 组件模板优先 `view/text/image`,不用 `div/span/img`。
---
## 数据流架构(强约束,两形态通用)
```text ```text
页面组件 (views/home/index.vue) 页面组件 (views/news/index.vue 或 pages/news/index.vue)
│ ① 页面只调用 store 的 action / 读取 store 的 state(模板渲染数据) │ ① 只读 store state / 调 store action
▼ ▼
Pinia store (stores/modules/home.ts) —— 数据层 状态层 (stores/modules/news.ts) —— 数据层:取数 + 状态 + 缓存 + 跨页共享
│ ② store 的 action 内部调用 api 接口函数 → await → 写入 state / 返回结果 │ ② action 内部调用 api 函数 → await → 写 state
▼ ▼
API (api/home.ts) —— 接口层 API (api/news.ts) —— 接口层:只发请求、只返回带类型的数据
│ ③ 经 utils/request.ts 发 HTTP 请求 │ ③ request.ts(axios / uni.request 封装)
▼ ▼
后端服务器 后端
``` ```
铁律: 铁律:
- **页面禁止 `import` api 层文件**;获取数据必须调对应页面 store 的方法。 - 页面**禁止** import api;取数必须走 store。
- **api 层是纯接口层**:只发请求、返回数据(带类型),不做业务状态处理、不缓存。 - api 是**纯接口层**:不发业务判断、不缓存、不 toast。
- **store 承担取数 + 数据状态 + 页面间共享 + 缓存**;与页面同名的 store 责任边界即该页面。 - store 承担取数、状态、缓存、跨页共享;本地缓存用 `storage.ts`,不直接写 `localStorage` / `uni.setStorageSync`。
- 调用链必须是 `页面 → store → api` 单向,禁止跳过 store 或反向依赖。 - 依赖方向单向:`页面 → store → api`,禁止反向与跨层直连。
## src/ 目录结构(强制划分) ---
``` ## 各层职责速查
src/
├── api/ # ★ 接口层:按页面命名,一页一文件(home.ts、news.ts、product.ts、about.ts) | 层 | 目录 | 一句话职责 | 禁止 |
├── assets/ # 参与构建的静态资源(images/、icons/、fonts/) | ------------ | --------------------------------------------------------- | ---------------------------------------- | ------------------------------ |
├── components/ # ★ 公共组件:仅跨页面复用的全站通用组件(禁含具体页面业务) | 页面 | `views/<page>/index.vue`(uni:`pages/<page>/index.vue`) | 渲染 + 交互 + 编排 | 直接调 api、直接 axios/request |
│ ├── AppHeader.vue # 全站头部(导航) | 页面私有组件 | `views/<page>/cpns/`(uni 同名) | 仅本页用 | 被其他页 import |
│ ├── AppFooter.vue # 全站页脚 | 公共组件 | `components/` | ≥2 处复用 | 含单页业务逻辑 |
│ ├── AppButton.vue # 通用按钮 | 状态层 | `stores/modules/<domain>.ts` | 取数 + 状态 + 缓存 | 拼 URL、写 UI 逻辑 |
│ ├── AppSwiper.vue # 通用轮播 | 接口层 | `api/<domain>.ts` | 请求分发 + 类型 | 状态、弹窗提示 |
│ └── index.ts # 统一导出 | 请求层 | `utils/request.ts` | 唯一实例/封装:token、统一解包、错误提示 | 被页面直接 import axios |
├── composables/ # 通用组合式逻辑(useScroll、useLoading、useCountDown、useIntersection) | 工具 | `utils/`(纯函数) / `composables/`(响应式) | — | 互相混放 |
├── directives/ # 自定义指令(lazy 图片懒加载、debounce、throttle + index.ts 统一注册)
├── layout/ # 全站布局骨架:index.vue = <AppHeader/> + <router-view/> + <AppFooter/> > **页面私有组件目录名统一用 `cpns/`**(不再用 `components/`,避免与全局 `components/` 混淆)。
├── router/ # 路由:index.ts(入口)、routes.ts(静态路由表)、guard.ts(守卫,可选)
├── stores/ # ★ 数据层(Pinia):按页面命名,一页一 store ### 组件归属判定(唯一判据)
│ ├── index.ts # pinia 实例(main.ts 中 app.use(pinia))
│ └── modules/ ```mermaid
│ ├── home.ts # useHomeStore:首页数据(banner、推荐、公告) graph TD
│ ├── news.ts # useNewsStore:新闻列表/分页/详情 A[新组件] --> B{被几个页面/分包使用?}
│ ├── product.ts # useProductStore:产品列表/详情 B -->|1 个| C[放该页面 cpns/]
│ └── site.ts # useSiteStore:站点公共数据(站点配置、全局 loading),非页面级 B -->|≥2 个| D{业务无关还是业务相关?}
├── styles/ # ★ 全局样式(less) D -->|业务无关<br/>按钮/弹窗/上传| E[全局 components/]
│ ├── index.less # 样式入口(main.ts 引入),组织以下文件 D -->|业务相关| F[全局 components/<domain>/]
│ ├── variables.less # ★ less 变量唯一来源(颜色/间距/字号/圆角),禁止组件内魔法色值 F --> G[差异用 props 注入]
│ ├── mixins.less # 混合宏(flex、文本省略、滚动条)
│ ├── reset.less # 样式重置
│ └── common.less # 通用工具类(容器宽度、间距类)
├── types/ # 类型定义
│ ├── global.d.ts # 全局声明(Window 扩展、import.meta.env 兜底)
│ ├── api.d.ts # 接口通用结构:ApiResponse<T>、PageResult<T>
│ ├── router.d.ts # RouteMeta 扩展
│ └── business/ # 按页面与 api 对应:home.d.ts / news.d.ts / product.d.ts
├── utils/ # 纯函数工具(无 Vue 依赖):request.ts、storage.ts、validate.ts、format.ts
└── views/ # ★ 页面:一页一目录
├── home/
│ ├── index.vue # 页面主组件(路由通过 index.ts 加载)
│ ├── index.ts # 页面出口(barrel):export default 页面 + 导出本页独有组件
│ └── cpns/ # 页面独有组件(head、banner、news-list 等,仅本页面使用)
│ ├── Head.vue
│ └── Banner.vue
├── news/
│ ├── index.vue # 列表页
│ ├── detail.vue # 详情页
│ ├── index.ts
│ └── cpns/
├── product/ # 同上结构
├── about/ # 同上结构
└── login/ # 登录(仅存在会员体系时)
``` ```
**命名一致性(可分层互查)**:页面 `home` → `api/home.ts` + `stores/modules/home.ts`(`useHomeStore`) + `types/business/home.d.ts` + `views/home/`,四者同名。 ### 命名一致性
## src/ 每个文件的作用(新建/修改文件前先对照本节定位) 同一业务域 `news` 的四层落位:
### 入口与根文件 | 层 | Vue 形态 | uni-app 形态 |
| -------- | ----------------------------------------- | ------------------------------------------ |
| 页面 | `views/news/index.vue` | `pages/news/index.vue` + `pages.json` 注册 |
| 私有组件 | `views/news/cpns/` | `pages/news/cpns/` |
| 接口 | `api/news.ts` | `api/news.ts` |
| 状态 | `stores/modules/news.ts` → `useNewsStore` | 同左 |
| 类型 | `types/business/news.d.ts` | `types/business/news.d.ts` |
| 文件 | 作用 | ---
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `src/main.ts` | 应用入口:`createApp(App)`,注册 `pinia`、`router`、自定义指令(`setupDirectives`)、引入全局 `styles/index.less`。不写业务逻辑。 |
| `src/App.vue` | 根组件,承载 `<router-view/>` 与全局过渡;不含业务代码。 |
| `src/env.d.ts` | Vite 环境变量类型声明(`ImportMetaEnv` / `ImportMeta`)。 |
### `api/` —— 接口层(一页一文件,与页面同名) ## 规范速查(形态无关的硬性项)
| 文件 | 作用 |
| ---------------- | ------------------------------------------------------ |
| `api/home.ts` | 首页接口:`getHomeBanner`、`getRecommend`、`getNotice` |
| `api/news.ts` | 新闻接口:`getNewsList`、`getNewsDetail` |
| `api/product.ts` | 产品接口:`getProductList`、`getProductDetail` |
| `api/about.ts` | 关于我们/公司介绍接口 |
| `api/auth.ts` | 登录/注册/用户信息(仅会员体系需要时) |
> **纯接口层**:函数只负责 `request<T>({ url, method, params/data })`,不做数据加工、不维护状态。新增页面 → 新建 `api/<page>.ts`。
### `components/` —— 公共组件(仅跨页面复用)
| 文件 | 作用 |
| -------------------------- | ------------------------------------------------------------ |
| `components/AppHeader.vue` | 全站头部:logo、导航菜单(路由跳转) |
| `components/AppFooter.vue` | 全站页脚:版权、友链 |
| `components/AppButton.vue` | 通用按钮(disabled/loading/类型统一) |
| `components/AppSwiper.vue` | 通用轮播(首页 banner、产品图) |
| `components/index.ts` | 统一导出(barrel),页面与页面组件统一从 `@/components` 导入 |
> 判定:被 ≥2 个页面使用 → 放这里;仅 1 个页面使用 → 放 `views/<page>/cpns/`。
### `composables/` —— 通用组合式逻辑(与 UI 解耦)
| 文件 | 作用 |
| -------------------------------- | ------------------------ |
| `composables/useScroll.ts` | 滚动监听/吸顶/回到顶部 |
| `composables/useLoading.ts` | 请求 loading 状态管理 |
| `composables/useCountDown.ts` | 倒计时(验证码等) |
| `composables/useIntersection.ts` | 元素可见性(配合懒加载) |
### `directives/` —— 自定义指令
| 文件 | 作用 |
| ------------------------ | --------------------------------------------- |
| `directives/lazy.ts` | `v-lazy` 图片懒加载(网页端常用) |
| `directives/debounce.ts` | `v-debounce` 防抖 |
| `directives/throttle.ts` | `v-throttle` 节流 |
| `directives/index.ts` | `setupDirectives(app)` 统一注册,main.ts 调用 |
### `layout/` —— 全站布局骨架
| 文件 | 作用 |
| -------------------- | -------------------------------------------------------------------------- |
| `layout/index.vue` | 全站公共布局:`<AppHeader/>` + `<router-view/>` + `<AppFooter/>`,仅此一层 |
| `layout/components/` | 布局内部组件(如返回顶部按钮);需要全站复用的上移 `components/` |
### `router/` —— 路由系统(网页端:静态路由为主)
| 文件 | 作用 |
| ------------------ | ------------------------------------------------------------------------- |
| `router/index.ts` | 创建 `createRouter`(createWebHistory)+ 注册静态路由 + 挂守卫 |
| `router/routes.ts` | 静态路由表:`/home`、`/news`、`/news/:id`、`/product`、`/about`、404 兜底 |
| `router/guard.ts` | 路由守卫(可选):登录态校验(会员页)、滚动位置恢复、document.title |
```ts
// 路由加载页面(指向 views/home/index.ts —— 页面出口)
{
path: '/home',
name: 'Home',
component: () => import('@/views/home'), // 解析到 index.ts 的 default(index.vue)
meta: { title: '首页' },
}
```
### `stores/modules/` —— 数据层(Pinia,一页一 store,与页面同名)
| 文件 | 作用 |
| --------------------------- | ------------------------------------------------------------------------------------------- |
| `stores/modules/home.ts` | `useHomeStore`:state(banner/推荐/公告)+ actions(`fetchHomeData`,内部调 `api/home.ts`) |
| `stores/modules/news.ts` | `useNewsStore`:列表/分页/详情 state + `fetchNewsList`、`fetchNewsDetail` |
| `stores/modules/product.ts` | `useProductStore`:产品列表/详情/分类筛选 |
| `stores/modules/site.ts` | `useSiteStore`:站点配置、全局 loading、主题(非页面级公共数据) |
| `stores/modules/user.ts` | `useUserStore`:登录态/token(仅会员体系需要时) |
```ts
// stores/modules/home.ts —— 数据层写法示例(页面 → store → api)
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getHomeBanner, getRecommend } from '@/api/home'
import type { Banner, RecommendItem } from '@/types/business/home'
export const useHomeStore = defineStore('home', () => {
const banners = ref<Banner[]>([])
const recommends = ref<RecommendItem[]>([])
async function fetchHomeData() {
// ① store 调 api 层拿数据
const [bannerData, recommendData] = await Promise.all([
getHomeBanner(),
getRecommend(),
])
// ② 写入 state——完成页面所需数据
banners.value = bannerData
recommends.value = recommendData
}
return { banners, recommends, fetchHomeData }
})
```
### `styles/` —— 全局样式(less)
| 文件 | 作用 |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `styles/index.less` | 入口,main.ts `import '@/styles/index.less'` |
| `styles/variables.less` | ★ 变量唯一来源:`@primary-color`、`@text-color`、`@font-size-base`、`@spacing-base`… |
| `styles/mixins.less` | 混合宏(flex、文本省略) |
| `styles/reset.less` | 重置 |
| `styles/common.less` | 通用工具类 |
组件内样式:`<style lang="less" scoped>`,变量经 vite `css.preprocessorOptions.less.additionalData` 自动注入 `@import '@/styles/variables.less';`,组件内直接使用 `@primary-color`。
### `types/` —— 类型定义
| 文件 | 作用 |
| -------------------------- | --------------------------------------------------------------------- |
| `types/api.d.ts` | `ApiResponse<T> { code, msg, data }`、`PageResult<T> { list, total }` |
| `types/router.d.ts` | `declare module 'vue-router'` 扩展 `RouteMeta`(title 等) |
| `types/business/home.d.ts` | 首页实体类型(Banner/RecommendItem),与 `api/home.ts` 对应 |
| `types/business/news.d.ts` | 新闻实体(NewsItem/NewsQuery),与 `api/news.ts` 对应 |
> 新页面 → 新建 `types/business/<page>.d.ts`;禁止在组件内散落实体类型。
### `utils/` —— 纯函数工具
| 文件 | 作用 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `utils/request.ts` | ★ 唯一 axios 实例:拦截器注入 token/统一解包 `ApiResponse`/统一错误提示,导出 `request<T>`。组件与页面禁止再 import axios |
| `utils/storage.ts` | localStorage/sessionStorage 泛型封装 |
| `utils/validate.ts` | 校验规则(手机号/邮箱/URL) |
| `utils/format.ts` | 格式化(金额/日期/千分位) |
### `views/` —— 页面(一页一目录,三件套:index.vue + index.ts + cpns/)
| 文件/目录 | 作用 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `views/home/index.vue` | 首页主组件:组合 `cpns/` 的独有组件,数据由 `useHomeStore` 提供 |
| `views/home/index.ts` | **页面出口(barrel)**:`export { default } from './index.vue'` + 导出 `cpns/` 组件;路由与外部引用统一走它"展示"页面 |
| `views/home/cpns/Head.vue` | 首页独有头部区块(banner 下导航条等),仅本页使用 |
| `views/home/cpns/Banner.vue` | 首页独有轮播区块(渲染 store 的 banners) |
| `views/news/index.vue` | 新闻列表页 |
| `views/news/detail.vue` | 新闻详情页(与 index.vue 同目录,同页面模块) |
| `views/news/index.ts` | 出口 barrel(同上) |
| `views/news/cpns/` | 新闻页独有组件(筛选栏、列表项卡片) |
```ts
// views/home/index.ts —— 页面出口(barrel)写法
export { default } from './index.vue' // 页面主组件(default)
export { default as HomeHead } from './cpns/Head.vue'
export { default as HomeBanner } from './cpns/Banner.vue'
```
> 页面独有组件的归属判定:**只属于某一个页面 → 进该页面 `cpns/`;被 ≥2 个页面共用 → 上移 `components/`**。
## 项目功能模块图(功能 → 涉及文件)
| 功能 | 涉及文件 | 说明 |
| --------------------- | --------------------------------------------------------------------------------------- | ------------------------------ |
| 全站布局(头部/页脚) | `layout/index.vue` + `components/AppHeader.vue` + `components/AppFooter.vue` | 路由首层挂在 layout 下 |
| 首页 | `api/home.ts` + `stores/modules/home.ts` + `views/home/`(index.vue + index.ts + cpns) | 页面 → store → api 取数 |
| 新闻中心 | `api/news.ts` + `stores/modules/news.ts` + `views/news/` | 列表 + 详情 |
| 产品展示 | `api/product.ts` + `stores/modules/product.ts` + `views/product/` | 列表 + 详情 + 分类 |
| 关于我们 | `api/about.ts` + `stores/modules/about.ts` + `views/about/` | 公司介绍内容 |
| 会员登录(可选) | `api/auth.ts` + `stores/modules/user.ts` + `views/login/` + `router/guard.ts` | 登录态/token |
| 轮播/动效 | `components/AppSwiper.vue` + `composables/useScroll.ts` + `directives/lazy.ts` | 首页 banner、图片懒加载 |
| 站点公共数据 | `stores/modules/site.ts` + `styles/variables.less` | 站点配置、全局 loading、主题色 |
## 统一规范速查
### 路由规范
- 路由 `component` 指向页面出口:`() => import('@/views/<page>')`(解析到 `index.ts` 的 default = `index.vue`)
- 每个路由必须声明 `meta.title`;`RouteMeta` 扩展在 `src/types/router.d.ts`
- 网页端以静态路由为主,无需动态权限路由;嵌套页面(news 列表/详情)在同一页面目录下
- 公共页面挂载在 `layout/index.vue` 下,页面自身不重复写 header/footer
### 状态管理(Pinia)规范 —— 数据层
- store 文件:`src/stores/modules/<page>.ts`,命名 `use<Page>Store`(PascalCase),与 api/views 同名
- **统一组合式(setup)写法**:state 用 `ref`,getters 用 `computed`,actions 用普通函数,最后 return 全部
- **store 的 action 负责调用对应页面 api 层的接口取数**,页面只调 action
- 组件内:state/getters 用 `storeToRefs` 解构;actions 直接解构调用
- 简单展示型数据也走 store(保持数据层统一),页面禁止直接 import api
### 接口 API 规范 —— 接口层
- **全项目唯一 axios 实例**在 `src/utils/request.ts`,禁止页面/组件 import axios
- 统一响应结构 `ApiResponse<T> { code, msg, data }`(见 `types/api.d.ts`)
- 接口函数名 = 动词 + 资源:`getHomeBanner` / `getNewsList` / `getNewsDetail` / `getProductList`
- `api/<page>.ts` 只做请求分发,不做数据处理;处理与状态归 store
- baseURL 用环境变量 `import.meta.env.VITE_API_BASE_URL` 区分环境(.env / .env.development / .env.production)
- 错误统一由拦截器兜底,store/页面仅在需特殊处理时 catch
### 组件规范 ### 组件规范
- 公共组件 `components/`:文件 PascalCase(AppHeader.vue);被 ≥2 页面使用才放这里 - 统一 `<script setup lang="ts">`。
- 页面独有组件 `views/<page>/cpns/`:**严禁被其他页面 import**;语义化命名(Head.vue、Banner.vue) - props:`defineProps<Props>()` + `withDefaults`;emits:`defineEmits<{ change: [val: number] }>()`,事件名 camelCase。
- 页面结构三件套:`index.vue`(页面) + `index.ts`(出口 barrel) + `cpns/`(独有组件) - 模板内不写复杂表达式(超过一次三元/逻辑运算即抽方法或 computed)。
- 组件统一 `<script setup lang="ts">`;props 用 `defineProps<Props>()` + `withDefaults`;emits 用 `defineEmits<{ 事件名: [参数类型] }>()` - 单文件 > 400 行 → 拆到同页面 `cpns/`。
- 组件事件 camelCase 命名;模板内不放复杂表达式;样式默认 `<style lang="less" scoped>` - uni-app 组件模板用 `view/text/image`。
- 单文件超 400 行即拆分到 `cpns/`
### 样式规范(less) ### 样式规范
- 全项目使用 **less**,禁止新增 `.scss/.css` 业务样式 - **跟随项目既有方案**(less 或 scss),不混用两套预处理器。
- 全局变量统一在 `styles/variables.less`;组件内禁止魔法色值 - 变量单一来源:Vue 项目 `src/styles/variables.*`;uni-app 项目 `uni.scss`。
- vite `css.preprocessorOptions.less` 配置 `additionalData` 自动注入 variables - 组件内禁止魔法色值/魔法间距。
- 依赖:`pnpm add -D less`(vite 内置 less 编译支持) - Vue 项目:`<style scoped lang="less|scss">`;uni-app 页面样式**不加 `scoped`**(小程序不支持),靠类名隔离。
### 工程化硬性约定 ### 类型规范
- **导入一律 `@/` 别名**;禁止跨目录相对路径(`../` 超过一层即违规) - 实体类型 → `types/business/<domain>.d.ts`;接口通用结构 → `types/api.d.ts`。
- utils 与 composables 判据:**需要响应式状态或生命周期 → composables;否则 → utils** - **禁止** `as any` / `@ts-ignore` / `@ts-expect-error`。
- 类型归位:实体类型 → `types/business/<page>.d.ts`(与 api 同名);接口结构 → `types/api.d.ts`
- **禁止 `as any` / `@ts-ignore` / `@ts-expect-error`**
## 后续使用方式(工作流) ### 导入规范
### 场景速查:想做什么 → 去找哪个文件 - 有别名(`@/`)的项目一律用别名;禁止多级跨层 `../../../`。
- uni-app 需在 `vite.config` 的 `resolve.alias` 或 `tsconfig` 的 `paths` 配置 `@/`。
| 想做的事 | 动哪个文件 | 注意 | ### 请求规范
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| 给首页加一块新数据 | `api/home.ts` 加接口函数 → `stores/modules/home.ts` 加 action/state → `views/home/index.vue` 调 store action,结构放 `views/home/cpns/` 新组件 | 遵循数据流:页面 → store → api |
| 新增一个页面模块(如 news) | `types/business/news.d.ts` → `api/news.ts` → `stores/modules/news.ts` → `views/news/`(index.vue+index.ts+cpns) → `router/routes.ts` 注册 | 四者同名,走七步流程 |
| 页面要取数据 | 调对应 `use<Page>Store` 的 action(`storeToRefs` 取 state) | **禁止直接 import api** |
| 一个组件多个页面要用 | 放 `components/`(公共组件) | 从 `@/components` 导入 |
| 一个组件只有本页面用 | 放 `views/<page>/cpns/` | 禁止其他页面引用 |
| 改全站颜色/字体/间距 | `styles/variables.less` | 禁止组件内魔法值 |
| 图片懒加载 | 模板 `v-lazy`(`directives/lazy.ts`) | 勿手写滚动判断 |
| 通用逻辑(响应式) | `composables/` | 纯函数则 `utils/` |
| 弹窗/表单等小块 | 页面内 `cpns/` | 复用时上移 components/ |
| 路由新增页面 | `router/routes.ts` 注册 | component 指向 `@/views/<page>` |
### 新增页面模块七步流程(以"新闻 news"为例) - 全项目**唯一**请求实例/封装:`utils/request.ts`;页面与组件禁止直接 `axios` / `uni.request` / `fetch`。
- baseURL 走环境变量(`import.meta.env.VITE_*`)。
- 错误统一由拦截器兜底,业务侧仅在需要特殊处理时 `catch`。
1. **类型**:`types/business/news.d.ts` 定义 `NewsItem / NewsQuery / PageResult` ---
2. **接口层**:`api/news.ts`(`getNewsList`、`getNewsDetail`,走 `request<T>`)
3. **数据层**:`stores/modules/news.ts`(`useNewsStore`,action 内调用 api 函数取数)
4. **页面**:`views/news/index.vue`(列表)+ `views/news/detail.vue`(详情)+ `views/news/index.ts`(出口 barrel)+ `views/news/cpns/`(独有组件)
5. **路由**:`router/routes.ts` 注册(`component: () => import('@/views/news')`,配 `meta.title`)
6. **复用检查**:被 ≥2 页面使用的组件上移 `components/`
7. **验证**:`pnpm build`(含 vue-tsc 类型检查)零错误
### 日常开发姿势 ## 工作流
- **每次动手前**:按"第一步"读取目录,确认目标文件存在与现状,再决定新建还是修改。 ### 场景速查:想做什么 → 改哪些文件
- **新建文件前**:对照"src/ 每个文件的作用"确认归属(api/stores/views 四者同名)。
- **改数据相关代码**:先确认数据流方向——页面只碰 store,store 只碰 api,api 只发请求。 | 想做的事 | 动作 |
- **组件归属**:先自问"有几个页面用它"——1 个 → cpns/;≥2 个 → components/。 | ---------------------- | ------------------------------------------------------------------------------------------ |
- **规格冲突时**:本 skill 为唯一现行规范;未覆盖场景 → 询问用户或按最贴近的既有模式执行后复述假设。 | 给已有页面加一块新数据 | api 加函数 → store 加 action/state → 页面调 action,UI 放该页 `cpns/` 新组件 |
| 新增业务模块 | 类型 → api → store → 页面目录(index.vue + cpns/) → 路由(Vue)或 `pages.json`(uni-app) |
| 页面要取数据 | 调 `use<Domain>Store`;**禁止**直接 import api |
| 组件多页要用 | 上移 `components/` |
| 组件仅本页用 | 留在该页面 `cpns/` |
| 改全局颜色/间距 | 样式变量文件(`variables.*` / `uni.scss`) |
| 小程序主包体积超标 | 把非首屏页面迁入 `subPackages` |
### 新增业务模块流程(以 `news` 为例)
1. **类型** `types/business/news.d.ts`
2. **接口** `api/news.ts`
3. **状态** `stores/modules/news.ts`(action 内调 api)
4. **页面** `views/news/index.vue`(或 `pages/news/index.vue`)+ `cpns/`
5. **注册** `router/routes.ts`(`component: () => import('@/views/news/index.vue')`)或 `pages.json`
6. **复用检查** ≥2 处复用的上移 `components/`
7. **验证** 类型检查 + 构建零错误(`pnpm build` / `npm run build` / `vue-tsc --noEmit`)
### 最小改动纪律
- 每次动手前先读现状,确认是"新建"还是"修改"。
- 只碰与需求相关的行;不顺手重构、不顺手换技术栈。
- 规范冲突时:项目内显式文档 > 本 skill;本 skill 未覆盖时,先说明假设再执行。
---
## 修改代码时的强制检查清单 ## 修改代码时的强制检查清单
修改任何已存在文件时,逐条核对(不满足即视为违反规范,必须先修正再继续): 修改任何已存在文件时逐条核对,不满足必须先修正:
- [ ] 文件是否位于其职责对应的目录(接口在 api/、数据在 stores/modules/、页面在 views/<page>/ 等) - [ ] 文件位于其职责对应目录(接口在 api/、状态在 stores/、页面在 views|pages/)
- [ ] **页面是否直接 import 了 api 层?(违规——数据必须经 store)** - [ ] **页面是否直接 import 了 api?**(违规)
- [ ] **store 名 / api 文件名 / 页面目录名 / 类型文件名 是否与页面一致(可分层互查)** - [ ] api / store / 页面 / 类型 是否同业务域命名一致
- [ ] 导入是否使用 `@/` 别名 - [ ] 页面目录是否只有 `index.vue`(+ 同模块次级页面)+ `cpns/`,**没有多余 `index.ts`**
- [ ] 组件归属是否正确:≥2 页面共用 → components/;单页面独有 → views/<page>/cpns/ - [ ] 私有组件是否在 `cpns/` 且无其他页面 import
- [ ] 不在组件里直接 new axios 实例 / 直接调用原生 fetch - [ ] 导入使用 `@/` 别名,无多级跨层相对路径
- [ ] 样式是否使用 **less**(`<style lang="less" scoped>` / \*.less),颜色是否取自 variables.less - [ ] 未在组件内直接 `axios` / `uni.request` / `fetch`
- [ ] 类型是否齐全(无 `as any`、无 `@ts-ignore`) - [ ] 样式沿用项目既有预处理器,色值取自变量文件
- [ ] 新页面是否有完整三件套(index.vue + index.ts + cpns/)并在 router 注册 - [ ] uni-app:新页面已在 `pages.json` 注册;未把非首屏页放进主包;页面样式未加 `scoped`
- [ ] 运行时 `pnpm build`(含 vue-tsc 类型检查)零错误 - [ ] 无 `as any` / `@ts-ignore`
- [ ] 构建/类型检查零错误
> 本规范为项目强约束:**新增与修改的代码必须严格遵循**。发现不符合结构约定的代码,应指出问题并要求按规范调整,而不是放任通过。 > 发现不符合规范的历史代码:**指出问题并给出改法**,除非本次需求正好涉及,否则不擅自批量重构。

1
.gitignore vendored
View File

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

View File

@@ -344,60 +344,61 @@ export function submitFeedback(feedback: Feedback): Promise<void> {
return Promise.resolve() return Promise.resolve()
} }
/** TODO(api): 后端帮助中心接口就绪后改为 request 调用;暂用设计稿假数据 */
export function getHelpFaqs(): Promise<HelpFaqGroup[]> { export function getHelpFaqs(): Promise<HelpFaqGroup[]> {
return Promise.resolve([ return Promise.resolve([
{ {
title: '1. 账户与登录', title: '账户与登录',
items: [ items: [
{ {
q: '如何注册账号?', q: '如何注册账号?',
a: '点击页面右上角"注册",输入手机号并获取验证码,设置 6-20 位登录密码即可完成注册。', a: '打开APP后点击“注册”,输入手机号获取验证码,设置登录密码即可完成注册。',
}, },
{ {
q: '忘记密码怎么办?', q: '忘记密码怎么办?',
a: '在登录页点击"忘记密码",通过绑定手机号验证码验证身份后即可重新设置密码。', a: '在登录页面点击“忘记密码”,输入注册手机号获取验证码,验证通过后即可重置新密码。',
}, },
{ {
q: '一个账号可以多地登录吗?', q: '一个账号可以多端登录吗?',
a: '同一账号同时仅允许在 1 台设备上保持在线,超出将自动踢出最早登录的设备。', a: '同一账号仅允许在 1 台设备上保持在线,超出将自动踢出最早登录的设备。',
}, },
{ {
q: '如何修改绑定手机号?', q: '如何修改绑定手机号?',
a: '进入 个人中心 → 账号安全 → 绑定手机,验证原手机号后即可绑定新号码。', a: '进入「我的 -> 设置 -> 账户安全 -> 更换手机号」,验证原手机号后即可绑定新号码。',
}, },
], ],
}, },
{ {
title: '2. 功能使用指南', title: '功能使用指南',
items: [ items: [
{ {
q: '如何提交需求?', q: '如何提交报修/工单?',
a: '进入"发布需求"页面,填写需求描述、预算、周期等信息并提交,专业顾问将在一个工作日内与您联系。', a: '首页点击「报修」 -> 选择报修类型 -> 填写问题描述并上传图片 -> 提交。提交后可在「我的 -> 我的工单」中查看处理进度。',
}, },
{ {
q: '如何查看我的消息?', q: '如何预约会议室?',
a: '登录后点击右上角消息图标,或进入 个人中心 → 消息通知,即可查看系统通知与顾问消息。', a: '进入「服务 -> 会议室预订」,选择日期、时间段及会议室,确认后提交预约,预约成功将收到消息通知。',
}, },
{ {
q: '如何申请售后?', q: '如何查看我的消息通知?',
a: '在"我的订单"中找到对应项目,点击"申请售后"填写问题描述并上传附件,平台将尽快为您处理。', a: '首页右上角铃铛图标进入消息中心,支持按“全部 / 未读 / 系统通知 / 业务提醒”分类查看。',
}, },
], ],
}, },
{ {
title: '3. 系统与权限', title: '系统与权限',
items: [ items: [
{ {
q: '无权限怎么办?', q: 'APP提示“无权限”怎么办?',
a: '请确认当前账号角色是否具备该模块权限;如仍无法访问,请联系专属客服或平台管理员为您开通。', a: '部分功能需要管理员授权后方可使用,请联系贵司系统管理员为您分配对应角色权限。',
}, },
{ {
q: '收不到通知怎么办?', q: '为什么收不到推送通知?',
a: '请检查浏览器通知权限与消息设置是否开启;若仍未收到,请联系客服核实账号通知配置。', a: '请检查:①手机系统设置中是否允许本APP发送通知;②APP内「我的 -> 设置 -> 消息推送」是否已开启。',
}, },
{ {
q: '支持哪些浏览器?', q: 'APP支持哪些系统版本?',
a: '推荐使用 Chrome、Edge、Firefox、Safari 等主流浏览器的最新版本访问本平台。', a: 'iOS 13.0 及以上版本,Android 8.0 及以上版本。建议使用最新系统版本以获得最佳体验。',
}, },
], ],
}, },

BIN
src/assets/home/chahua.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -85,90 +85,71 @@ 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;
padding: @spacing-xl 0 @spacing-base;
&__inner { &__inner {
.flex(row, flex-start, flex-start); .flex(row, space-between, flex-start);
flex-wrap: wrap;
gap: @spacing-xl;
width: 100%; width: 100%;
max-width: 1920px; max-width: @page-content;
margin: 0 auto; margin: 0 auto;
height: 250px;
box-sizing: border-box; box-sizing: border-box;
padding-top: 54px; padding: 0 @page-gutter;
padding-left: clamp(
@spacing-base,
calc((100% - 1609px) / 2),
155px
);
overflow: hidden;
} }
&__about { &__about {
flex: 0 1 625px; // x155 → 780 flex: 0 1 360px;
max-width: 420px;
min-width: 0; min-width: 0;
} }
&__brand { &__brand {
.flex(row, flex-start); .flex(row, flex-start);
margin-bottom: 20px; margin-bottom: @spacing-sm;
margin-left: 45px;
} }
&__name { &__name {
font-size: 28px; font-size: @font-size-lg;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 1.4;
color: @white; color: @white;
} }
&__desc { &__desc {
margin-top: 0; margin: 0;
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 23px; line-height: 1.6;
color: fade(@white, 72%);
} }
&__col { &__col {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 0; gap: 2px;
flex: 0 0 auto;
min-width: 0; min-width: 0;
&--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: 20px; margin: 0 0 @spacing-sm;
font-size: 28px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 1.4;
white-space: nowrap; white-space: nowrap;
color: @white; color: @white;
} }
&__link { &__link {
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 23px; line-height: 1.7;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
color: fade(@white, 72%);
&:hover { &:hover {
color: @primary-light; color: @primary-light;
@@ -176,32 +157,31 @@ const coreBusiness = [
} }
&__contact { &__contact {
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 23px; line-height: 1.7;
color: fade(@white, 72%);
} }
&__bottom { &__bottom {
width: 867px; width: 100%;
max-width: calc(100% - 32px); max-width: @page-content;
margin: 0 auto; margin: @spacing-lg auto 0;
padding-bottom: 20px; padding: @spacing-base @page-gutter 0;
text-align: left; border-top: 1px solid fade(@white, 10%);
// legal[0] 需落在相对 footer 顶 332px 处 text-align: center;
margin-top: 62px; box-sizing: border-box;
} }
&__legal { &__legal {
margin: 0; margin: 0;
font-size: 14px; font-size: @font-size-xs;
font-weight: 500; font-weight: 400;
line-height: 23px; line-height: 1.6;
white-space: nowrap; color: fade(@white, 55%);
color: @white;
&--icp { &--icp {
margin-top: 16px; margin-top: @spacing-xs;
margin-left: 315px;
} }
} }
} }

View File

@@ -106,16 +106,16 @@ onMounted(() => void messageStore.fetchUnreadCount())
&__nav { &__nav {
.flex(row, center); .flex(row, center);
gap: @header-nav-gap; gap: clamp(@spacing-lg, 4vw, 56px);
height: 100%; height: 100%;
&-item { &-item {
position: relative; position: relative;
.flex(row, center); .flex(row, center);
height: 100%; height: 100%;
font-size: 18px; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
color: @black; color: @heading-color;
transition: color 0.2s; transition: color 0.2s;
&:hover, &:hover,
@@ -127,8 +127,8 @@ onMounted(() => void messageStore.fetchUnreadCount())
content: ''; content: '';
position: absolute; position: absolute;
left: 50%; left: 50%;
bottom: 11px; bottom: 12px;
width: 29px; width: 24px;
height: 2px; height: 2px;
border-radius: 1px; border-radius: 1px;
background: @home-tab-active; background: @home-tab-active;
@@ -139,20 +139,21 @@ onMounted(() => void messageStore.fetchUnreadCount())
&__actions { &__actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 29px; gap: @spacing-base;
margin-left: auto; margin-left: auto;
flex-shrink: 0; flex-shrink: 0;
} }
&__action { &__action {
.flex(row, center); .flex(row, center);
gap: 5px; gap: @spacing-xs;
width: 106px; min-width: 96px;
height: 36px; height: 36px;
border-radius: @radius-sm; padding: 0 @spacing-base;
border-radius: @radius-round;
background: @primary-color; background: @primary-color;
color: @white; color: @white;
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
transition: opacity 0.2s; transition: opacity 0.2s;
@@ -161,8 +162,8 @@ onMounted(() => void messageStore.fetchUnreadCount())
} }
&-icon { &-icon {
width: 18px; width: 16px;
height: 18px; height: 16px;
} }
} }
@@ -196,7 +197,7 @@ onMounted(() => void messageStore.fetchUnreadCount())
} }
&__login { &__login {
font-size: 16px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
} }
} }

View File

@@ -97,16 +97,16 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
&__tag { &__tag {
position: absolute; position: absolute;
top: 87px; top: 12px;
left: 22px; left: 12px;
height: 44px; height: 28px;
padding: 0 14px; padding: 0 @spacing-sm;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 28px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
line-height: 44px; line-height: 28px;
&--hardware { &--hardware {
background: @tag-hardware; background: @tag-hardware;

View File

@@ -292,15 +292,15 @@ function clearChips(): void {
<style lang="less" scoped> <style lang="less" scoped>
.case-filter { .case-filter {
flex-shrink: 0; flex-shrink: 0;
width: 516px; width: @cases-filter-width;
padding: 15px 0 40px; padding: @spacing-lg 0 @spacing-xl;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__head, &__head,
&__group, &__group,
&__actions { &__actions {
padding: 0 50px 0 35px; padding: 0 @spacing-lg;
} }
&__head { &__head {
@@ -308,15 +308,15 @@ function clearChips(): void {
} }
&__title { &__title {
font-size: 28px; font-size: @font-size-xl;
font-weight: 500; font-weight: 600;
line-height: 41px; line-height: 1.4;
color: @cases-filter-title; color: @cases-filter-title;
} }
&__link { &__link {
color: @primary-strong; color: @primary-strong;
font-size: 16px; font-size: @font-size-sm;
&:hover { &:hover {
color: @primary-hover; color: @primary-hover;
@@ -324,32 +324,36 @@ function clearChips(): void {
} }
&__group { &__group {
margin-top: 24px; margin-top: @spacing-lg;
} }
&__head + &__group { &__head + &__group {
margin-top: 11px; margin-top: @spacing-base;
} }
&__group-title { &__group-title {
font-size: 24px; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
line-height: 35px; line-height: 1.4;
color: @cases-text; color: @cases-text;
} }
&__rows { &__rows {
margin-top: 19px; margin-top: @spacing-sm;
display: flex;
flex-direction: column;
gap: 2px;
} }
&__row { &__row {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 12px; gap: @spacing-sm;
width: 100%; width: 100%;
height: 50px; height: 40px;
padding: 0; padding: 0 @spacing-sm;
border-radius: @radius-sm;
color: @cases-text; color: @cases-text;
font-size: 20px; font-size: @font-size-base;
text-align: left; text-align: left;
transition: background 0.2s; transition: background 0.2s;
@@ -367,8 +371,8 @@ function clearChips(): void {
&__box { &__box {
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 14px;
height: 16px; height: 14px;
border: 1px solid @cases-input-border; border: 1px solid @cases-input-border;
border-radius: 2px; border-radius: 2px;
@@ -380,9 +384,9 @@ function clearChips(): void {
content: ''; content: '';
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 4px; left: 3px;
width: 5px; width: 4px;
height: 8px; height: 7px;
border: solid @white; border: solid @white;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(45deg); transform: rotate(45deg);
@@ -397,7 +401,7 @@ function clearChips(): void {
&__row-count { &__row-count {
color: @cases-muted; color: @cases-muted;
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
} }
@@ -409,17 +413,17 @@ function clearChips(): void {
&__pills { &__pills {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px 33px; gap: @spacing-sm;
margin-top: 21px; margin-top: @spacing-sm;
} }
&__pill { &__pill {
height: 53px; height: 36px;
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: 20px; font-size: @font-size-sm;
.ellipsis(); .ellipsis();
transition: all 0.2s; transition: all 0.2s;
@@ -429,15 +433,14 @@ function clearChips(): void {
&.is-active, &.is-active,
&.is-light { &.is-light {
background: @pill-selected-bg; background: @filter-selected-bg;
color: @white; color: @primary-strong;
font-weight: 500; font-weight: 500;
} }
} }
// ElDatePicker 有两层元素:外层 div.case-filter__date 与内部 .el-range-editor, // ElDatePicker 有两层元素:外层 div.case-filter__date 与内部 .el-range-editor,
// 真正控制盒子尺寸的是内层。所以尺寸/背景/圆角都要用 :deep() 打到内层, // 真正控制盒子尺寸的是内层。所以尺寸/背景/圆角都要用 :deep() 打到内层。
// 且不能写在 &__date 上——那样只命中外层,视觉上看不到变化。
&__date-wrap { &__date-wrap {
grid-column: 1 / -1; grid-column: 1 / -1;
width: 100%; width: 100%;
@@ -448,7 +451,7 @@ function clearChips(): void {
align-items: center; align-items: center;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
height: 53px; height: 36px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @input-bg; background: @input-bg;
@@ -461,7 +464,7 @@ function clearChips(): void {
&__date { &__date {
justify-content: center; justify-content: center;
min-width: 0; min-width: 0;
height: 53px; height: 36px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @input-bg; background: @input-bg;
@@ -481,32 +484,33 @@ function clearChips(): void {
:deep(.el-range-input) { :deep(.el-range-input) {
flex: 0 1 auto; flex: 0 1 auto;
width: 92px; width: 88px;
background: transparent; background: transparent;
color: @cases-text; color: @cases-text;
font-size: 20px; font-size: @font-size-sm;
&::placeholder { &::placeholder {
color: @cases-text; color: @cases-muted;
} }
} }
:deep(.el-range-separator) { :deep(.el-range-separator) {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0 @spacing-xs; margin: 0 @spacing-xs;
color: @cases-text; color: @cases-muted;
font-size: 20px; font-size: @font-size-sm;
line-height: 53px; line-height: 36px;
} }
&.is-focus { &.is-focus {
background: @input-bg; background: @white;
box-shadow: 0 0 0 1px @cases-accent inset;
} }
} }
&__selected { &__selected {
margin: 23px 50px 0 35px; margin: @spacing-lg @spacing-lg 0;
padding: 16px 24px 21px; padding: @spacing-base;
border-radius: @radius-base; border-radius: @radius-base;
background: @chips-panel-bg; background: @chips-panel-bg;
} }
@@ -517,30 +521,30 @@ function clearChips(): void {
&__selected-title { &__selected-title {
color: @cases-text; color: @cases-text;
font-size: 20px; font-size: @font-size-base;
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: 14px; font-size: @font-size-sm;
} }
&__chips { &__chips {
.flex(row, flex-start, center); .flex(row, flex-start, center);
flex-wrap: wrap; flex-wrap: wrap;
gap: 14px 13px; gap: @spacing-sm;
margin-top: 16px; margin-top: @spacing-sm;
} }
&__chip { &__chip {
height: 32px; height: 28px;
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: 14px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
transition: color 0.2s; transition: color 0.2s;
@@ -551,32 +555,32 @@ function clearChips(): void {
&__actions { &__actions {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 13px; gap: @spacing-sm;
margin-top: 30px; margin-top: @spacing-lg;
} }
&__reset { &__reset {
width: 135px; flex: 0 0 88px;
height: 30px; height: 32px;
border-radius: @radius-round; border-radius: @radius-round;
background: @cases-reset-bg; background: @cases-reset-bg;
color: @black; color: @text-color;
font-size: 14px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
transition: filter 0.2s; transition: color 0.2s;
&:hover { &:hover {
filter: brightness(0.96); color: @primary-color;
} }
} }
&__confirm { &__confirm {
flex: 1; flex: 1;
height: 30px; height: 32px;
border-radius: @radius-round; border-radius: @radius-round;
background: @cases-confirm; background: @cases-confirm;
color: @white; color: @white;
font-size: 14px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -62,29 +62,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: 24px; gap: @spacing-base;
width: 100%; width: 100%;
height: 128px; min-height: 64px;
box-sizing: border-box; box-sizing: border-box;
// 设计稿 #31:输入框 x=653(栏左 606 + 47),重置 1305,搜索 1429 padding: 0 @spacing-lg;
padding: 0 47px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__field { &__field {
position: relative; position: relative;
// 输入框右边界 1245 → 重置按钮 1305,设计稿间距 60 flex: 0 1 360px;
margin-right: 36px; min-width: 0;
flex: 0 0 592px;
} }
&__icon { &__icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 16px; right: @spacing-base;
left: auto; left: auto;
width: 27px; width: 18px;
height: 27px; height: 18px;
transform: translateY(-50%); transform: translateY(-50%);
fill: none; fill: none;
stroke: @cases-icon; stroke: @cases-icon;
@@ -93,15 +91,15 @@ const keyword = computed<string>({
&__input { &__input {
width: 100%; width: 100%;
height: 49px; height: 36px;
padding: 0 52px 0 20px; padding: 0 40px 0 @spacing-base;
border: 1px solid @cases-input-border; border: 1px solid @cases-input-border;
border-radius: @radius-sm; border-radius: @radius-round;
background: @white; background: @input-bg;
color: @cases-text; color: @cases-text;
font-size: 20px; font-size: @font-size-base;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s, background-color 0.2s;
&::placeholder { &::placeholder {
color: @text-color-disabled; color: @text-color-disabled;
@@ -109,22 +107,24 @@ const keyword = computed<string>({
&:focus { &:focus {
border-color: @cases-accent; border-color: @cases-accent;
background: @white;
} }
} }
&__btn { &__btn {
min-width: 100px; flex-shrink: 0;
height: 49px; min-width: 72px;
padding: 0 @spacing-base; height: 36px;
border-radius: @radius-sm; padding: 0 @spacing-lg;
background: @cases-accent; border-radius: @radius-round;
color: @white; background: @pill-bg;
font-size: 20px; color: @text-color;
font-size: @font-size-base;
font-weight: 400; font-weight: 400;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
background: @primary-hover; color: @primary-color;
} }
&--primary { &--primary {
@@ -143,7 +143,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: 16px; font-size: @font-size-base;
transition: color 0.2s; transition: color 0.2s;
&:hover, &:hover,

View File

@@ -71,54 +71,55 @@ const emit = defineEmits<{
<style lang="less" scoped> <style lang="less" scoped>
.filter-panel { .filter-panel {
padding: 24px 36px; padding: @spacing-lg;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__bar { &__bar {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: 18px; gap: @spacing-base;
} }
&__input { &__input {
flex: 1; flex: 1;
max-width: 456px; max-width: 360px;
margin-right: 28px; margin-right: 0;
height: 38px; height: 36px;
padding: 0 @spacing-base; padding: 0 @spacing-base;
border: 1px solid @cases-input-border; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-round;
background: @white; background: @input-bg;
color: @cases-text; color: @text-color;
font-size: 15px; font-size: @font-size-base;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s, background-color 0.2s;
&::placeholder { &::placeholder {
color: @cases-input-border; color: @text-color-disabled;
} }
&:focus { &:focus {
border-color: @cases-accent; border-color: @primary-color;
background: @white;
} }
} }
&__btn { &__btn {
min-width: 77px; min-width: 72px;
height: 38px; height: 36px;
padding: 0 @spacing-base; padding: 0 @spacing-lg;
border-radius: @radius-sm; border-radius: @radius-round;
background: @cases-accent; background: @pill-bg;
color: @white; color: @text-color;
font-size: 15px; font-size: @font-size-base;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
background: @primary-hover; color: @primary-color;
} }
&--primary { &--primary {
background: @cases-accent; background: @primary-color;
color: @white; color: @white;
&:hover { &:hover {
@@ -131,26 +132,26 @@ const emit = defineEmits<{
&__chips { &__chips {
.flex(row, flex-start, center); .flex(row, flex-start, center);
flex-wrap: wrap; flex-wrap: wrap;
gap: 11px 10px; gap: @spacing-sm;
margin-top: @spacing-base; margin-top: @spacing-base;
padding: 12px 18px 16px; padding: @spacing-base;
border-radius: @radius-base; border-radius: @radius-base;
background: @chips-panel-bg; background: @chips-panel-bg;
font-size: 15px; font-size: @font-size-base;
} }
&__chips-label { &__chips-label {
color: @cases-text; color: @text-color;
font-weight: 500; font-weight: 500;
} }
&__chip { &__chip {
height: 25px; height: 28px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
color: @cases-accent; color: @primary-color;
font-size: 11px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;

View File

@@ -23,25 +23,25 @@ defineProps<Props>()
<style lang="less" scoped> <style lang="less" scoped>
.page-head { .page-head {
background: @white; background: @white;
padding-bottom: 72px; padding: @spacing-lg 0;
border-bottom: 1px solid @border-color;
&__title { &__title {
padding-top: 52px; padding-top: 0;
font-size: 22px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
line-height: 29px; line-height: 1.3;
color: @finance-ink; color: @heading-color;
} }
&__slogan { &__slogan {
// 设计稿 #20:slogan 水平居中(当前唯一使用方为财务中心页) text-align: left;
text-align: center;
padding-bottom: 0; padding-bottom: 0;
margin-top: 9px; margin-top: @spacing-sm;
font-size: 13px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.5;
color: @finance-muted-text; color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -67,18 +67,18 @@ function go(page: number): void {
.pagination { .pagination {
.flex(row, center); .flex(row, center);
gap: @spacing-xs; gap: @spacing-xs;
padding: 12px 0; padding: @spacing-base 0;
&__btn { &__btn {
.flex(row, center); .flex(row, center);
min-width: 20px; min-width: 28px;
height: 25px; height: 28px;
padding: 0 3px; padding: 0 6px;
border: 1px solid @border-color; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
color: @text-color-secondary; color: @text-color;
font-size: 11px; font-size: @font-size-sm;
transition: all 0.2s; transition: all 0.2s;
&:hover:not(:disabled):not(.is-ellipsis) { &:hover:not(:disabled):not(.is-ellipsis) {
@@ -87,9 +87,10 @@ function go(page: number): void {
} }
&.is-active { &.is-active {
border-color: @cases-page-active-bg; border-color: @primary-strong;
background: @cases-page-active-bg; background: @primary-light;
color: @primary-strong; color: @primary-strong;
font-weight: 500;
} }
&.is-ellipsis { &.is-ellipsis {

View File

@@ -32,42 +32,44 @@ function onGetCode(): void {
.sms-code { .sms-code {
.flex(row, stretch); .flex(row, stretch);
gap: @spacing-sm; gap: @spacing-sm;
width: 100%;
&__input { &__input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 40px; height: 44px;
padding: 0 12px; padding: 0 @spacing-base;
border: 1px solid #dde1e7; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @white; background: @bg-color-light;
font-size: 13px; font-size: @font-size-base;
color: #1f2937; color: @heading-color;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s, background 0.2s;
box-sizing: border-box;
&:focus { &:focus {
border-color: @primary-strong; border-color: @primary-strong;
background: @white;
} }
&::placeholder { &::placeholder {
color: #1f2937; color: @text-color-secondary;
} }
} }
// 设计稿 #38:「获取验证码」是带蓝色描边的独立按钮(62×16 文字居中),
// 不是纯文字链接。按键宽 88px,与输入框间距 @spacing-sm。
&__btn { &__btn {
flex-shrink: 0; flex-shrink: 0;
width: 88px; min-width: 120px;
height: 40px; height: 44px;
padding: 0; padding: 0 @spacing-base;
border: 1px solid @primary-strong; border: 1px solid @primary-strong;
border-radius: @radius-sm; border-radius: @radius-base;
background: @white; background: @white;
color: @primary-strong; color: @primary-strong;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 500;
white-space: nowrap;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -78,8 +80,9 @@ function onGetCode(): void {
} }
&:disabled { &:disabled {
border-color: @text-color-disabled; border-color: @border-color;
color: @text-color-disabled; background: @bg-color-light;
color: @text-color-secondary;
cursor: not-allowed; cursor: not-allowed;
} }
} }

View File

@@ -71,6 +71,7 @@ function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
desc: subtitle, desc: subtitle,
slogan: subtitle, slogan: subtitle,
icon: vo.logoUrl ?? '', icon: vo.logoUrl ?? '',
cover: vo.coverUrl ?? '',
serviceContent, serviceContent,
contents, contents,
serviceTypes, serviceTypes,
@@ -99,6 +100,7 @@ function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
desc: subtitle, desc: subtitle,
slogan: subtitle, slogan: subtitle,
icon: ps.logoUrl ?? '', icon: ps.logoUrl ?? '',
cover: ps.coverUrl ?? '',
serviceContent, serviceContent,
contents, contents,
serviceTypes, serviceTypes,

View File

@@ -85,9 +85,8 @@
.flex(column, center, center); .flex(column, center, center);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
// 设计稿三页的标题顶分别为 306/287/268,卡片在 1080 视口中居中后 // 底部多留白,整体视觉中心上移,「欢迎登录」不再偏下
// 会比标注高 15px,用上内边距把内容整体下移补平。 padding: 24px 0 120px;
padding: 80px 0 40px;
} }
// 设计稿表单区统一 693px 宽。标题/副标题按自身文字宽度相对表单居中, // 设计稿表单区统一 693px 宽。标题/副标题按自身文字宽度相对表单居中,

View File

@@ -77,7 +77,7 @@
@header-height: 72px; @header-height: 72px;
@header-nav-gap: 94px; @header-nav-gap: 94px;
@footer-height: 481px; @footer-height: 280px;
@footer-inset: 70px; @footer-inset: 70px;
// 登录/注册/找回密码三页共用的品牌区宽度。设计稿 #36/#38/#39 的左侧渐变 // 登录/注册/找回密码三页共用的品牌区宽度。设计稿 #36/#38/#39 的左侧渐变
@@ -90,10 +90,10 @@
@shadow-lg: 0 4px 16px rgba(31, 39, 51, 0.12); @shadow-lg: 0 4px 16px rgba(31, 39, 51, 0.12);
@filter-selected-bg: #e7eeff; @filter-selected-bg: #e7eeff;
@pill-bg: #ededed; @pill-bg: #f5f6f8;
@pill-selected-bg: #075cff; @pill-selected-bg: #075cff;
@chips-panel-bg: #eaf1ff; @chips-panel-bg: #eaf1ff;
@input-bg: #ededed; @input-bg: #f5f6f8;
@tag-hardware: #4caf50; @tag-hardware: #4caf50;
@tag-iot: #5720ba; @tag-iot: #5720ba;
@tag-manufacturing: #ff8f1f; @tag-manufacturing: #ff8f1f;
@@ -105,19 +105,23 @@
@cases-filter-title: #222222; @cases-filter-title: #222222;
@cases-text: #333333; @cases-text: #333333;
@cases-muted: #666666; @cases-muted: #666666;
@cases-input-border: #999999; @cases-input-border: #cdd3dd;
@cases-icon: #797979; @cases-icon: #797979;
@cases-reset-bg: #f0f0f0; @cases-reset-bg: #f5f6f8;
@cases-card-border: #e1dfdf; @cases-card-border: #e4e7ec;
@cases-card-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); @cases-card-shadow: 0 2px 8px rgba(31, 39, 51, 0.06);
@cases-page-active-bg: #e0e9fa; @cases-page-active-bg: #e0e9fa;
@cases-filter-width: 320px;
@demand-selected-bg: #024fff; @demand-selected-bg: #024fff;
@demand-accent: #024fff; @demand-accent: #024fff;
@demand-secondary-bg: #075cff; @demand-secondary-bg: #075cff;
@project-table-head-bg: #e8e8e8; @project-table-head-bg: #f7f8fa;
@project-table-line: #bbbbbb; @project-table-line: #e4e7ec;
@project-filter-width: 320px;
@project-pill-bg: #f5f6f8;
@project-input-bg: #f5f6f8;
@project-accent: #024fff; @project-accent: #024fff;
@project-track: #edeff2; @project-track: #edeff2;
@@ -181,8 +185,8 @@
@demand-ink: #121212; @demand-ink: #121212;
@demand-heading: #333333; @demand-heading: #333333;
@demand-subtitle: #666666; @demand-subtitle: #666666;
@demand-line: #999999; @demand-line: #e4e7ec;
@demand-upload-bg: #ededed; @demand-upload-bg: #f5f6f8;
@demand-radio-idle: #c7c2c2; @demand-radio-idle: #c7c2c2;
@auth-gradient-from: #505be8; @auth-gradient-from: #505be8;
@auth-gradient-to: #8b3fe8; @auth-gradient-to: #8b3fe8;
@@ -208,7 +212,7 @@
@project-cell: #4b5563; @project-cell: #4b5563;
@project-card: #ffffff; @project-card: #ffffff;
@project-field-bg: #f3f4f6; @project-field-bg: #f3f4f6;
@project-input-border: #dde1e7; @project-input-border: #e4e7ec;
@project-head-bg: #f7f8fa; @project-head-bg: #f7f8fa;
@project-upload-tag: #ef4444; @project-upload-tag: #ef4444;
@project-banner-soft: #fdeeee; @project-banner-soft: #fdeeee;
@@ -223,12 +227,12 @@
@contract-accent: #075cff; @contract-accent: #075cff;
@contract-muted: #666666; @contract-muted: #666666;
@contract-head-text: #222222; @contract-head-text: #666666;
@contract-cell-text: #333333; @contract-cell-text: #333333;
@contract-line: #999999; @contract-line: #e4e7ec;
@contract-table-head-bg: #ededed; @contract-table-head-bg: #f7f8fa;
@contract-placeholder: #b3b9c4; @contract-placeholder: #b3b9c4;
@contract-reset-bg: #eef0f3; @contract-reset-bg: #f5f6f8;
@contract-qr-bg: #d6d6d6; @contract-qr-bg: #d6d6d6;
@finance-hero-gradient: linear-gradient( @finance-hero-gradient: linear-gradient(
@@ -239,8 +243,8 @@
); );
@finance-chart-bar: #4a90c4; @finance-chart-bar: #4a90c4;
@finance-chart-grid: #edeef2; @finance-chart-grid: #edeef2;
@finance-table-head-bg: #f1f3f5; @finance-table-head-bg: #f7f8fa;
@finance-field-bg: #f3f4f6; @finance-field-bg: #f5f6f8;
@finance-page-active: #d5e2fe; @finance-page-active: #d5e2fe;
@finance-summary-cta: #ffb754; @finance-summary-cta: #ffb754;
@finance-result-circle: #7ed321; @finance-result-circle: #7ed321;
@@ -252,7 +256,7 @@
@profile-nav-active-bg: #e7eeff; @profile-nav-active-bg: #e7eeff;
@profile-card-border: #f0f1f3; @profile-card-border: #f0f1f3;
@profile-line: #e5e7eb; @profile-line: #e5e7eb;
@profile-input-border: #dde1e7; @profile-input-border: #e4e7ec;
@profile-radius: 6px; @profile-radius: 6px;
@profile-page-bg: #f4f5f9; @profile-page-bg: #f4f5f9;
@profile-ink: #1f2937; @profile-ink: #1f2937;

View File

@@ -35,6 +35,7 @@ export interface BusinessLine {
desc: string desc: string
slogan: string slogan: string
icon: string icon: string
cover: string
serviceContent: string serviceContent: string
contents: ServiceContentBlock[] contents: ServiceContentBlock[]
serviceTypes: ServiceType[] serviceTypes: ServiceType[]

View File

@@ -6,30 +6,79 @@ function goHome(): void {
<template> <template>
<section class="not-found"> <section class="not-found">
<h1>404</h1> <div class="not-found__card">
<p class="text-secondary"> <p class="not-found__code">
页面不存在或已下线 404
</p> </p>
<button class="not-found__back link" @click="goHome"> <h1 class="not-found__title">
返回首页 页面不存在
</button> </h1>
<p class="not-found__desc text-secondary">
页面不存在或已下线,请返回首页继续浏览
</p>
<button
class="not-found__back"
type="button"
@click="goHome"
>
返回首页
</button>
</div>
</section> </section>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.not-found { .not-found {
.flex(column, center); .flex(column, center, center);
gap: @spacing-base;
min-height: 100vh; min-height: 100vh;
padding: @spacing-lg;
background: @bg-color;
h1 { &__card {
font-size: 64px; .flex(column, center, center);
gap: @spacing-sm;
width: 100%;
max-width: 420px;
padding: @spacing-xxl @spacing-xl;
border-radius: @radius-base;
background: @white;
text-align: center;
}
&__code {
margin: 0;
font-size: @font-size-hero;
font-weight: 600;
line-height: 1;
color: @primary-color;
}
&__title {
margin: 0;
font-size: @font-size-xl;
font-weight: 600;
color: @heading-color; color: @heading-color;
} }
&__back { &__desc {
color: @primary-color; margin: 0 0 @spacing-base;
font-size: @font-size-base; font-size: @font-size-base;
} }
&__back {
height: 40px;
padding: 0 @spacing-xl;
border: none;
border-radius: @radius-round;
background: @primary-color;
color: @white;
font-size: @font-size-base;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: @primary-hover;
}
}
} }
</style> </style>

View File

@@ -18,7 +18,7 @@ const coverStyle = computed(() =>
<template> <template>
<section class="case-hero" :style="coverStyle"> <section class="case-hero" :style="coverStyle">
<div class="container case-hero__inner"> <div class="case-hero__inner">
<span v-if="tag" class="case-hero__tag">{{ tag }}</span> <span v-if="tag" class="case-hero__tag">{{ tag }}</span>
<h1 class="case-hero__title"> <h1 class="case-hero__title">
{{ props.item.title }} {{ props.item.title }}
@@ -31,17 +31,19 @@ const coverStyle = computed(() =>
.case-hero { .case-hero {
position: relative; position: relative;
width: 100%; width: 100%;
height: 260px; height: 180px;
border-radius: @radius-base;
background-color: @primary-color; background-color: @primary-color;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
overflow: hidden;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 42%, rgba(15, 23, 42, 0.55)); background: linear-gradient(180deg, fade(@black, 0%) 40%, fade(@black, 45%));
} }
&__inner { &__inner {
@@ -50,7 +52,8 @@ const coverStyle = computed(() =>
.flex(column, flex-end, flex-start); .flex(column, flex-end, flex-start);
gap: @spacing-sm; gap: @spacing-sm;
height: 100%; height: 100%;
padding-bottom: @spacing-lg; box-sizing: border-box;
padding: 0 @spacing-lg @spacing-lg;
} }
&__tag { &__tag {
@@ -64,9 +67,9 @@ const coverStyle = computed(() =>
&__title { &__title {
color: @white; color: @white;
font-size: @font-size-xxl; font-size: @font-size-title;
font-weight: 600; font-weight: 600;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35); text-shadow: 0 1px 8px fade(@black, 35%);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@@ -51,19 +51,21 @@ onMounted(load)
</div> </div>
<template v-if="item"> <template v-if="item">
<CaseHero :item="item" /> <div class="container case-detail__body">
<CaseHero :item="item" />
<div class="container case-detail__main"> <div class="case-detail__main">
<CaseDetailHeader :item="item" @favorite="onFavorite" @share="onShare" /> <CaseDetailHeader :item="item" @favorite="onFavorite" @share="onShare" />
<CaseRichText :item="item" /> <CaseRichText :item="item" />
<button <button
class="case-detail__cta" class="case-detail__cta"
type="button" type="button"
@click="router.push('/demand')" @click="router.push('/demand')"
> >
一键填报类似案例 一键填报类似案例
</button> </button>
</div>
</div> </div>
</template> </template>
@@ -75,27 +77,33 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.case-detail { .case-detail {
padding-bottom: @spacing-xxl; min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__crumb { &__crumb {
padding-top: 6px; padding-top: @spacing-base;
padding-bottom: 6px; padding-bottom: @spacing-base;
font-size: @font-size-base; font-size: @font-size-sm;
}
&__body {
.flex(column, flex-start, stretch);
gap: @spacing-base;
} }
&__main { &__main {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base; gap: @spacing-base;
padding-top: @spacing-lg;
} }
&__cta { &__cta {
width: 100%; width: 100%;
height: 44px; height: 40px;
border-radius: @radius-base; border-radius: @radius-round;
background: @primary-color; background: @primary-color;
color: @white; color: @white;
font-size: @font-size-lg; font-size: @font-size-base;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -109,53 +109,49 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.cases { .cases {
padding-bottom: 78px; min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__hero { &__hero {
height: 179px;
box-sizing: border-box; box-sizing: border-box;
padding-top: 32px; padding: @spacing-lg 0;
background: @white; background: @white;
border-bottom: 1px solid @border-color;
} }
// 设计稿 #31:hero 标题 x=112、副标题 x=112;
// 主体筛选栏 x=70、右侧操作区右边界 1850。两套留白并存,
// 所以 hero 内容单独内缩,不动全局 .container 的 70px 留白。
&__hero-inner { &__hero-inner {
padding-left: 112px; padding-left: @spacing-xl;
padding-right: 112px; padding-right: @spacing-xl;
} }
&__title { &__title {
margin: 0; margin: 0;
font-size: 36px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.3;
color: @cases-hero-title; color: @cases-hero-title;
} }
&__slogan { &__slogan {
margin: 15px 0 0; margin: @spacing-sm 0 0;
font-size: 24px; font-size: @font-size-base;
font-weight: 500; font-weight: 400;
line-height: 37px; line-height: 1.5;
color: @cases-muted; color: @cases-muted;
} }
&__body { &__body {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 20px; gap: @spacing-lg;
padding-top: 20px; padding-top: @spacing-lg;
} }
&__main { &__main {
// 设计稿:右侧操作区 x=606 w=1244(右边界 1850,与筛选栏区留白 70 对称)。
// mixin 签名是 .flex(direction; justify; align),第三参才是 align-items。
// 纵向排列时 justify 无意义,align 用 stretch 让子项撑满 1244。
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 24px; gap: @spacing-lg;
} }
&__pager { &__pager {

View File

@@ -25,167 +25,172 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
<template> <template>
<div class="contract-info"> <div class="contract-info">
<h3 class="contract-info__section contract-info__section--1"> <section class="contract-info__section">
<span class="contract-info__step">01</span> <h3 class="contract-info__heading">
<span class="contract-info__section-title">合同信息</span> <span class="contract-info__step">01</span>
</h3> <span>合同信息</span>
<div class="contract-info__field contract-info__field--1 contract-info__field--l"> </h3>
<span class="contract-info__label">项目名称</span> <div class="contract-info__grid">
<span class="contract-info__value">{{ contract.contractName }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">项目名称</span>
<div class="contract-info__field contract-info__field--1 contract-info__field--r"> <span class="contract-info__value">{{ contract.contractName }}</span>
<span class="contract-info__label">合同类型</span> </div>
<span class="contract-info__value">{{ contract.contractType }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">合同类型</span>
<div class="contract-info__field contract-info__field--2 contract-info__field--l"> <span class="contract-info__value">{{ contract.contractType }}</span>
<span class="contract-info__label">合同编号</span> </div>
<span class="contract-info__value">{{ contract.contractNo }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">合同编号</span>
<div class="contract-info__field contract-info__field--2 contract-info__field--r"> <span class="contract-info__value">{{ contract.contractNo }}</span>
<span class="contract-info__label">签订日期</span> </div>
<span class="contract-info__value">{{ contract.signDate || '—' }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">签订日期</span>
<div class="contract-info__field contract-info__field--3 contract-info__field--l"> <span class="contract-info__value">{{ contract.signDate || '—' }}</span>
<span class="contract-info__label">生效日期</span> </div>
<span class="contract-info__value">{{ contract.effectiveDate }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">生效日期</span>
<div class="contract-info__field contract-info__field--3 contract-info__field--r"> <span class="contract-info__value">{{ contract.effectiveDate }}</span>
<span class="contract-info__label">到期日期</span> </div>
<span class="contract-info__value">{{ contract.expiryDate }}</span> <div class="contract-info__field">
</div> <span class="contract-info__label">到期日期</span>
<div class="contract-info__field contract-info__field--4 contract-info__field--l"> <span class="contract-info__value">{{ contract.expiryDate }}</span>
<span class="contract-info__label">合同状态</span> </div>
<StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" /> <div class="contract-info__field">
</div> <span class="contract-info__label">合同状态</span>
<StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" />
</div>
</div>
</section>
<h3 class="contract-info__section contract-info__section--2"> <section class="contract-info__section">
<span class="contract-info__step">02</span> <h3 class="contract-info__heading">
<span class="contract-info__section-title">关联项目</span> <span class="contract-info__step">02</span>
</h3> <span>关联项目</span>
<div class="contract-info__field contract-info__field--5 contract-info__field--l"> </h3>
<span class="contract-info__label"> <div class="contract-info__grid">
项目名称 <div class="contract-info__field">
<button class="contract-info__link" @click="emit('viewProject')">查看详情</button> <span class="contract-info__label">
</span> 项目名称
<span class="contract-info__value">{{ contract.relatedProjectName }}</span> <button
</div> type="button"
<div class="contract-info__field contract-info__field--5 contract-info__field--r"> class="contract-info__link"
<span class="contract-info__label">项目编号</span> @click="emit('viewProject')"
<span class="contract-info__value">{{ contract.projectNo }}</span> >
</div> 查看详情
</button>
<h3 class="contract-info__section contract-info__section--3">
<span class="contract-info__step">03</span>
<span class="contract-info__section-title">合同文件</span>
</h3>
<ul class="contract-info__files">
<li
v-for="(file, index) in contract.files"
:key="`${file.name}-${index}`"
class="contract-info__file"
>
<div class="contract-info__file-info">
<span class="contract-info__file-name" :title="file.name">{{ file.name }}</span>
<span class="contract-info__file-meta">
{{ formatFileSize(file.size) }} · {{ file.uploadTime }}
</span> </span>
<span class="contract-info__value">{{ contract.relatedProjectName }}</span>
</div> </div>
<div class="contract-info__file-actions"> <div class="contract-info__field">
<button class="contract-info__link" @click="emit('preview', file)"> <span class="contract-info__label">项目编号</span>
预览 <span class="contract-info__value">{{ contract.projectNo }}</span>
</button>
<button class="contract-info__link" @click="emit('download', file)">
下载
</button>
</div> </div>
</li> </div>
<li v-if="contract.files.length === 0" class="contract-info__empty"> </section>
暂无合同文件
</li> <section class="contract-info__section">
</ul> <h3 class="contract-info__heading">
<span class="contract-info__step">03</span>
<span>合同文件</span>
</h3>
<ul class="contract-info__files">
<li
v-for="(file, index) in contract.files"
:key="`${file.name}-${index}`"
class="contract-info__file"
>
<div class="contract-info__file-info">
<span class="contract-info__file-name" :title="file.name">{{ file.name }}</span>
<span class="contract-info__file-meta">
{{ formatFileSize(file.size) }} · {{ file.uploadTime }}
</span>
</div>
<div class="contract-info__file-actions">
<button
type="button"
class="contract-info__link"
@click="emit('preview', file)"
>
预览
</button>
<button
type="button"
class="contract-info__link"
@click="emit('download', file)"
>
下载
</button>
</div>
</li>
<li v-if="contract.files.length === 0" class="contract-info__empty">
暂无合同文件
</li>
</ul>
</section>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.contract-info { .contract-info {
position: relative; width: 100%;
width: 1780px; margin-top: @spacing-base;
height: 1152px; padding: @spacing-xl;
margin-top: 21px; border-radius: @radius-base;
border-radius: @radius-lg;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
&__section { &__section + &__section {
position: absolute; margin-top: @spacing-xxl;
left: 54px;
top: 0;
height: 70px;
&--1 { top: 36px; }
&--2 { top: 649px; }
&--3 { top: 901px; }
} }
&__section-title { &__heading {
position: absolute; .flex(row, flex-start, center);
left: 60px; gap: @spacing-sm;
top: 23px; margin: 0 0 @spacing-lg;
margin: 0; font-size: @font-size-lg;
font-size: 28px;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 1.4;
color: @contract-cell-text; color: @contract-cell-text;
white-space: nowrap; // 父级 h3 内子元素全部脱流,标题会逐字换行,强制不换行
} }
&__step { &__step {
position: absolute; font-size: @font-size-title;
left: 0; font-weight: 600;
top: 50%; line-height: 1.2;
font-size: 48px;
font-weight: 500;
line-height: 70px;
color: @project-accent; color: @project-accent;
white-space: nowrap; }
transform: translateY(-50%);
&__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: @spacing-lg;
} }
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
position: absolute; gap: @spacing-sm;
width: 764px; min-width: 0;
gap: 14px;
} }
&__field--1 { top: 174px; }
&__field--2 { top: 296px; }
&__field--3 { top: 418px; }
&__field--4 { top: 540px; }
&__field--5 { top: 770px; }
&__field--5:has(&__label) { top: 749px; }
&__field--l { left: 54px; }
&__field--r { left: 930px; }
&__label { &__label {
font-size: 20px; .flex(row, flex-start, center);
gap: @spacing-sm;
font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 29px; line-height: 1.4;
color: @contract-cell-text; color: @contract-cell-text;
} }
&__value { &__value {
.flex(row, flex-start, center); .flex(row, flex-start, center);
width: 100%; width: 100%;
height: 49px; height: 40px;
padding: 0 18px; padding: 0 @spacing-base;
border: 1px solid @contract-line; border: 1px solid @border-color;
border-radius: 8px; border-radius: @radius-sm;
background: @contract-page-bg; background: @bg-color-light;
font-size: 20px; font-size: @font-size-base;
line-height: 29px; line-height: 1.4;
color: @contract-cell-text; color: @contract-cell-text;
box-sizing: border-box; box-sizing: border-box;
.ellipsis(); .ellipsis();
@@ -194,19 +199,19 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__field :deep(.status-tag) { &__field :deep(.status-tag) {
.flex(row, flex-start, center); .flex(row, flex-start, center);
width: 100%; width: 100%;
height: 49px; height: 40px;
padding: 0 18px; padding: 0 @spacing-base;
border: 1px solid @contract-line; border: 1px solid @border-color;
border-radius: 8px; border-radius: @radius-sm;
background: @contract-page-bg; background: @bg-color-light;
font-size: 20px; font-size: @font-size-base;
line-height: 29px; line-height: 1.4;
color: @contract-cell-text; color: @contract-cell-text;
box-sizing: border-box; box-sizing: border-box;
} }
&__link { &__link {
font-size: 18px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
color: @contract-accent; color: @contract-accent;
transition: opacity 0.2s; transition: opacity 0.2s;
@@ -216,18 +221,9 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
} }
} }
&__label &__link {
margin-left: 10px;
font-size: 16px;
}
&__files { &__files {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
position: absolute; gap: @spacing-sm;
top: 1001px; // 1153 − 152
left: 54px; // 124 − 70
width: 1640px;
gap: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
@@ -236,28 +232,29 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__file { &__file {
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
height: 90px; min-height: 64px;
padding: 0 30px; padding: 0 @spacing-base;
background: @contract-table-head-bg; border-radius: @radius-base;
background: @bg-color-light;
box-sizing: border-box; box-sizing: border-box;
} }
&__file-info { &__file-info {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 4px; gap: 2px;
min-width: 0; min-width: 0;
} }
&__file-name { &__file-name {
font-size: 20px; font-size: @font-size-base;
line-height: 29px; line-height: 1.4;
color: @contract-cell-text; color: @contract-cell-text;
.ellipsis(); .ellipsis();
} }
&__file-meta { &__file-meta {
font-size: 14px; font-size: @font-size-sm;
line-height: 20px; line-height: 1.4;
color: @contract-muted; color: @contract-muted;
} }
@@ -269,9 +266,11 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__empty { &__empty {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 90px; min-height: 64px;
padding: 0 30px; padding: 0 @spacing-base;
font-size: 18px; border-radius: @radius-base;
background: @bg-color-light;
font-size: @font-size-base;
color: @contract-muted; color: @contract-muted;
} }
} }

View File

@@ -25,9 +25,9 @@ const emit = defineEmits<{ refresh: [] }>()
<style lang="less" scoped> <style lang="less" scoped>
.sign-panel { .sign-panel {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 48px; gap: @spacing-xl;
padding: 28px 32px; padding: @spacing-xl;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
&__left { &__left {

View File

@@ -26,7 +26,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
<div class="contract-table"> <div class="contract-table">
<el-table <el-table
:data="contracts" :data="contracts"
border
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="contractNo" label="合同编号" show-overflow-tooltip /> <el-table-column prop="contractNo" label="合同编号" show-overflow-tooltip />
@@ -77,7 +76,7 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
<style lang="less" scoped> <style lang="less" scoped>
.contract-table { .contract-table {
:deep(.el-table) { :deep(.el-table) {
font-size: 20px; font-size: @font-size-sm;
--el-table-border-color: @contract-line; --el-table-border-color: @contract-line;
--el-table-header-bg-color: @contract-table-head-bg; --el-table-header-bg-color: @contract-table-head-bg;
--el-table-header-text-color: @contract-head-text; --el-table-header-text-color: @contract-head-text;
@@ -85,20 +84,26 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
--el-table-row-hover-bg-color: @bg-color-light; --el-table-row-hover-bg-color: @bg-color-light;
background: @white; background: @white;
&::before {
display: none;
}
.el-table__cell { .el-table__cell {
padding: 0; padding: 0;
border-right: none;
} }
.cell { .cell {
padding: 0 10px; padding: 0 @spacing-sm;
font-size: 20px; font-size: @font-size-sm;
line-height: 29px; line-height: 1.4;
} }
th.el-table__cell { th.el-table__cell {
height: 64px; height: 44px;
font-weight: 400; font-weight: 500;
background: @contract-table-head-bg; background: @contract-table-head-bg;
border-bottom: 1px solid @contract-line;
.cell { .cell {
color: @contract-head-text; color: @contract-head-text;
@@ -107,8 +112,9 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
} }
td.el-table__cell { td.el-table__cell {
height: 96px; height: 64px;
color: @contract-cell-text; color: @contract-cell-text;
border-bottom: 1px solid @contract-line;
} }
.el-table__row:hover > td.el-table__cell { .el-table__row:hover > td.el-table__cell {
@@ -118,11 +124,11 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__actions { &__actions {
.flex(row, center); .flex(row, center);
gap: @spacing-sm; gap: @spacing-base;
} }
&__btn { &__btn {
font-size: 13px; font-size: @font-size-sm;
color: @contract-accent; color: @contract-accent;
background: transparent; background: transparent;
border: none; border: none;

View File

@@ -65,17 +65,20 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.contract-detail { .contract-detail {
padding-bottom: @spacing-xxl; min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
.container { .container {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 0;
} }
&__breadcrumb { &__breadcrumb {
padding-top: 32px; padding-top: @spacing-lg;
font-size: 24px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 27px; line-height: 1.5;
color: @contract-muted; color: @contract-muted;
} }

View File

@@ -82,41 +82,40 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.contract { .contract {
padding-bottom: @spacing-xxl; min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__head { &__head {
padding: 24px 0 52px; padding: @spacing-lg 0;
background: @white; background: @white;
border-bottom: 1px solid @border-color;
.container {
padding-left: 42px;
}
} }
&__title { &__title {
font-size: 36px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.3;
color: @contract-ink; color: @contract-ink;
} }
&__slogan { &__slogan {
margin-top: 13.5px; margin-top: @spacing-sm;
font-size: 24px; font-size: @font-size-base;
font-weight: 500; font-weight: 400;
line-height: 37px; line-height: 1.5;
color: @contract-muted; color: @contract-muted;
} }
} }
.contract .contract__card { .contract .contract__card {
margin-top: 20px; margin-top: @spacing-lg;
padding: 51px 64px 59px; padding: @spacing-lg;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
:deep(.filter-panel) { :deep(.filter-panel) {
margin-bottom: 39px; margin-bottom: @spacing-lg;
padding: 0; padding: 0;
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
@@ -128,26 +127,19 @@ onMounted(() => {
} }
:deep(.filter-panel__bar) { :deep(.filter-panel__bar) {
gap: 8px; gap: @spacing-base;
}
.filter-panel__btn {
margin-left: 78px;
}
.filter-panel__btn--primary {
margin-left: 40px;
} }
:deep(.filter-panel__input) { :deep(.filter-panel__input) {
flex: 0 0 592px; flex: 0 1 360px;
width: 592px; width: auto;
max-width: none; max-width: 360px;
margin-right: 0; margin-right: 0;
height: 49px; height: 36px;
border-color: transparent; border: 1px solid @border-color;
border-radius: 0; border-radius: @radius-round;
background: @input-bg; background: @input-bg;
font-size: 20px; font-size: @font-size-base;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -157,53 +149,50 @@ onMounted(() => {
:deep(.filter-panel__input:focus) { :deep(.filter-panel__input:focus) {
border-color: @primary-strong; border-color: @primary-strong;
background: @white;
} }
:deep(.filter-panel__btn) { :deep(.filter-panel__btn) {
order: 1; min-width: 72px;
min-width: 0; height: 36px;
height: 29px; padding: 0 @spacing-lg;
padding: 0 12px;
border: none; border: none;
border-radius: 0; border-radius: @radius-round;
background: transparent; background: @pill-bg;
color: @contract-placeholder; color: @text-color;
font-size: 20px; font-size: @font-size-base;
} }
:deep(.filter-panel__btn:hover) { :deep(.filter-panel__btn:hover) {
border-color: transparent; color: @primary-color;
color: @text-color;
} }
:deep(.filter-panel__btn--primary) { :deep(.filter-panel__btn--primary) {
order: 2; background: @contract-accent;
border-radius: 0; color: @white;
background: transparent;
color: @contract-accent;
} }
:deep(.filter-panel__btn--primary:hover) { :deep(.filter-panel__btn--primary:hover) {
background: transparent; background: @primary-color;
color: @primary-color; color: @white;
} }
:deep(.pagination) { :deep(.pagination) {
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: @spacing-xs;
padding: 0; padding: 0;
margin-top: 28px; margin-top: @spacing-lg;
} }
:deep(.pagination__btn) { :deep(.pagination__btn) {
min-width: 40px; min-width: 28px;
height: 26px; height: 28px;
padding: 0 4px; padding: 0 6px;
border: 1px solid @contract-line; border: 1px solid @border-color;
border-radius: 0; border-radius: @radius-sm;
background: @white; background: @white;
color: @contract-line; color: @text-color;
font-size: 14px; font-size: @font-size-sm;
} }
:deep(.pagination__btn:hover:not(:disabled):not(.is-ellipsis)) { :deep(.pagination__btn:hover:not(:disabled):not(.is-ellipsis)) {
@@ -213,8 +202,9 @@ onMounted(() => {
:deep(.pagination__btn.is-active) { :deep(.pagination__btn.is-active) {
border-color: @contract-accent; border-color: @contract-accent;
background: @contract-accent; background: @primary-light;
color: @white; color: @contract-accent;
font-weight: 500;
} }
:deep(.pagination__btn.is-ellipsis) { :deep(.pagination__btn.is-ellipsis) {
@@ -222,20 +212,5 @@ onMounted(() => {
background: transparent; background: transparent;
color: @contract-muted; color: @contract-muted;
} }
:deep(.pagination__btn:first-child),
:deep(.pagination__btn:last-child) {
min-width: auto;
padding: 0 2px;
border: none;
background: transparent;
color: @contract-muted;
font-size: 12px;
}
:deep(.pagination__btn:first-child:disabled),
:deep(.pagination__btn:last-child:disabled) {
opacity: 1;
}
} }
</style> </style>

View File

@@ -63,16 +63,18 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.contract-sign { .contract-sign {
min-height: 100%;
padding-top: @spacing-lg; padding-top: @spacing-lg;
padding-bottom: @spacing-xxl; padding-bottom: @spacing-page-bottom;
background: @bg-color;
.container { .container {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 12px; gap: @spacing-base;
} }
&__breadcrumb { &__breadcrumb {
font-size: 11px; font-size: @font-size-sm;
color: @text-color-disabled; color: @text-color-disabled;
> span:last-child { > span:last-child {
@@ -84,16 +86,16 @@ onMounted(load)
&__footer { &__footer {
.flex(row, flex-end); .flex(row, flex-end);
width: 100%; width: 100%;
margin-top: 19px; margin-top: @spacing-base;
:deep(.app-button) { :deep(.app-button) {
height: 30px; height: 36px;
padding: 0 24px; padding: 0 @spacing-lg;
border: none; border: none;
border-radius: @radius-sm; border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 11px; font-size: @font-size-sm;
} }
:deep(.app-button:hover) { :deep(.app-button:hover) {

View File

@@ -49,35 +49,32 @@ const emit = defineEmits<{
<style lang="less" scoped> <style lang="less" scoped>
.demand-bar { .demand-bar {
// 设计稿里操作栏不是 fixed 底栏,而是卡片内的一行:
// el66 提示文字 x=240 y=2297,按钮 el36/34/32 x=1009/1288/1567 y=2280 w=232 h=63。
// 内容左边界 194,提示文字比它右移 46px;按钮右边界 1799 与输入框右边界齐平。
.flex(row, space-between, center); .flex(row, space-between, center);
margin-top: 66px; margin-top: @spacing-xxl;
gap: @spacing-lg;
&__tip { &__tip {
// 提示文字左边界 240 = 内容左边界 194 + 46。 margin: 0;
margin: 0 0 0 114px; font-size: @font-size-base;
font-size: 20px; font-weight: 400;
font-weight: 500; line-height: 1.5;
line-height: 29px; color: @text-color-secondary;
color: @demand-heading;
} }
&__actions { &__actions {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 47px; gap: @spacing-base;
flex-shrink: 0; flex-shrink: 0;
} }
&__btn { &__btn {
width: 232px; min-width: 120px;
height: 63px; height: 40px;
padding: 0; padding: 0 @spacing-lg;
border: none; border: none;
border-radius: @radius-base; border-radius: @radius-round;
background: @demand-secondary-bg; background: @demand-secondary-bg;
font-size: 20px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
color: @white; color: @white;
cursor: pointer; cursor: pointer;

View File

@@ -202,11 +202,10 @@ 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);
// el59标签底 1405 → el54 顶 1419 = 14px;el54 底 1509 → el53 顶 1529 = 20px。 gap: @spacing-base;
gap: 14px;
&__list { &__list {
gap: 20px; gap: @spacing-sm;
} }
&__head { &__head {
@@ -215,38 +214,38 @@ function onDownload(file: DemandAttachment): void {
&__label { &__label {
flex-shrink: 0; flex-shrink: 0;
font-size: 20px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 29px; line-height: 1.4;
color: @demand-heading; color: @demand-heading;
} }
&__hint { &__hint {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
margin-left: 20px; margin-left: @spacing-base;
font-size: 16px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 23px; line-height: 1.5;
color: @demand-line; color: @text-color-secondary;
} }
// 设计稿 el54:x=194 y=1419 w=1605 h=90,整块浅灰上传区。
&__drop { &__drop {
width: 100%; width: 100%;
height: 90px; height: 72px;
padding: 0; padding: 0;
border: none; border: 1px dashed @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @demand-upload-bg; background: @bg-color-light;
font-family: inherit; font-family: inherit;
font-size: 16px; font-size: @font-size-base;
color: @demand-line; color: @text-color-secondary;
cursor: pointer; cursor: pointer;
transition: background 0.2s, color 0.2s; transition: background 0.2s, color 0.2s, border-color 0.2s;
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: darken(@demand-upload-bg, 4%); border-color: @demand-accent;
background: @filter-selected-bg;
color: @demand-accent; color: @demand-accent;
} }
@@ -262,35 +261,34 @@ function onDownload(file: DemandAttachment): void {
&__list { &__list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 20px; gap: @spacing-sm;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
// el53:x=194 y=1529 w=1605 h=90,空态占位块;有附件时列表项本身就是这个高度。
&__list:empty { &__list:empty {
height: 90px; height: 72px;
border-radius: @radius-sm; border-radius: @radius-base;
background: @demand-upload-bg; background: @bg-color-light;
} }
&__item { &__item {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: @spacing-base; gap: @spacing-base;
min-height: 90px; min-height: 64px;
padding: 0 @spacing-lg; padding: 0 @spacing-base;
border-radius: @radius-sm; border-radius: @radius-base;
background: @demand-upload-bg; background: @bg-color-light;
} }
&__badge { &__badge {
.flex(row, center); .flex(row, center);
flex-shrink: 0; flex-shrink: 0;
width: 44px; width: 36px;
height: 44px; height: 36px;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 12px; font-size: @font-size-xs;
font-weight: 700; font-weight: 700;
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
@@ -298,18 +296,18 @@ function onDownload(file: DemandAttachment): void {
&__meta { &__meta {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
gap: 4px; gap: 2px;
min-width: 0; min-width: 0;
} }
&__name { &__name {
font-size: 20px; font-size: @font-size-base;
color: @demand-ink; color: @demand-ink;
.ellipsis(); .ellipsis();
} }
&__sub { &__sub {
font-size: 16px; font-size: @font-size-sm;
color: @text-color-secondary; color: @text-color-secondary;
} }

View File

@@ -39,7 +39,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
v-else v-else
class="biz-types__icon" class="biz-types__icon"
:name="option.icon" :name="option.icon"
:size="68" :size="48"
/> />
<span class="biz-types__name">{{ option.label }}</span> <span class="biz-types__name">{{ option.label }}</span>
<span v-if="option.desc" class="biz-types__desc">{{ option.desc }}</span> <span v-if="option.desc" class="biz-types__desc">{{ option.desc }}</span>
@@ -63,20 +63,17 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
&__grid { &__grid {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 17px; gap: @spacing-base;
} }
&__card { &__card {
position: relative; position: relative;
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 0; gap: 0;
height: 206px; min-height: 160px;
box-sizing: border-box; box-sizing: border-box;
// 设计稿 el21 "互联网软件" y=545 → 卡片 y=439 距顶 106px。 padding: @spacing-lg;
// 插画 el4 y=444 h=110 的框底 554 已越过 name 顶545,说明插画切片含 border: 1px solid @border-color;
// 透明边距;按文字层定位为准,插画框给68px 见方、左对齐即可。
padding: 4px 16px 0;
border: 1px solid @demand-line;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
text-align: left; text-align: left;
@@ -88,18 +85,17 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
} }
&.is-active { &.is-active {
border-color: @demand-selected-bg; border-color: @demand-accent;
background: @demand-selected-bg; background: @filter-selected-bg;
} }
} }
// el8 "圆形51" x=442 y=467 w=30 h=30:卡片 x=194 w=307,右内缩 29px、上移 22px。
&__check { &__check {
position: absolute; position: absolute;
top: 22px; top: @spacing-base;
right: 19px; right: @spacing-base;
width: 30px; width: 20px;
height: 30px; height: 20px;
border: 1px solid @demand-radio-idle; border: 1px solid @demand-radio-idle;
border-radius: 50%; border-radius: 50%;
background: @white; background: @white;
@@ -107,41 +103,42 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
} }
&__card.is-active &__check { &__card.is-active &__check {
border-color: @white; border-color: @demand-accent;
background: @white; background: @white;
box-shadow: inset 0 0 0 3px @demand-selected-bg; box-shadow: inset 0 0 0 5px @demand-accent;
} }
// el4 插画 x=233 / el3 x=542 / el1 x=875 / el2 x=1190:
// 四张卡的插画都在卡片左边线右侧 27~39px 处,不是水平居中,按左对齐处理。
&__icon { &__icon {
margin-bottom: 33px; margin-bottom: @spacing-base;
} }
&__logo { &__logo {
width: 68px; width: 48px;
height: 68px; height: 48px;
margin-bottom: 33px; margin-bottom: @spacing-base;
object-fit: contain; object-fit: contain;
} }
&__name { &__name {
font-size: 28px; font-size: @font-size-lg;
font-weight: 500; font-weight: 500;
line-height: 45px; line-height: 1.4;
color: @demand-ink; color: @demand-ink;
} }
&__desc { &__desc {
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 23px; line-height: 1.5;
color: @demand-ink; color: @text-color-secondary;
}
&__card.is-active &__name {
color: @demand-accent;
} }
&__card.is-active &__name,
&__card.is-active &__desc { &__card.is-active &__desc {
color: @white; color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -99,24 +99,24 @@ function onRemark(event: Event): void {
} }
&__label { &__label {
font-size: 20px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 29px; line-height: 1.4;
color: @demand-heading; color: @demand-heading;
} }
&__input { &__input {
.text-control(); .text-control();
height: 49px; height: 40px;
padding: 0 18px; padding: 0 @spacing-base;
border-color: @demand-line; border-color: @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 20px; font-size: @font-size-base;
color: @demand-ink; color: @demand-ink;
&::placeholder { &::placeholder {
font-weight: 400; font-weight: 400;
color: @demand-line; color: @text-color-disabled;
} }
} }
@@ -127,27 +127,27 @@ function onRemark(event: Event): void {
&__textarea { &__textarea {
.text-control(); .text-control();
height: 270px; height: 160px;
min-height: 270px; min-height: 160px;
padding: 10px 18px 24px; padding: @spacing-base;
border-color: @demand-line; border-color: @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 20px; font-size: @font-size-base;
color: @demand-ink; color: @demand-ink;
line-height: 29px; line-height: 1.5;
resize: vertical; resize: vertical;
&::placeholder { &::placeholder {
font-weight: 400; font-weight: 400;
color: @demand-line; color: @text-color-disabled;
} }
} }
&__count { &__count {
position: absolute; position: absolute;
right: @spacing-lg; right: @spacing-base;
bottom: @spacing-sm; bottom: @spacing-sm;
font-size: 16px; font-size: @font-size-sm;
color: @text-color-disabled; color: @text-color-disabled;
} }
} }

View File

@@ -201,9 +201,9 @@ function onBudgetManualBlur(event: Event): void {
} }
&__label { &__label {
font-size: 20px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 29px; line-height: 1.4;
color: @demand-heading; color: @demand-heading;
} }
@@ -214,16 +214,16 @@ function onBudgetManualBlur(event: Event): void {
&__input { &__input {
.text-control(); .text-control();
height: 49px; height: 40px;
padding: 0 18px; padding: 0 @spacing-base;
border-color: @demand-line; border-color: @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 20px; font-size: @font-size-base;
color: @demand-ink; color: @demand-ink;
&::placeholder { &::placeholder {
font-weight: 400; font-weight: 400;
color: @demand-line; color: @text-color-disabled;
} }
} }
@@ -234,47 +234,47 @@ function onBudgetManualBlur(event: Event): void {
&__textarea { &__textarea {
.text-control(); .text-control();
height: 270px; height: 160px;
min-height: 270px; min-height: 160px;
padding: 10px 18px 24px; padding: @spacing-base;
border-color: @demand-line; border-color: @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 20px; font-size: @font-size-base;
color: @demand-ink; color: @demand-ink;
line-height: 29px; line-height: 1.5;
resize: vertical; resize: vertical;
&::placeholder { &::placeholder {
font-weight: 400; font-weight: 400;
color: @demand-line; color: @text-color-disabled;
} }
} }
&__count { &__count {
position: absolute; position: absolute;
right: @spacing-lg; right: @spacing-base;
bottom: @spacing-sm; bottom: @spacing-sm;
font-size: 16px; font-size: @font-size-sm;
color: @text-color-disabled; color: @text-color-disabled;
} }
&__budgets { &__budgets {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
gap: 17px; gap: @spacing-base;
} }
&__budget { &__budget {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 49px; height: 40px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border: 1px solid @demand-line; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @white; background: @white;
font-size: 20px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
font-family: inherit; font-family: inherit;
color: @demand-line; color: @text-color;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -287,8 +287,8 @@ function onBudgetManualBlur(event: Event): void {
&.is-active { &.is-active {
border-color: @demand-accent; border-color: @demand-accent;
background: @demand-selected-bg; background: @filter-selected-bg;
color: @white; color: @demand-accent;
font-weight: 500; font-weight: 500;
} }
} }
@@ -298,7 +298,7 @@ function onBudgetManualBlur(event: Event): void {
&::placeholder { &::placeholder {
font-weight: 400; font-weight: 400;
color: @demand-line; color: @text-color-disabled;
} }
} }
} }

View File

@@ -120,7 +120,9 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.demand-page { .demand-page {
// 操作栏已收进卡片内(设计稿 el36 y=2280),不需要为fixed 底栏预留空白。 min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&.is-loading .demand { &.is-loading .demand {
opacity: 0.6; opacity: 0.6;
@@ -128,65 +130,60 @@ onMounted(() => {
} }
&__hero { &__hero {
height: 179px;
box-sizing: border-box; box-sizing: border-box;
padding-top: 32px; padding: @spacing-lg 0;
background: @white; background: @white;
border-bottom: 1px solid @border-color;
} }
&__hero-inner { &__hero-inner {
// 设计稿 el82:标题左边界 112px(hero 白底 1920 宽,标题左起 112)。 padding-left: @spacing-xl;
padding-left: 112px;
} }
&__title { &__title {
margin: 0; margin: 0;
font-size: 36px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.3;
color: @demand-ink; color: @demand-ink;
} }
&__subtitle { &__subtitle {
margin: 15px 0 0; margin: @spacing-sm 0 0;
font-size: 24px; font-size: @font-size-base;
font-weight: 500; font-weight: 400;
line-height: 35px; line-height: 1.5;
color: @demand-subtitle; color: @demand-subtitle;
} }
} }
.demand { .demand {
margin-top: 20px; // 271 − (72+179) = 20 margin-top: @spacing-lg;
// 设计稿 el83 卡片 x=70 w=1780;内容左边界 194(el68 输入框 x=194 w=1605), padding: @spacing-xl;
// 右边界 1799。故左内边距 56、右内边距 51(不对称是设计稿本身如此)。 border-radius: @radius-base;
padding: 48px 51px 114px 56px;
background: @white; background: @white;
&__heading { &__heading {
// 设计稿三个 heading 的标签都比序号低 23px(el80 "01" y=319 → .flex(row, flex-start, center);
// el79 "选择业务类型*" y=342),不是垂直居中,故用flex-start + margin-top。 gap: @spacing-sm;
.flex(row, flex-start, flex-start); margin: 0 0 @spacing-lg;
gap: 4px; padding-left: 0;
margin: 0 0 50px;
padding-left: 8px;
} }
&__index { &__index {
flex-shrink: 0; flex-shrink: 0;
font-size: 48px; font-size: @font-size-title;
font-weight: 500; font-weight: 600;
line-height: 70px; line-height: 1.2;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: @demand-accent; color: @demand-accent;
} }
&__label { &__label {
// 与序号的顶部落差 23px(el80 y=319 → el79 y=342)。 margin-top: 0;
margin-top: 23px; font-size: @font-size-lg;
font-size: 28px;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 1.4;
color: @demand-heading; color: @demand-heading;
} }
@@ -195,28 +192,24 @@ onMounted(() => {
color: @danger-color; color: @danger-color;
} }
// 内容左边界 194:卡片 x=70 + padding-left 56 + 内容缩进 68。
&__heading + * { &__heading + * {
margin-left: 68px; margin-left: 0;
} }
// el29业务类型卡片底 645 → el78 "02" 顶 705 = 60px。
&__section + &__section { &__section + &__section {
margin-top: 60px; margin-top: @spacing-xxl;
} }
// el53 附件灰块底 1619 → el76 "03" 顶 1689 = 70px。
&__section + &__section:last-of-type { &__section + &__section:last-of-type {
margin-top: 70px; margin-top: @spacing-xxl;
} }
&__section + &__section &__heading { &__section + &__section &__heading {
margin-bottom: 0; margin-bottom: @spacing-lg;
} }
// el78 "02" 底 775 → el70 "项目名称*" 顶 789 = 14px。
&__section + &__section &__heading + * { &__section + &__section &__heading + * {
margin-top: 14px; margin-top: 0;
} }
} }
</style> </style>

View File

@@ -150,16 +150,17 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.bill-detail { .bill-detail {
width: 100%; width: 100%;
padding-bottom: 325px; min-height: 100%;
background: @contract-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 86px; min-height: 48px;
padding-top: 34px; padding-top: @spacing-lg;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.5;
color: @project-muted; color: @project-muted;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -154,8 +154,9 @@ onMounted(() => {
align-items: stretch; align-items: stretch;
gap: 0; gap: 0;
width: 100%; width: 100%;
padding-bottom: 14px; min-height: 100%;
background: @contract-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
.flex(row, flex-start, center); .flex(row, flex-start, center);
@@ -185,9 +186,9 @@ onMounted(() => {
&__stats { &__stats {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 0; gap: 0;
height: 106px; min-height: 96px;
margin-top: 11px; margin-top: @spacing-base;
border: 1px solid @finance-card-line; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
@@ -209,19 +210,19 @@ onMounted(() => {
} }
&__stat-value { &__stat-value {
font-size: 26px; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
line-height: 35px; line-height: 1.3;
color: @finance-ink; color: @finance-ink;
} }
&__toolbar { &__toolbar {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 0; gap: @spacing-base;
height: 71px; min-height: 64px;
margin-top: 14px; margin-top: @spacing-base;
padding: 0 0 0 22px; padding: 0 @spacing-lg;
border: 1px solid @finance-card-line; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
@@ -249,27 +250,29 @@ onMounted(() => {
:deep(.el-radio-button.is-active .el-radio-button__inner) { :deep(.el-radio-button.is-active .el-radio-button__inner) {
border-color: @primary-strong; border-color: @primary-strong;
background: @primary-strong; background: @filter-selected-bg;
color: @white; color: @primary-strong;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
box-shadow: -1px 0 0 0 @primary-strong; box-shadow: none;
} }
} }
&__search { &__search {
width: 1300px; flex: 0 1 360px;
height: 37px; width: auto;
margin-left: 33px; height: 36px;
flex-shrink: 0; margin-left: 0;
flex-shrink: 1;
min-width: 0;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
width: 1300px; width: 100%;
height: 37px; height: 36px;
padding: 0 16px; padding: 0 @spacing-base;
background: @finance-field-bg; background: @finance-field-bg;
box-shadow: none; box-shadow: 0 0 0 1px @border-color inset;
border-radius: 0; border-radius: @radius-round;
} }
:deep(.el-input__inner) { :deep(.el-input__inner) {
@@ -314,36 +317,41 @@ onMounted(() => {
} }
&__board { &__board {
margin-top: 14px; margin-top: @spacing-base;
border: 1px solid @finance-card-line; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
overflow: hidden; overflow: hidden;
padding: @spacing-base @spacing-lg @spacing-lg;
} }
:deep(.pagination) { :deep(.pagination) {
justify-content: flex-end; justify-content: flex-end;
gap: 3.7px; gap: @spacing-xs;
padding: 8px 26px 30px 0; padding: @spacing-base 0 0;
} }
:deep(.pagination__btn) { :deep(.pagination__btn) {
min-width: 0; min-width: 28px;
height: 16px; height: 28px;
padding: 0 3px; padding: 0 6px;
border: none; border: 1px solid @border-color;
border-radius: 0; border-radius: @radius-sm;
background: transparent; background: @white;
color: @finance-muted-text; color: @text-color;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
&.is-active { &.is-active {
min-width: 21.45px; min-width: 28px;
border-color: @primary-strong;
background: @primary-light;
color: @primary-strong;
font-weight: 500;
} }
&:hover:not(:disabled):not(.is-ellipsis) { &:hover:not(:disabled):not(.is-ellipsis) {
border: none; border-color: @primary-strong;
color: @primary-strong; color: @primary-strong;
} }

View File

@@ -210,11 +210,13 @@ onBeforeUnmount(() => {
.checkout { .checkout {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: @spacing-base; gap: @spacing-base;
min-height: 100%;
padding-top: @spacing-lg; padding-top: @spacing-lg;
padding-bottom: @spacing-xxl; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
font-size: @font-size-base; font-size: @font-size-sm;
} }
&__layout { &__layout {

View File

@@ -126,31 +126,36 @@ watch(
<style lang="less" scoped> <style lang="less" scoped>
.bill-table { .bill-table {
width: 1765px; width: 100%;
margin: 6.5px 0 0 14.5px; margin: @spacing-sm 0 0;
font-size: 12px; font-size: @font-size-sm;
color: @finance-entry-text; color: @finance-entry-text;
:deep(.el-table) {
--el-table-border-color: @border-color;
&::before {
display: none;
}
}
:deep(.el-table__header-wrapper th.el-table__cell) { :deep(.el-table__header-wrapper th.el-table__cell) {
height: 41.77px; height: 44px;
padding: 0; padding: 0;
background: @finance-table-head-bg; background: @finance-table-head-bg;
border-right: 1px solid @project-divider; border-right: none;
border-bottom: 1px solid @project-divider; border-bottom: 1px solid @border-color;
color: @finance-entry-text; color: @text-color-secondary;
font-size: 12px; font-size: @font-size-sm;
font-weight: 600; font-weight: 500;
text-align: left; text-align: left;
} }
:deep(.el-table__header-wrapper th.el-table__cell:first-child) {
border-left: 1px solid @project-divider;
}
:deep(.el-table__body-wrapper td.el-table__cell) { :deep(.el-table__body-wrapper td.el-table__cell) {
height: 43.77px; height: 56px;
padding: 0; padding: 0;
border-bottom: 1px solid @project-divider; border-right: none;
border-bottom: 1px solid @border-color;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -159,17 +164,16 @@ watch(
} }
:deep(.el-table .cell) { :deep(.el-table .cell) {
padding-left: 10px; padding-left: @spacing-sm;
padding-right: 10px; padding-right: @spacing-sm;
line-height: normal; line-height: normal;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// 首列(多选)左缩进对齐设计稿
:deep(.el-table-column--selection .cell) { :deep(.el-table-column--selection .cell) {
padding-left: 43.5px; padding-left: @spacing-base;
} }
&__ops { &__ops {
@@ -177,12 +181,12 @@ watch(
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 18px; gap: @spacing-base;
white-space: nowrap; white-space: nowrap;
} }
&__link { &__link {
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
padding: 0; padding: 0;
} }

View File

@@ -164,20 +164,22 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.finance { .finance {
width: 100%; width: 100%;
min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__top { &__top {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
gap: 14px; gap: @spacing-base;
margin-top: -56px; margin-top: @spacing-lg;
} }
&__entries { &__entries {
.flex(row, space-between, center); .flex(row, space-between, center);
flex: 0 0 672px; flex: 0 0 672px;
height: 135px; height: 135px;
// 设计稿 #20:首入口中心 x=1272、尾中心 x=1756,两侧对称 padding 66px 命中
padding: 0 66px; padding: 0 66px;
border: 1px solid @finance-card-line; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -137,16 +137,17 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.invoice-titles { .invoice-titles {
width: 100%; width: 100%;
padding-bottom: 459px; min-height: 100%;
background: @contract-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 81px; min-height: 48px;
padding-top: 36px; padding-top: @spacing-lg;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.5;
color: @project-muted; color: @project-muted;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -167,9 +168,9 @@ onMounted(() => {
&__head { &__head {
.flex(row, space-between, center); .flex(row, space-between, center);
height: 62px; min-height: 56px;
padding: 0 33.56px 0 36px; padding: 0 @spacing-lg;
border: 1px solid @finance-card-line; border: none;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -265,8 +265,10 @@ onMounted(() => {
gap: @spacing-base; gap: @spacing-base;
width: 100%; width: 100%;
max-width: @container-width; max-width: @container-width;
min-height: 100%;
margin: 0 auto; margin: 0 auto;
padding: @spacing-lg @spacing-base @spacing-xxl; padding: @spacing-lg @spacing-base @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
font-size: 11px; font-size: 11px;
@@ -346,13 +348,14 @@ onMounted(() => {
&.is-active { &.is-active {
border-color: @primary-color; border-color: @primary-color;
background: @primary-color; background: @filter-selected-bg;
color: @white; color: @primary-color;
font-weight: 500;
&:hover { &:hover {
border-color: @primary-color; border-color: @primary-color;
background: @primary-color; background: @filter-selected-bg;
color: @white; color: @primary-color;
} }
} }
} }

View File

@@ -131,15 +131,16 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.pay-result { .pay-result {
width: 100%; width: 100%;
padding-bottom: 344px; min-height: 100%;
background: @contract-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
.flex(row, flex-start, center); .flex(row, flex-start, center);
padding-top: 22px; padding-top: @spacing-lg;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.5;
color: @project-muted; color: @project-muted;
} }
@@ -164,13 +165,13 @@ onMounted(() => {
&__check { &__check {
.flex(row, center, center); .flex(row, center, center);
width: 120px; width: 72px;
height: 120px; height: 72px;
margin: 0 auto; margin: 0 auto;
border-radius: 50%; border-radius: 50%;
background: @finance-result-circle; background: @finance-result-circle;
color: @white; color: @white;
font-size: 56px; font-size: @font-size-hero;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
} }

View File

@@ -161,8 +161,10 @@ onMounted(() => {
.pay { .pay {
width: 100%; width: 100%;
max-width: @container-width; max-width: @container-width;
min-height: 100%;
margin: 0 auto; margin: 0 auto;
padding: @spacing-lg @spacing-base @spacing-xxl; padding: @spacing-lg @spacing-base @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
margin-bottom: @spacing-base; margin-bottom: @spacing-base;
@@ -235,8 +237,8 @@ onMounted(() => {
&__amount { &__amount {
margin-top: 4px; margin-top: 4px;
font-size: 26px; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
color: @danger-color; color: @danger-color;
line-height: 1.2; line-height: 1.2;
} }

View File

@@ -61,59 +61,92 @@ onBeforeUnmount(stopAutoplay)
<template> <template>
<section class="home-banner"> <section class="home-banner">
<div class="home-banner__carousel" aria-hidden="true"> <div class="home-banner__stage">
<div <div class="home-banner__carousel" aria-hidden="true">
v-for="(src, index) in slides" <div
:key="`${src}-${index}`" v-for="(src, index) in slides"
class="home-banner__slide" :key="`${src}-${index}`"
:class="{ 'home-banner__slide--active': index === activeIndex }" class="home-banner__slide"
> :class="{ 'home-banner__slide--active': index === activeIndex }"
<img
class="home-banner__image"
:class="{ 'home-banner__image--inline': !hasBanners }"
:src="src"
alt=""
> >
<img
class="home-banner__image"
:class="{ 'home-banner__image--inline': !hasBanners }"
:src="src"
alt=""
>
</div>
</div> </div>
</div>
<div class="home-banner__inner"> <div class="home-banner__inner">
<h1 class="home-banner__title">虫子智造研发一体化平台</h1> <h1 class="home-banner__title">
<p class="home-banner__subtitle">一站式智能软硬件研发与量产综合服务商</p> 虫子智造研发一体化平台
</h1>
<p class="home-banner__subtitle">
一站式智能软硬件研发与量产综合服务商
</p>
<div class="home-banner__actions"> <div class="home-banner__actions">
<button class="home-banner__btn home-banner__btn--primary" type="button" @click="goDemand"> <button
需求提报 class="home-banner__btn home-banner__btn--primary"
</button> type="button"
<button class="home-banner__btn home-banner__btn--ghost" type="button" @click="goCases"> @click="goDemand"
案例中心 >
</button> 需求提报
</button>
<button
class="home-banner__btn home-banner__btn--ghost"
type="button"
@click="goCases"
>
案例中心
</button>
</div>
</div> </div>
</div>
<div v-if="slides.length > 1" class="home-banner__dots" role="tablist" aria-label="头图轮播"> <div
<button v-if="slides.length > 1"
v-for="(_, index) in slides" class="home-banner__dots"
:key="index" role="tablist"
class="home-banner__dot" aria-label="头图轮播"
:class="{ 'home-banner__dot--active': index === activeIndex }" >
type="button" <button
role="tab" v-for="(_, index) in slides"
:aria-selected="index === activeIndex" :key="index"
@click="goTo(index)" class="home-banner__dot"
/> :class="{ 'home-banner__dot--active': index === activeIndex }"
type="button"
role="tab"
:aria-selected="index === activeIndex"
@click="goTo(index)"
/>
</div>
</div> </div>
</section> </section>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.home-banner { .home-banner {
position: relative;
height: 661px;
overflow: hidden;
background: @home-banner-bg;
width: 100%; width: 100%;
flex: 0 0 auto; flex: 0 0 auto;
padding: @spacing-lg @home-gutter 0;
box-sizing: border-box;
background: transparent;
@media (max-width: 1600px) {
padding: @spacing-lg @spacing-lg 0;
}
&__stage {
position: relative;
width: 100%;
max-width: @home-content;
height: 420px;
margin: 0 auto;
overflow: hidden;
border-radius: @radius-base;
background: @home-banner-bg;
}
&__carousel { &__carousel {
position: absolute; position: absolute;
@@ -138,15 +171,14 @@ onBeforeUnmount(stopAutoplay)
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
// 内置插图:按设计稿定位与尺寸原样显示,不铺满
&--inline { &--inline {
position: absolute; position: absolute;
top: 31px; top: 24px;
right: 269px; right: 48px;
left: auto; left: auto;
bottom: auto; bottom: auto;
width: 723px; width: 420px;
height: 598px; height: 348px;
margin-left: auto; margin-left: auto;
object-fit: contain; object-fit: contain;
} }
@@ -156,50 +188,48 @@ onBeforeUnmount(stopAutoplay)
position: relative; position: relative;
z-index: 1; z-index: 1;
width: 100%; width: 100%;
max-width: @container-width;
height: 100%; height: 100%;
margin: 0 auto;
pointer-events: none; pointer-events: none;
} }
&__title { &__title {
position: absolute; position: absolute;
top: 184px; top: 96px;
left: 150px; left: 64px;
margin: 0; margin: 0;
color: @banner-title-color; color: @banner-title-color;
font-size: @font-size-hero-lg; font-size: @font-size-hero;
font-weight: 700; font-weight: 700;
line-height: 87px; line-height: 1.25;
white-space: nowrap; white-space: nowrap;
} }
&__subtitle { &__subtitle {
position: absolute; position: absolute;
top: 287px; top: 168px;
left: 150px; left: 64px;
margin: 0; margin: 0;
color: @banner-title-color; color: @banner-title-color;
font-size: 24px; font-size: @font-size-lg;
font-weight: 500; font-weight: 500;
line-height: 35px; line-height: 1.4;
white-space: nowrap; white-space: nowrap;
} }
&__actions { &__actions {
position: absolute; position: absolute;
top: 392px; top: 232px;
left: 150px; left: 64px;
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 20px; gap: @spacing-base;
pointer-events: auto; pointer-events: auto;
} }
&__btn { &__btn {
width: 183px; width: 140px;
height: 55px; height: 44px;
border-radius: 8px; border-radius: @radius-base;
font-size: 18px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
line-height: 1; line-height: 1;
cursor: pointer; cursor: pointer;
@@ -220,7 +250,7 @@ onBeforeUnmount(stopAutoplay)
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
border-radius: 8px; border-radius: @radius-base;
padding: 2px; padding: 2px;
background: linear-gradient(135deg, @btn-gradient-from 0%, @btn-gradient-to 100%); background: linear-gradient(135deg, @btn-gradient-from 0%, @btn-gradient-to 100%);
-webkit-mask: -webkit-mask:
@@ -238,16 +268,17 @@ onBeforeUnmount(stopAutoplay)
&__dots { &__dots {
position: absolute; position: absolute;
left: 929px; left: 50%;
top: 594px; bottom: @spacing-base;
z-index: 1; z-index: 1;
.flex(row, flex-start, center); .flex(row, center, center);
gap: 13px; gap: @spacing-sm;
transform: translateX(-50%);
} }
&__dot { &__dot {
width: 5px; width: 6px;
height: 5px; height: 6px;
padding: 0; padding: 0;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
@@ -255,7 +286,7 @@ onBeforeUnmount(stopAutoplay)
cursor: pointer; cursor: pointer;
&--active { &--active {
width: 44px; width: 28px;
border-radius: 3px; border-radius: 3px;
background: @home-banner-dot; background: @home-banner-dot;
} }

View File

@@ -1,6 +1,7 @@
<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 defaultCover from '@/assets/home/chahua.png'
interface Props { interface Props {
lines: BusinessLine[] lines: BusinessLine[]
@@ -9,6 +10,16 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const FALLBACK_ACTIVE: BusinessType[] = ['internet'] const FALLBACK_ACTIVE: BusinessType[] = ['internet']
function isImageUrl(value: string): boolean {
return /^(https?:)?\/\//i.test(value) || value.startsWith('/') || value.startsWith('data:')
}
function coverSrc(line: BusinessLine): string {
const remote = (line.cover || line.icon).trim()
if (remote && isImageUrl(remote)) return remote
return defaultCover
}
</script> </script>
<template> <template>
@@ -25,6 +36,11 @@ const FALLBACK_ACTIVE: BusinessType[] = ['internet']
'home-business__card--active': FALLBACK_ACTIVE.includes(line.id), 'home-business__card--active': FALLBACK_ACTIVE.includes(line.id),
}" }"
> >
<img
class="home-business__cover"
:src="coverSrc(line)"
: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 }}
@@ -39,61 +55,73 @@ const FALLBACK_ACTIVE: BusinessType[] = ['internet']
<style lang="less" scoped> <style lang="less" scoped>
.home-business { .home-business {
padding: 44px 70px 54px; padding: @spacing-xl;
border: 1px solid @home-section-border; border: none;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
&__title { &__title {
margin: 0 0 47px; margin: 0 0 @spacing-lg;
font-size: @font-size-xxl; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
color: @home-ink; color: @home-ink;
} }
&__grid { &__grid {
display: flex; display: grid;
gap: 22px; grid-template-columns: repeat(4, 1fr);
gap: @spacing-base;
} }
&__card { &__card {
position: relative; position: relative;
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
flex: 0 1 368px;
min-width: 0; min-width: 0;
height: 206px; min-height: 168px;
box-sizing: border-box; box-sizing: border-box;
padding: 0 20px; padding: @spacing-lg;
overflow: hidden; overflow: hidden;
border: 1px solid @home-business-border; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
transition: transition:
transform 0.2s, transform 0.2s,
box-shadow 0.2s; box-shadow 0.2s,
border-color 0.2s;
&:hover { &:hover {
border-color: @primary-color;
box-shadow: @shadow-base; box-shadow: @shadow-base;
transform: translateY(-2px); transform: translateY(-2px);
} }
} }
&__cover {
display: block;
width: 72px;
height: 72px;
object-fit: contain;
flex-shrink: 0;
}
&__check { &__check {
position: absolute; position: absolute;
top: 27px; top: @spacing-base;
right: 37px; right: @spacing-base;
z-index: 1;
flex-shrink: 0; flex-shrink: 0;
width: 31px; width: 20px;
height: 31px; height: 20px;
border: 1px solid @home-ring; border: 1px solid @home-ring;
border-radius: @radius-round; border-radius: @radius-round;
background: @white;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 21px; width: 12px;
height: 21px; height: 12px;
margin: auto; margin: auto;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
@@ -102,37 +130,43 @@ const FALLBACK_ACTIVE: BusinessType[] = ['internet']
} }
&__name { &__name {
margin: 104px 0 0; margin: @spacing-base 0 0;
font-size: @font-size-title; padding-right: @spacing-xl;
font-size: @font-size-lg;
font-weight: 500; font-weight: 500;
line-height: 41px; line-height: 1.4;
color: @home-ink; color: @home-ink;
} }
&__desc { &__desc {
margin: 4px 0 0; margin: @spacing-xs 0 0;
font-size: @font-size-md; padding-right: @spacing-xl;
font-weight: 500; font-size: @font-size-sm;
line-height: 23px; font-weight: 400;
color: @home-ink; line-height: 1.5;
.multi-ellipsis(1); color: @text-color-secondary;
.multi-ellipsis(2);
} }
&__card--active { &__card--active {
border-color: @primary-strong; border-color: @primary-color;
background: @primary-strong; background: @filter-selected-bg;
.home-business__check { .home-business__check {
border-color: @white; border-color: @primary-color;
&::after { &::after {
opacity: 1; opacity: 1;
background: @primary-color;
} }
} }
.home-business__name, .home-business__name {
color: @primary-strong;
}
.home-business__desc { .home-business__desc {
color: @white; color: @text-color-secondary;
} }
} }
} }

View File

@@ -33,9 +33,7 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
<template> <template>
<section class="home-cases"> <section class="home-cases">
<div class="home-cases__head"> <div class="home-cases__head">
<h3 class="home-cases__title"> <h3 class="home-cases__title">案例库精选</h3>
案例库精选
</h3>
<div class="home-cases__tabs"> <div class="home-cases__tabs">
<button <button
v-for="cat in categories" v-for="cat in categories"
@@ -60,7 +58,12 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
@click="$router.push(`/cases/${item.id}`)" @click="$router.push(`/cases/${item.id}`)"
> >
<div class="home-cases__cover"> <div class="home-cases__cover">
<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" />
</div> </div>
@@ -85,26 +88,26 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
<style lang="less" scoped> <style lang="less" scoped>
.home-cases { .home-cases {
padding: 40px 66px 60px; padding: @spacing-xl;
border: 1px solid @home-section-border; border: none;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
&__head { &__head {
.flex(row, flex-start); .flex(row, flex-start, center);
gap: 24px; gap: @spacing-lg;
margin-bottom: 47px; margin-bottom: @spacing-lg;
} }
&__title { &__title {
font-size: @font-size-xxl; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
color: @home-ink; color: @home-ink;
} }
&__tabs { &__tabs {
.flex(row, center); .flex(row, center);
gap: 36px; gap: @spacing-lg;
} }
&__tab { &__tab {
@@ -140,25 +143,29 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
} }
&__grid { &__grid {
display: flex; display: grid;
gap: 24px; grid-template-columns: repeat(4, 1fr);
gap: @spacing-lg;
} }
&__card { &__card {
overflow: hidden; overflow: hidden;
flex: 0 1 368px;
min-width: 0; min-width: 0;
height: 248px; min-height: 220px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid @home-case-border; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: 0 4px 10px fade(@black, 8%); box-shadow: none;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition:
transform 0.2s,
box-shadow 0.2s,
border-color 0.2s;
&:hover { &:hover {
box-shadow: 0 6px 14px fade(@black, 12%); border-color: @primary-color;
box-shadow: @shadow-base;
transform: translateY(-2px); transform: translateY(-2px);
.home-cases__view { .home-cases__view {
@@ -184,7 +191,7 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
} }
&__body { &__body {
padding: 0 20px; padding: 0 20px 10px 20px;
} }
&__name { &__name {
@@ -200,7 +207,7 @@ const visibleCases = computed<Case[]>(() => props.cases.slice(0, 4))
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 0 @spacing-lg; gap: 0 @spacing-lg;
margin-top: 43px; margin-top: 20px;
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
color: @home-body; color: @home-body;

View File

@@ -173,15 +173,15 @@ 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 0 529px; flex: 0 0 420px;
width: 529px; width: 420px;
height: 245px; min-height: 0;
box-sizing: border-box; box-sizing: border-box;
min-width: 0; min-width: 0;
gap: 0; gap: 0;
padding: 18px 40px 28px; padding: @spacing-lg;
border: 1px solid @home-card-border; border: none;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
overflow: hidden; overflow: hidden;
@@ -190,22 +190,22 @@ function onMore(): void {
} }
&__title { &__title {
font-size: @font-size-xxl; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.4;
color: @home-ink; color: @home-ink;
} }
&__list { &__list {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 30px; gap: @spacing-base;
margin-top: 17px; margin-top: @spacing-base;
flex-wrap: wrap; flex-wrap: wrap;
} }
&__item { &__item {
.flex(column, flex-start, center); .flex(column, flex-start, center);
gap: 8px; gap: @spacing-sm;
flex: none; flex: none;
transition: transform 0.2s; transition: transform 0.2s;
@@ -216,14 +216,14 @@ function onMore(): void {
&__tile { &__tile {
display: block; display: block;
width: 97px; width: 72px;
height: 97px; height: 72px;
border-radius: @radius-base; border-radius: @radius-base;
object-fit: cover; object-fit: cover;
} }
&__label { &__label {
font-size: @font-size-lg; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
color: @home-ink; color: @home-ink;
text-align: center; text-align: center;
@@ -231,15 +231,16 @@ function onMore(): void {
&__more { &__more {
position: absolute; position: absolute;
top: 120px; top: 50%;
right: 25px; right: @spacing-base;
width: 32px; width: 28px;
height: 32px; height: 28px;
padding: 0; padding: 0;
border: 0; border: 0;
background: transparent; background: transparent;
color: #c9cdd4; color: @text-color-disabled;
cursor: pointer; cursor: pointer;
transform: translateY(-50%);
transition: color 0.2s; transition: color 0.2s;
&:hover { &:hover {

View File

@@ -27,10 +27,10 @@ defineProps<Props>()
.home-contents { .home-contents {
&__block { &__block {
margin-bottom: @spacing-base; margin-bottom: @spacing-base;
padding: 40px 45px 0; padding: @spacing-xl;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
min-height: 593px; min-height: 0;
box-sizing: border-box; box-sizing: border-box;
&:last-child { &:last-child {
@@ -39,10 +39,10 @@ defineProps<Props>()
} }
&__title { &__title {
margin: 0 0 301px; margin: 0 0 @spacing-lg;
font-size: 36px; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.4;
color: @home-ink; color: @home-ink;
} }

View File

@@ -43,40 +43,41 @@ function isImageIcon(icon?: string): boolean {
<style lang="less" scoped> <style lang="less" scoped>
.home-service-types { .home-service-types {
margin-bottom: 20px; margin-bottom: @spacing-base;
padding: 24px 42px 0; padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
height: 287px; min-height: 0;
box-sizing: border-box; box-sizing: border-box;
&__title { &__title {
margin: 0 0 132px; margin: 0 0 @spacing-lg;
font-size: 36px; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
line-height: 52px; line-height: 1.4;
color: @home-ink; color: @home-ink;
} }
&__list { &__list {
.flex(row, flex-start); .flex(row, flex-start);
gap: 0; gap: @spacing-base;
flex-wrap: wrap;
} }
&__item { &__item {
.flex(column, center); .flex(column, center);
width: 187px; width: 120px;
gap: 47px; gap: @spacing-base;
} }
&__icon { &__icon {
.flex(row, center); .flex(row, center);
width: 68px; width: 48px;
height: 68px; height: 48px;
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-light; background: @primary-light;
color: @white; color: @white;
font-size: 24px; font-size: @font-size-lg;
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;

View File

@@ -30,30 +30,30 @@ const items = [
<style lang="less" scoped> <style lang="less" scoped>
.home-stats { .home-stats {
.flex(row, flex-end); .flex(row, flex-end);
gap: 30px; gap: @spacing-base;
&__item { &__item {
.flex(column, center, center); .flex(column, center, center);
gap: 0; gap: 2px;
width: 112px; width: 88px;
height: 112px; height: 88px;
min-width: 112px; min-width: 88px;
padding: 0; padding: 0;
border-radius: @radius-sm; border-radius: @radius-base;
background: @primary-strong; background: @filter-selected-bg;
} }
&__value { &__value {
font-size: 48px; font-size: @font-size-title;
font-weight: 500; font-weight: 600;
line-height: 1.2; line-height: 1.2;
color: @white; color: @primary-strong;
} }
&__label { &__label {
font-size: @font-size-base; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
color: @white; color: @text-color-secondary;
} }
} }
</style> </style>

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;
font-size: 52px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
color: @home-welcome; color: @home-welcome;
} }
&__wave { &__wave {
.flex(row, center); .flex(row, center);
width: 44px; width: 32px;
height: 44px; height: 32px;
border-radius: @radius-round; border-radius: @radius-round;
background: fade(@black, 4%); background: fade(@black, 4%);
font-size: @font-size-xl; font-size: @font-size-lg;
} }
&__summary { &__summary {
font-size: 24px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
color: @home-welcome; color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -58,14 +58,16 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.business-detail { .business-detail {
padding-top: 32px; min-height: 100%;
padding-bottom: @spacing-xxl; padding-top: @spacing-lg;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__breadcrumb { &__breadcrumb {
margin-bottom: 49px; margin-bottom: @spacing-lg;
font-size: 24px; font-size: @font-size-base;
font-weight: 500; font-weight: 400;
line-height: 37px; line-height: 1.5;
color: @home-breadcrumb; color: @home-breadcrumb;
.link { .link {
@@ -74,18 +76,19 @@ onMounted(load)
} }
&__hero { &__hero {
.flex(column, flex-start); .flex(column, flex-start, flex-start);
justify-content: center;
margin-bottom: @spacing-base; margin-bottom: @spacing-base;
padding: 129px 29px 0; padding: @spacing-xl;
border-radius: @radius-lg; border-radius: @radius-base;
background: @home-detail-hero-bg; background: @home-detail-hero-bg;
height: 219px; min-height: 140px;
box-sizing: border-box; box-sizing: border-box;
} }
&__loading { &__loading {
padding: 80px 0; padding: @spacing-xxl 0;
font-size: 16px; font-size: @font-size-base;
color: @text-color-secondary; color: @text-color-secondary;
} }
@@ -95,23 +98,23 @@ onMounted(load)
&__intro { &__intro {
.flex(column, flex-start); .flex(column, flex-start);
gap: 8px; gap: @spacing-sm;
} }
&__name { &__name {
margin: 0; margin: 0;
font-size: 26px; font-size: @font-size-xl;
font-weight: 400; font-weight: 600;
line-height: 33px; line-height: 1.3;
color: @home-body; color: @home-body;
} }
&__slogan { &__slogan {
margin: 0; margin: 0;
font-size: 14px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
line-height: 18px; line-height: 1.5;
color: @home-body; color: @text-color-secondary;
} }
&__cta { &__cta {

View File

@@ -53,25 +53,25 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.home { .home {
.flex(column, flex-start); .flex(column, flex-start);
gap: 20px; gap: @spacing-lg;
padding-top: 0; padding-top: 0;
padding-bottom: @spacing-xxl; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__dashboard { &__dashboard {
.flex(row, space-between, stretch); .flex(row, space-between, stretch);
gap: 60px; gap: @spacing-lg;
} }
&__dashboard-main { &__dashboard-main {
.flex(row, space-between, center); .flex(row, space-between, center);
flex: 0 0 1091px; flex: 1 1 auto;
width: 1091px;
min-width: 0; min-width: 0;
box-sizing: border-box; box-sizing: border-box;
gap: @spacing-lg; gap: @spacing-lg;
padding: 0 59px 0 105px; padding: @spacing-lg @spacing-xl;
border: 1px solid @home-card-border; border: none;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
} }
} }

View File

@@ -67,70 +67,58 @@ const localAgree = computed({
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿 #36(账号密码登录)纵向坐标:
// 账号标签 457 → 密码标签 585(差 128)
// 密码输入框底 ~648 → 勾选行 641 区间 → 登录按钮文字 688
// 表单左边界与 title 内缩对齐:1032.5 - 982.5(title 层中心对齐后) ≈ 0,
// 实测 title padding-left 17px 抵消 693/714 的差,故表单不再额外内缩。
.login-form { .login-form {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
margin: 26px 0 0; gap: @spacing-lg;
width: 100%; width: 100%;
margin-top: @spacing-xl;
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
// 标签底 474 → 输入框顶 480,间距 6px gap: @spacing-sm;
gap: 6px;
width: 100%; width: 100%;
} }
// 第一个字段底(480+40=520)→ 第二个字段标签顶 585,间距 65px
&__field + &__field {
margin-top: 65px;
}
&__label { &__label {
font-size: 13px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
line-height: 17px; line-height: 1.4;
color: #1f2937; color: @heading-color;
text-align: center; text-align: left;
} }
&__input { &__input {
width: 100%; width: 100%;
height: 40px; height: 44px;
padding: 0 12px; padding: 0 @spacing-base;
border: 1px solid #dde1e7; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @white; background: @bg-color-light;
font-size: 13px; font-size: @font-size-base;
color: #1f2937; color: @heading-color;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s, background 0.2s;
box-sizing: border-box; box-sizing: border-box;
&:focus { &:focus {
border-color: @primary-strong; border-color: @primary-strong;
background: @white;
} }
&::placeholder { &::placeholder {
color: #1f2937; color: @text-color-secondary;
} }
} }
// 设计稿 #36:勾选框 (1042.2,642) 7.5×14,文字 12px 起于 x=1059。
// 注:设计稿勾选标注 y=642 与密码输入框底 648 重叠,二者无法同时满足,
// 这里按视觉取勾选行落在输入框下方 8px。
&__agree { &__agree {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 8px; gap: @spacing-sm;
margin-top: 8px; margin: 0;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; line-height: 1.5;
line-height: 16px; color: @text-color-secondary;
color: #6b7280;
cursor: pointer; cursor: pointer;
user-select: none;
input { input {
flex-shrink: 0; flex-shrink: 0;
@@ -146,17 +134,15 @@ const localAgree = computed({
} }
} }
// 设计稿 #36:按钮文字 (1352,688) 54×20 居中,按钮盒约 693×46
&__submit { &__submit {
width: 100%; width: 100%;
height: 46px; height: 44px;
// 勾选行底 657 → 按钮顶 674 margin: 0;
margin-top: 17px;
border: none; border: none;
border-radius: @radius-sm; border-radius: @radius-base;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 15px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -85,65 +85,58 @@ function onSubmit(): void {
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
// 设计稿 #38(手机验证码登录)纵向坐标:
// 手机号标签 438 → 验证码标签 520(差 82)
// 验证码输入框底 ~588 → 勾选行 622 → 登录按钮文字 669
.sms-login { .sms-login {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
margin: 26px 0 0; gap: @spacing-lg;
width: 100%; width: 100%;
margin-top: @spacing-xl;
&__field { &__field {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 6px; gap: @spacing-sm;
width: 100%; width: 100%;
} }
// 第一个字段底(457+40=497... 按标签 438+17+6+40=501)→ 第二标签 520,间距 19px
&__field + &__field {
margin-top: 19px;
}
&__label { &__label {
font-size: 13px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
line-height: 17px; line-height: 1.4;
color: #1f2937; color: @heading-color;
text-align: center; text-align: left;
} }
&__input { &__input {
width: 100%; width: 100%;
height: 40px; height: 44px;
padding: 0 12px; padding: 0 @spacing-base;
border: 1px solid #dde1e7; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @white; background: @bg-color-light;
font-size: 13px; font-size: @font-size-base;
color: #1f2937; color: @heading-color;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s, background 0.2s;
box-sizing: border-box; box-sizing: border-box;
&:focus { &:focus {
border-color: @primary-strong; border-color: @primary-strong;
background: @white;
} }
&::placeholder { &::placeholder {
color: #1f2937; color: @text-color-secondary;
} }
} }
// 设计稿 #38:勾选框 (992.2,623) 7.5×14,勾选文字 622
&__agree { &__agree {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 8px; gap: @spacing-sm;
margin-top: 14px; margin: 0;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; line-height: 1.5;
line-height: 16px; color: @text-color-secondary;
color: #6b7280;
cursor: pointer; cursor: pointer;
user-select: none;
input { input {
flex-shrink: 0; flex-shrink: 0;
@@ -159,16 +152,15 @@ function onSubmit(): void {
} }
} }
// 设计稿 #38:按钮文字 (1302,669) 54×20
&__submit { &__submit {
width: 100%; width: 100%;
height: 46px; height: 44px;
margin-top: 17px; margin: 0;
border: none; border: none;
border-radius: @radius-sm; border-radius: @radius-base;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 15px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

View File

@@ -144,26 +144,31 @@ async function onGetCode(target: string): Promise<void> {
<style lang="less" scoped> <style lang="less" scoped>
.auth-page { .auth-page {
// 设计稿 #36:两个 tab 顶 406,字号 14px,选中下划线 2px 位于 429。
// tab 文字底 425,下划线顶 429 → padding-bottom 6px。
// 上间距 26px:副标题底 380 → tab 文字顶 406。
&__tabs { &__tabs {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 24px; gap: @spacing-xl;
margin: 26px 0 0; width: 100%;
margin-top: @spacing-xl;
border-bottom: 1px solid @border-color;
} }
&__tab { &__tab {
position: relative; position: relative;
padding: 0 0 6px; padding: 0 0 @spacing-sm;
margin-bottom: -1px;
border: none; border: none;
background: transparent; background: transparent;
font-size: 14px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
color: #4b5563; line-height: 1.4;
color: @text-color-secondary;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
&:hover {
color: @heading-color;
}
&.is-active { &.is-active {
font-weight: 600; font-weight: 600;
color: @primary-strong; color: @primary-strong;
@@ -175,24 +180,23 @@ async function onGetCode(target: string): Promise<void> {
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 2px; height: 2px;
border-radius: 1px;
background: @primary-strong; background: @primary-strong;
} }
} }
} }
// 设计稿 #36:「没有账号?」(1325,774)、「立即注册」(1384.5,774) 12px。
// 「找回密码」为设计稿外的补充入口,按同样式接在分隔符后。
&__switch { &__switch {
align-self: center; align-self: center;
margin: 53px 0 0; margin: @spacing-xl 0 0;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 16px; line-height: 1.5;
color: #6b7280; color: @text-color-secondary;
&-sep { &-sep {
margin: 0 8px; margin: 0 @spacing-sm;
color: #d0d5dd; color: @border-color;
} }
} }
} }

View File

@@ -190,7 +190,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 40px; height: 40px;
padding: 0 12px; padding: 0 12px;
border: 1px solid #dde1e7; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 13px; font-size: 13px;

View File

@@ -191,7 +191,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 40px; height: 40px;
padding: 0 12px; padding: 0 12px;
border: 1px solid #dde1e7; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
font-size: 13px; font-size: 13px;

View File

@@ -7,12 +7,18 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const emit = defineEmits<{ view: [id: string] }>() const emit = defineEmits<{ view: [message: Message] }>()
const emojiMap: Record<MessageType, string> = { const LABEL_MAP: Record<MessageType, string> = {
在线客服: '💬', 在线客服: '客',
状态通知: '📢', 状态通知: '态',
系统通知: '🔔', 系统通知: '系',
}
const TONE_MAP: Record<MessageType, string> = {
在线客服: 'is-service',
状态通知: 'is-status',
系统通知: 'is-system',
} }
</script> </script>
@@ -22,10 +28,17 @@ const emojiMap: Record<MessageType, string> = {
:class="{ 'is-unread': !message.isRead }" :class="{ 'is-unread': !message.isRead }"
role="button" role="button"
tabindex="0" tabindex="0"
@click="emit('view', message.id)" @click="emit('view', message)"
@keydown.enter="emit('view', message.id)" @keydown.enter="emit('view', message)"
> >
<span class="message-item__emoji">{{ emojiMap[message.type] }}</span> <span
class="message-item__icon"
:class="TONE_MAP[message.type]"
aria-hidden="true"
>
{{ LABEL_MAP[message.type] }}
<span v-if="!message.isRead" class="message-item__dot" aria-label="未读" />
</span>
<div class="message-item__body"> <div class="message-item__body">
<div class="message-item__head"> <div class="message-item__head">
<span class="message-item__title">{{ message.title }}</span> <span class="message-item__title">{{ message.title }}</span>
@@ -34,18 +47,17 @@ const emojiMap: Record<MessageType, string> = {
<p class="message-item__summary"> <p class="message-item__summary">
{{ message.summary }} {{ message.summary }}
</p> </p>
<span class="message-item__type">{{ message.type }}</span>
</div> </div>
<span v-if="!message.isRead" class="message-item__dot" aria-label="未读" />
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.message-item { .message-item {
position: relative;
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 16px; gap: @spacing-base;
min-height: 86px; min-height: 88px;
padding: 19px 29px; padding: @spacing-base @spacing-lg;
background: @white; background: @white;
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
@@ -55,20 +67,38 @@ const emojiMap: Record<MessageType, string> = {
background: @bg-color-light; background: @bg-color-light;
} }
&__emoji { &__icon {
position: relative;
flex-shrink: 0; flex-shrink: 0;
.flex(row, center); .flex(row, center);
width: 42px; width: 40px;
height: 42px; height: 40px;
border-radius: @radius-round; border-radius: @radius-round;
background: @message-item-icon; background: @primary-light;
font-size: 20px; color: @primary-strong;
font-size: @font-size-sm;
font-weight: 600;
line-height: 1; line-height: 1;
&.is-service {
background: @primary-light;
color: @primary-strong;
}
&.is-status {
background: fade(@success-color, 12%);
color: @success-color;
}
&.is-system {
background: fade(@warning-color, 14%);
color: @warning-color;
}
} }
&__body { &__body {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 8px; gap: @spacing-xs;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
@@ -80,33 +110,44 @@ const emojiMap: Record<MessageType, string> = {
} }
&__title { &__title {
font-size: 14px; font-size: @font-size-base;
font-weight: 600; font-weight: 500;
line-height: 20px; line-height: 1.4;
color: @finance-ink; color: @heading-color;
.ellipsis(); .ellipsis();
} }
&__time { &__time {
flex-shrink: 0; flex-shrink: 0;
font-size: 12px; font-size: @font-size-xs;
line-height: 20px; line-height: 1.4;
color: @message-tabs-text; color: @text-color-secondary;
} }
&__summary { &__summary {
font-size: 12px; margin: 0;
line-height: 18px; font-size: @font-size-sm;
color: @message-tabs-text; line-height: 1.5;
color: @text-color-secondary;
.ellipsis(); .ellipsis();
} }
&__type {
width: fit-content;
padding: 0 @spacing-sm;
border-radius: @radius-round;
background: @bg-color-light;
color: @text-color-secondary;
font-size: @font-size-xs;
line-height: 20px;
}
&__dot { &__dot {
position: absolute; position: absolute;
top: 21px; top: 0;
left: 60px; right: 0;
width: 14px; width: 8px;
height: 14px; height: 8px;
border: 2px solid @white; border: 2px solid @white;
border-radius: @radius-round; border-radius: @radius-round;
background: @message-tabs-badge; background: @message-tabs-badge;

View File

@@ -8,7 +8,7 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const emit = defineEmits<{ view: [id: string] }>() const emit = defineEmits<{ view: [message: Message] }>()
</script> </script>
<template> <template>
@@ -31,20 +31,21 @@ const emit = defineEmits<{ view: [id: string] }>()
.message-list { .message-list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 0;
margin-top: 16px; margin-top: 0;
padding: 0; padding: 0;
border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
overflow: hidden; overflow: hidden;
.message-item + .message-item { :deep(.message-item + .message-item) {
border-top: 1px solid @finance-card-line; border-top: 1px solid @border-color;
} }
&__empty { &__empty {
padding: 60px 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;
font-size: 13px; font-size: @font-size-sm;
} }
} }
</style> </style>

View File

@@ -41,9 +41,10 @@ function countOf(value: MessageType | 'all', counts: MessageTabCounts): number {
<style lang="less" scoped> <style lang="less" scoped>
.message-tabs { .message-tabs {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 2px;
height: 438px; min-height: 0;
padding: 14px 0; padding: @spacing-sm 0;
border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
@@ -51,22 +52,24 @@ function countOf(value: MessageType | 'all', counts: MessageTabCounts): number {
&__tab { &__tab {
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-sm; gap: @spacing-sm;
height: 44px; height: 40px;
padding: 0 25px; margin: 0 @spacing-sm;
border-radius: 0; padding: 0 @spacing-base;
border-radius: @radius-sm;
background: transparent; background: transparent;
color: @message-tabs-text; color: @message-tabs-text;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 18px; line-height: 1.4;
transition: color 0.2s; transition: color 0.2s, background-color 0.2s;
&:hover { &:hover {
color: @primary-strong; color: @primary-strong;
background: @bg-color-light;
} }
&.is-active { &.is-active {
background: transparent; background: @filter-selected-bg;
color: @primary-strong; color: @primary-strong;
font-weight: 600; font-weight: 600;
} }
@@ -74,17 +77,22 @@ function countOf(value: MessageType | 'all', counts: MessageTabCounts): number {
&__count { &__count {
flex-shrink: 0; flex-shrink: 0;
min-width: 15px; min-width: 18px;
height: 15px; height: 18px;
padding: 0 4px; padding: 0 @spacing-xs;
border-radius: @radius-round; border-radius: @radius-round;
background: @message-tabs-badge; background: @bg-color-light;
color: @white; color: @text-color-secondary;
font-size: 10px; font-size: @font-size-xs;
font-weight: 400; font-weight: 500;
line-height: 15px; line-height: 18px;
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
} }
&__tab.is-active &__count {
background: fade(@primary-strong, 12%);
color: @primary-strong;
}
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<!-- 页面:消息详情(MessageDetail) --> <!-- 页面:消息详情(MessageDetail) -->
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, watch } from 'vue' import { computed, onMounted, watch } from 'vue'
import { ElButton, ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { PageHead } from '@/components' import { PageHead } from '@/components'
import { useMessageStore } from '@/stores/modules/message' import { useMessageStore } from '@/stores/modules/message'
@@ -42,7 +42,7 @@ onMounted(load)
slogan="查看站内消息完整内容与附件资料" slogan="查看站内消息完整内容与附件资料"
/> />
<div class="message-detail__wrap"> <div class="container message-detail__wrap">
<nav class="message-detail__breadcrumb"> <nav class="message-detail__breadcrumb">
<RouterLink to="/message" class="message-detail__breadcrumb-link"> <RouterLink to="/message" class="message-detail__breadcrumb-link">
消息中心 消息中心
@@ -53,12 +53,15 @@ onMounted(load)
<template v-if="item"> <template v-if="item">
<article class="message-detail__card"> <article class="message-detail__card">
<h1 class="message-detail__title"> <header class="message-detail__head">
{{ item.title }} <h1 class="message-detail__title">
</h1> {{ item.title }}
<p class="message-detail__time"> </h1>
发布时间:{{ item.publishTime }} <p class="message-detail__time">
</p> 发布时间:{{ item.publishTime }}
</p>
</header>
<div class="message-detail__content"> <div class="message-detail__content">
{{ item.content }} {{ item.content }}
</div> </div>
@@ -80,32 +83,33 @@ onMounted(load)
</span> </span>
</div> </div>
<div class="message-detail__file-actions"> <div class="message-detail__file-actions">
<el-button <button
type="button"
class="message-detail__file-btn" class="message-detail__file-btn"
link
@click="onPreview" @click="onPreview"
> >
预览 预览
</el-button> </button>
<el-button <button
type="button"
class="message-detail__file-btn" class="message-detail__file-btn"
link
@click="onDownload" @click="onDownload"
> >
下载 下载
</el-button> </button>
</div> </div>
</li> </li>
</ul> </ul>
</section> </section>
<div class="message-detail__actions"> <div class="message-detail__actions">
<el-button <button
type="button"
class="message-detail__back" class="message-detail__back"
@click="onBack" @click="onBack"
> >
返回 返回
</el-button> </button>
</div> </div>
</article> </article>
</template> </template>
@@ -122,167 +126,181 @@ onMounted(load)
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 0;
width: 100%; width: 100%;
padding-bottom: 60px; min-height: 100%;
background: @message-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__wrap { &__wrap {
width: 100%; padding-top: @spacing-lg;
padding: 28px 70px 0; padding-right: @home-gutter;
padding-left: @home-gutter;
box-sizing: border-box; box-sizing: border-box;
@media (max-width: 1600px) {
padding-right: @spacing-lg;
padding-left: @spacing-lg;
}
} }
&__breadcrumb { &__breadcrumb {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 4px; gap: @spacing-xs;
height: 49px; margin-bottom: @spacing-base;
font-size: 12px; font-size: @font-size-sm;
line-height: 17px; line-height: 1.5;
color: @project-muted; color: @text-color-secondary;
} }
&__breadcrumb-link { &__breadcrumb-link {
color: @project-muted; color: @text-color-secondary;
&:hover { &:hover {
color: @primary-strong; color: @primary-strong;
} }
} }
&__breadcrumb-sep {
color: @project-muted;
}
&__breadcrumb-current { &__breadcrumb-current {
color: @message-tabs-text; color: @heading-color;
font-weight: 500; font-weight: 500;
} }
&__card { &__card {
padding: 45px 47px 40px; padding: @spacing-xl;
border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }
&__head {
padding-bottom: @spacing-base;
border-bottom: 1px solid @border-color;
}
&__title { &__title {
font-size: 20px; margin: 0;
font-weight: 700; font-size: @font-size-xl;
line-height: 28px; font-weight: 600;
color: @finance-ink; line-height: 1.4;
text-align: center; color: @heading-color;
} }
&__time { &__time {
margin-top: 14px; margin: @spacing-sm 0 0;
font-size: 12px; font-size: @font-size-sm;
line-height: 18px; line-height: 1.4;
color: @project-muted; color: @text-color-secondary;
text-align: center;
} }
&__content { &__content {
margin-top: 16px; margin-top: @spacing-lg;
background: transparent; color: @text-color;
color: @message-tabs-text; font-size: @font-size-base;
font-size: 13px; line-height: 1.8;
line-height: 2.8; text-align: left;
text-align: center;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
&__attachments { &__attachments {
margin-top: 24px; margin-top: @spacing-xl;
}
&-title { &__attachments-title {
margin-bottom: 10px; margin: 0 0 @spacing-base;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 600;
color: @project-muted; color: @heading-color;
}
} }
&__file-list { &__file-list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 10px; gap: @spacing-sm;
margin: 0;
padding: 0;
} }
&__file { &__file {
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
width: 254px; width: 100%;
max-width: 100%; max-width: 520px;
padding: 15px 12px 15px 19px; padding: @spacing-base;
border-radius: @radius-sm; border: 1px solid @border-color;
background: @finance-row-bg; border-radius: @radius-base;
background: @bg-color-light;
list-style: none; list-style: none;
box-sizing: border-box; box-sizing: border-box;
} }
&__file-info { &__file-info {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 4px; gap: @spacing-xs;
min-width: 0; min-width: 0;
} }
&__file-name { &__file-name {
font-size: 12px; font-size: @font-size-sm;
font-weight: 700; font-weight: 600;
color: @finance-ink; color: @heading-color;
.ellipsis(); .ellipsis();
} }
&__file-meta { &__file-meta {
font-size: 11px; font-size: @font-size-xs;
color: @project-muted; color: @text-color-secondary;
.ellipsis(); .ellipsis();
} }
&__file-actions { &__file-actions {
.flex(column, center, center); .flex(row, flex-end, center);
gap: 2px; gap: @spacing-sm;
flex-shrink: 0; flex-shrink: 0;
} }
&__file-btn { &__file-btn {
padding: 0; height: 28px;
font-size: 12px; padding: 0 @spacing-sm;
font-weight: 400; border: none;
color: @message-tabs-text; border-radius: @radius-round;
background: transparent;
color: @primary-strong;
font-size: @font-size-sm;
cursor: pointer;
&:hover { &:hover {
color: @primary-strong; background: fade(@primary-strong, 8%);
} }
} }
&__actions { &__actions {
.flex(row, flex-end); .flex(row, flex-end);
margin-top: 40px; margin-top: @spacing-xl;
} }
&__back { &__back {
width: 94px; min-width: 88px;
height: 34px; height: 36px;
padding: 0; padding: 0 @spacing-lg;
border-color: @primary-strong; border: none;
border-radius: @radius-sm; border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 13px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
cursor: pointer;
transition: background 0.2s;
&:hover { &:hover {
border-color: @primary-strong; background: @primary-hover;
background: @primary-strong;
color: @white;
} }
} }
&__empty { &__empty {
padding: 80px 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;
font-size: 13px; font-size: @font-size-sm;
color: @project-muted; color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -6,7 +6,7 @@ import { useRouter } from 'vue-router'
import { ElButton, ElInput } from 'element-plus' import { ElButton, ElInput } from 'element-plus'
import { PageHead, PaginationBar } from '@/components' import { PageHead, PaginationBar } from '@/components'
import { useMessageStore } from '@/stores/modules/message' import { useMessageStore } from '@/stores/modules/message'
import type { MessageType } from '@/types/business/message' import type { Message, MessageType } from '@/types/business/message'
import MessageTabs from './cpns/MessageTabs.vue' import MessageTabs from './cpns/MessageTabs.vue'
import MessageList from './cpns/MessageList.vue' import MessageList from './cpns/MessageList.vue'
@@ -35,8 +35,13 @@ function onTabChange(type: MessageType | 'all'): void {
void messageStore.search() void messageStore.search()
} }
function onView(id: string): void { function onView(message: Message): void {
void router.push(`/message/${id}`) // 用 message.type 区分:在线客服进会话页,其余进消息详情
if (message.type === '在线客服') {
void router.push('/service')
return
}
void router.push(`/message/${message.id}`)
} }
function onMarkAllRead(): void { function onMarkAllRead(): void {
@@ -61,7 +66,7 @@ const UNREAD_LABEL = computed(() => (unreadCount.value > 0 ? `全部已读 (${un
slogan="统一接收项目进度、售后工单、账单待办等站内消息,重要业务动态实时提醒" slogan="统一接收项目进度、售后工单、账单待办等站内消息,重要业务动态实时提醒"
/> />
<div class="message__body"> <div class="container message__body">
<MessageTabs <MessageTabs
class="message__tabs" class="message__tabs"
:active="query.type" :active="query.type"
@@ -120,55 +125,63 @@ const UNREAD_LABEL = computed(() => (unreadCount.value > 0 ? `全部已读 (${un
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 0;
width: 100%; width: 100%;
padding-bottom: 14px; min-height: 100%;
background: @message-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__body { &__body {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 22px; gap: @spacing-lg;
width: 100%; width: 100%;
padding: 119px 25px 0; padding-top: @spacing-lg;
padding-right: @home-gutter;
padding-left: @home-gutter;
box-sizing: border-box; box-sizing: border-box;
@media (max-width: 1600px) {
padding-right: @spacing-lg;
padding-left: @spacing-lg;
}
} }
&__tabs { &__tabs {
flex-shrink: 0; flex-shrink: 0;
width: 198px; width: 200px;
} }
&__main { &__main {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: @spacing-base;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
&__toolbar { &__toolbar {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 0; gap: @spacing-base;
height: 69px; min-height: 64px;
padding: 0 21px 0 25px; padding: 0 @spacing-lg;
border: 1px solid @message-toolbar-btn-border; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }
&__search { &__search {
flex: 0 1 460px; flex: 0 1 360px;
height: 37px; height: 36px;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
height: 37px; height: 36px;
padding: 0 16px; padding: 0 @spacing-base;
border-radius: @radius-base; border-radius: @radius-round;
background: @finance-field-bg; background: @finance-field-bg;
box-shadow: none; box-shadow: 0 0 0 1px @border-color inset;
} }
:deep(.el-input__inner) { :deep(.el-input__inner) {
color: @finance-ink; color: @finance-ink;
font-size: 13px; font-size: @font-size-base;
&::placeholder { &::placeholder {
color: @contract-placeholder; color: @contract-placeholder;
@@ -177,11 +190,11 @@ const UNREAD_LABEL = computed(() => (unreadCount.value > 0 ? `全部已读 (${un
} }
&__btn { &__btn {
height: 28px; height: 36px;
margin-left: 14px; margin-left: 0;
padding: 0 15px; padding: 0 @spacing-lg;
border-radius: @radius-sm; border-radius: @radius-round;
font-size: 12px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
&:hover { &:hover {
@@ -195,15 +208,15 @@ const UNREAD_LABEL = computed(() => (unreadCount.value > 0 ? `全部已读 (${un
color: @white; color: @white;
&:hover { &:hover {
border-color: @primary-strong; border-color: @primary-hover;
background: @primary-strong; background: @primary-hover;
color: @white; color: @white;
} }
} }
&--read { &--read {
margin-left: auto; margin-left: auto;
border-color: @message-toolbar-btn-border; border-color: @border-color;
background: @white; background: @white;
color: @finance-entry-text; color: @finance-entry-text;
@@ -218,22 +231,22 @@ const UNREAD_LABEL = computed(() => (unreadCount.value > 0 ? `全部已读 (${un
&__pagination { &__pagination {
justify-content: flex-end; justify-content: flex-end;
align-self: stretch; align-self: stretch;
margin-top: 16px; margin-top: @spacing-base;
padding: 0; padding: 0;
:deep(.pagination__btn) { :deep(.pagination__btn) {
min-width: 23px; min-width: 28px;
height: 26px; height: 28px;
border-color: @message-toolbar-btn-border; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
color: @finance-entry-text; color: @text-color;
font-size: 12px; font-size: @font-size-sm;
} }
:deep(.pagination__btn.is-active) { :deep(.pagination__btn.is-active) {
border-color: @message-pager-active-bg; border-color: @primary-strong;
background: @message-pager-active-bg; background: @primary-light;
color: @primary-strong; color: @primary-strong;
font-weight: 500; font-weight: 500;
} }

View File

@@ -78,7 +78,7 @@ onMounted(() => {
class="address-row" class="address-row"
> >
<span v-if="address.isDefault" class="address-row__tag">默认</span> <span v-if="address.isDefault" class="address-row__tag">默认</span>
<div class="address-row__col address-row__col--name"> <div class="address-row__col">
<p class="address-row__label">收货人姓名</p> <p class="address-row__label">收货人姓名</p>
<EllipsisText class="address-row__value" :text="address.receiverName" /> <EllipsisText class="address-row__value" :text="address.receiverName" />
</div> </div>
@@ -90,7 +90,7 @@ onMounted(() => {
<p class="address-row__label">所在地区</p> <p class="address-row__label">所在地区</p>
<EllipsisText class="address-row__value" :text="address.region" /> <EllipsisText class="address-row__value" :text="address.region" />
</div> </div>
<div class="address-row__col address-row__col--detail"> <div class="address-row__col">
<p class="address-row__label">详细地址</p> <p class="address-row__label">详细地址</p>
<EllipsisText class="address-row__value" :text="address.detailAddress" /> <EllipsisText class="address-row__value" :text="address.detailAddress" />
</div> </div>
@@ -144,8 +144,9 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.address-page { .address-page {
.flex(column, flex-start); .flex(column, flex-start);
gap: 20px; gap: @spacing-lg;
width: 100%; width: 100%;
min-width: 0;
&__head { &__head {
.flex(row, space-between, center); .flex(row, space-between, center);
@@ -158,7 +159,7 @@ onMounted(() => {
font-size: @font-size-base; font-size: @font-size-base;
font-weight: 600; font-weight: 600;
color: @profile-ink; color: @profile-ink;
line-height: 20px; line-height: 1.4;
&::before { &::before {
content: ''; content: '';
@@ -174,7 +175,7 @@ onMounted(() => {
&__create { &__create {
height: 32px; height: 32px;
padding: 0 14px; padding: 0 @spacing-base;
border: none; border: none;
border-radius: @profile-radius; border-radius: @profile-radius;
background: @primary-strong; background: @primary-strong;
@@ -191,15 +192,16 @@ onMounted(() => {
&__list { &__list {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 15px; gap: @spacing-base;
width: 100%; width: 100%;
min-width: 0;
} }
&__empty { &__empty {
padding: @spacing-xxl 0; padding: @spacing-xxl 0;
width: 100%; width: 100%;
text-align: center; text-align: center;
font-size: 13px; font-size: @font-size-sm;
} }
:deep(.pagination) { :deep(.pagination) {
@@ -216,69 +218,70 @@ onMounted(() => {
.address-row { .address-row {
position: relative; position: relative;
.flex(row, flex-start, center); display: grid;
min-height: 78px; grid-template-columns:
padding: 14px 24px 14px 52px; minmax(88px, 0.9fr)
border: 1px solid @profile-card-border; minmax(110px, 1fr)
border-radius: @radius-lg; minmax(120px, 1.2fr)
background: @white; minmax(160px, 1.8fr)
auto;
align-items: center;
gap: @spacing-base;
min-height: 72px;
padding: @spacing-base @spacing-lg;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
box-sizing: border-box; box-sizing: border-box;
&__tag { &__tag {
position: absolute; position: absolute;
top: -9px; top: -9px;
left: 28px; left: @spacing-lg;
height: 18px; height: 18px;
padding: 0 10px; padding: 0 10px;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 11px; font-size: @font-size-xs;
font-weight: 500; font-weight: 500;
line-height: 18px; line-height: 18px;
} }
&__col { &__col {
flex: 0 0 405px;
min-width: 0; min-width: 0;
padding-right: 24px;
box-sizing: border-box;
&--detail {
flex: 1 1 auto;
}
} }
&__label { &__label {
margin: 0;
font-size: @font-size-sm; font-size: @font-size-sm;
color: @finance-axis-text; color: @text-color-secondary;
line-height: 16px; line-height: 1.4;
} }
&__value { &__value {
margin-top: 6px; margin-top: @spacing-xs;
font-size: @font-size-base; font-size: @font-size-base;
font-weight: 600; font-weight: 600;
color: @profile-ink; color: @profile-ink;
line-height: 20px; line-height: 1.4;
.ellipsis();
} }
&__actions { &__actions {
.flex(row, flex-start, center); .flex(row, flex-end, center);
flex-shrink: 0; flex-wrap: wrap;
gap: 12px; gap: @spacing-sm;
margin-left: 16px;
} }
&__btn { &__btn {
height: 28px; height: 28px;
padding: 0 14px; padding: 0 @spacing-sm;
border: 1px solid @profile-card-border; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-round;
background: @white; background: @white;
color: @finance-entry-text; color: @text-color;
font-size: @font-size-sm; font-size: @font-size-sm;
white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;

View File

@@ -71,13 +71,13 @@ const contacts = [
&__section { &__section {
& + & { & + & {
margin-top: 34px; margin-top: @spacing-lg;
} }
} }
&__section-title { &__section-title {
font-size: 14px; font-size: @font-size-base;
font-weight: 700; font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 20px; line-height: 20px;
text-align: center; text-align: center;

View File

@@ -148,10 +148,10 @@ function onSave(): void {
&__title { &__title {
position: relative; position: relative;
padding-left: 12px; padding-left: 12px;
font-size: 12px; font-size: @font-size-base;
font-weight: 700; font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 20px; line-height: 1.4;
&::before { &::before {
content: ''; content: '';
@@ -172,7 +172,7 @@ function onSave(): void {
} }
&__label { &__label {
font-size: 11px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
color: @heading-color; color: @heading-color;
} }
@@ -180,11 +180,11 @@ function onSave(): void {
&__input, &__input,
&__textarea { &__textarea {
width: 100%; width: 100%;
padding: 0 12px; padding: 0 @spacing-base;
border: 1px solid @profile-input-border; border: 1px solid @profile-input-border;
border-radius: @profile-radius; border-radius: @profile-radius;
background: @white; background: @white;
font-size: 11px; font-size: @font-size-sm;
color: @text-color; color: @text-color;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
@@ -195,7 +195,7 @@ function onSave(): void {
} }
&::placeholder { &::placeholder {
color: @heading-color; color: @text-color-secondary;
} }
} }
@@ -218,7 +218,7 @@ function onSave(): void {
.app-button { .app-button {
flex: 1; flex: 1;
height: 36px; height: 36px;
font-size: 12px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
border-radius: @profile-radius; border-radius: @profile-radius;
} }

View File

@@ -235,10 +235,10 @@ function onSubmit(): void {
} }
&__attach-title { &__attach-title {
font-size: 16px; font-size: @font-size-base;
font-weight: 700; font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 24px; line-height: 1.4;
} }
&__attach-hint { &__attach-hint {

View File

@@ -1,36 +1,50 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HelpFaqGroup } from '@/types/business/profile' import { computed } from 'vue'
import type { HelpFaqGroup, HelpFaqItem } from '@/types/business/profile'
interface Props { interface Props {
groups: HelpFaqGroup[] groups: HelpFaqGroup[]
} }
defineProps<Props>() const props = defineProps<Props>()
const questions = computed<HelpFaqItem[]>(() =>
props.groups.flatMap((group) => group.items),
)
</script> </script>
<template> <template>
<div class="help-faq"> <div class="help-faq">
<section <aside class="help-faq__aside">
v-for="(group, gi) in groups" <p
:key="group.title" v-for="item in questions"
class="help-faq__group"
>
<h3 class="help-faq__group-title">
{{ gi + 1 }}、{{ group.title }}
</h3>
<div
v-for="item in group.items"
:key="item.q" :key="item.q"
class="help-faq__item" class="help-faq__q"
> >
<p class="help-faq__q"> Q: {{ item.q }}
Q: {{ item.q }} </p>
</p> </aside>
<p class="help-faq__a text-secondary">
{{ item.a }} <div class="help-faq__main">
</p> <div class="help-faq__main-inner">
<section
v-for="(group, gi) in groups"
:key="group.title"
class="help-faq__group"
>
<h3 class="help-faq__group-title">
{{ gi + 1 }}、{{ group.title }}
</h3>
<p
v-for="item in group.items"
:key="`${group.title}-${item.q}`"
class="help-faq__a"
>
{{ item.a }}
</p>
</section>
</div> </div>
</section> </div>
<p v-if="groups.length === 0" class="help-faq__empty text-secondary"> <p v-if="groups.length === 0" class="help-faq__empty text-secondary">
暂无常见问题 暂无常见问题
@@ -40,44 +54,79 @@ defineProps<Props>()
<style lang="less" scoped> <style lang="less" scoped>
.help-faq { .help-faq {
.flex(column, flex-start); display: grid;
grid-template-columns: max-content 1fr;
column-gap: @spacing-xxl;
align-items: start;
width: 100%; width: 100%;
min-width: 0;
&__aside {
.flex(column, flex-start, flex-start);
gap: @spacing-xl;
text-align: left;
}
&__q {
margin: 0;
font-size: @font-size-base;
font-weight: 600;
color: @heading-color;
line-height: 1.6;
text-align: left;
white-space: nowrap;
}
&__main {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
&__main-inner {
width: 100%;
max-width: 480px;
margin: 0 auto;
text-align: center;
}
&__group { &__group {
.flex(column, flex-start, center);
gap: @spacing-base;
width: 100%;
& + & { & + & {
margin-top: 42px; margin-top: @spacing-xxl;
} }
} }
&__group-title { &__group-title {
font-size: 14px; margin: 0;
font-weight: 700; width: 100%;
font-size: @font-size-base;
font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 20px; line-height: 1.4;
text-align: center; text-align: center;
} }
&__item {
margin-top: 24px;
}
&__q {
font-size: 14px;
font-weight: 700;
color: @heading-color;
line-height: 20px;
}
&__a { &__a {
margin: 8px 0 0 184px; margin: 0;
font-size: 13px; width: 100%;
font-size: @font-size-sm;
line-height: 1.8; line-height: 1.8;
color: @text-color-secondary;
text-align: center;
} }
&__empty { &__empty {
grid-column: 1 / -1;
padding: @spacing-xxl 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;
font-size: 13px; font-size: @font-size-sm;
} }
} }
</style> </style>

View File

@@ -29,11 +29,11 @@ const WORK_TIME = '工作日 8:00-12:00 14:00-18:00'
// 设计稿 #01:浅蓝横幅卡,蓝圆 + 标题/电话/时间三行左对齐 // 设计稿 #01:浅蓝横幅卡,蓝圆 + 标题/电话/时间三行左对齐
&__card { &__card {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 32px; gap: @spacing-lg;
width: 100%; width: 100%;
padding: 30px 32px; padding: @spacing-lg;
border-radius: @radius-base; border-radius: @radius-base;
background: @profile-hotline-bg; background: @filter-selected-bg;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -50,25 +50,25 @@ const WORK_TIME = '工作日 8:00-12:00 14:00-18:00'
} }
&__title { &__title {
font-size: 15px; font-size: @font-size-base;
font-weight: 700; font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 22px; line-height: 1.4;
} }
&__number { &__number {
margin-top: 6px; margin-top: @spacing-sm;
font-size: 24px; font-size: @font-size-xl;
font-weight: 700; font-weight: 600;
color: @primary-strong; color: @primary-strong;
line-height: 32px; line-height: 1.3;
} }
&__time { &__time {
margin-top: 6px; margin-top: @spacing-xs;
font-size: 12px; font-size: @font-size-sm;
color: @text-color-secondary; color: @text-color-secondary;
line-height: 17px; line-height: 1.5;
} }
} }
</style> </style>

View File

@@ -32,24 +32,29 @@ const menus: MenuItem[] = [
<style lang="less" scoped> <style lang="less" scoped>
.profile-sider { .profile-sider {
.flex(column, flex-start); .flex(column, flex-start);
gap: 0; gap: 2px;
padding: @spacing-sm 0;
border-radius: @radius-base;
background: @white;
&__item { &__item {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 36px; height: 40px;
padding: 0 16px; margin: 0 @spacing-sm;
border-radius: @profile-radius; padding: 0 @spacing-base;
border-radius: @radius-sm;
color: @text-color; color: @text-color;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
color: @primary-strong; color: @primary-strong;
background: @bg-color-light;
} }
&.router-link-active { &.router-link-active {
background: @profile-nav-active-bg; background: @filter-selected-bg;
color: @primary-strong; color: @primary-strong;
font-weight: 500; font-weight: 500;
} }

View File

@@ -74,46 +74,41 @@ const STATUS_CLASS: Record<QualificationStatus, string> = {
<style lang="less" scoped> <style lang="less" scoped>
.qual-row { .qual-row {
position: relative; position: relative;
.flex(row, flex-start, center); display: grid;
min-height: 78px; grid-template-columns:
padding: 14px 24px 14px 52px; minmax(72px, 0.9fr)
border: 1px solid @profile-card-border; minmax(100px, 1.2fr)
border-radius: @radius-lg; minmax(100px, 1.1fr)
background: @white; minmax(100px, 1.1fr)
minmax(140px, 1.4fr)
minmax(64px, 0.7fr)
auto;
align-items: center;
gap: @spacing-base;
min-height: 72px;
padding: @spacing-base @spacing-lg;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
box-sizing: border-box; box-sizing: border-box;
&__col { &__col {
flex: 0 0 200px;
min-width: 0; min-width: 0;
padding-right: 20px;
box-sizing: border-box;
&--name {
flex: 0 0 270px;
}
&--valid {
flex: 0 0 250px;
}
&--status {
flex: 0 0 100px;
}
} }
&__label { &__label {
margin: 0;
font-size: @font-size-sm; font-size: @font-size-sm;
color: @finance-axis-text; color: @text-color-secondary;
line-height: 16px; line-height: 1.4;
} }
&__value { &__value {
margin-top: 6px; margin-top: @spacing-xs;
font-size: @font-size-base; font-size: @font-size-base;
font-weight: 600; font-weight: 600;
color: @profile-ink; color: @profile-ink;
line-height: 20px; line-height: 1.4;
.ellipsis();
} }
&__status { &__status {
@@ -131,20 +126,20 @@ const STATUS_CLASS: Record<QualificationStatus, string> = {
} }
&__actions { &__actions {
.flex(row, flex-start, center); .flex(row, flex-end, center);
flex-shrink: 0; flex-wrap: wrap;
gap: 12px; gap: @spacing-sm;
margin-left: auto;
} }
&__btn { &__btn {
height: 28px; height: 28px;
padding: 0 14px; padding: 0 @spacing-sm;
border: 1px solid @profile-card-border; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-round;
background: @white; background: @white;
color: @finance-entry-text; color: @text-color;
font-size: @font-size-sm; font-size: @font-size-sm;
white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;

View File

@@ -121,12 +121,12 @@ function goDeleteAccount(): void {
&__title { &__title {
position: relative; position: relative;
padding-left: 14px; padding-left: @spacing-base;
margin-bottom: 22px; margin-bottom: @spacing-base;
font-size: @font-size-base; font-size: @font-size-base;
font-weight: 600; font-weight: 600;
color: @profile-ink; color: @profile-ink;
line-height: 20px; line-height: 1.4;
&::before { &::before {
content: ''; content: '';
@@ -142,22 +142,23 @@ function goDeleteAccount(): void {
&__row { &__row {
.flex(row, flex-start, center); .flex(row, flex-start, center);
height: 85px; min-height: 72px;
padding: 0 48px 0 52px; padding: 0 @spacing-lg;
border: 1px solid @profile-line; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @bg-color-light;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s; transition: border-color 0.2s, background-color 0.2s;
&:hover { &:hover {
border-color: @profile-nav-active-bg; border-color: @primary-color;
background: @filter-selected-bg;
} }
} }
&__row + &__row { &__row + &__row {
margin-top: 16px; margin-top: @spacing-base;
} }
&__icon { &__icon {
@@ -227,10 +228,10 @@ function goDeleteAccount(): void {
&__delete { &__delete {
width: 100%; width: 100%;
height: 48px; height: 40px;
margin-top: 108px; margin-top: @spacing-xxl;
border: 1px solid @profile-line; border: 1px solid @border-color;
border-radius: @profile-radius; border-radius: @radius-round;
background: @white; background: @white;
color: @finance-axis-text; color: @finance-axis-text;
font-size: @font-size-base; font-size: @font-size-base;

View File

@@ -253,7 +253,7 @@ function onDeleteAccount(): void {
border: 2px solid @danger-color; border: 2px solid @danger-color;
border-radius: @radius-round; border-radius: @radius-round;
color: @danger-color; color: @danger-color;
font-size: 26px; font-size: @font-size-xl;
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
line-height: 1; line-height: 1;

View File

@@ -95,19 +95,21 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.favorites { .favorites {
padding-bottom: 78px; min-height: 100%;
padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__body { &__body {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 20px; gap: @spacing-lg;
padding-top: 20px; padding-top: @spacing-lg;
} }
&__main { &__main {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 24px; gap: @spacing-lg;
} }
&__pager { &__pager {

View File

@@ -37,11 +37,11 @@ onMounted(() => {
&__title { &__title {
position: relative; position: relative;
padding-left: 12px; padding-left: 14px;
font-size: 14px; font-size: @font-size-base;
font-weight: 700; font-weight: 600;
color: @heading-color; color: @profile-ink;
line-height: 20px; line-height: 1.4;
&::before { &::before {
content: ''; content: '';

View File

@@ -36,32 +36,34 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.profile { .profile {
min-height: 737px; min-height: 100%;
background: @profile-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__container { &__container {
padding: 24px 16px 0 10px; padding-top: @spacing-lg;
} }
&__body { &__body {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
gap: @spacing-lg;
width: 100%; width: 100%;
} }
&__aside { &__aside {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 16px; gap: @spacing-base;
flex: 0 0 156px; flex: 0 0 200px;
width: 156px; width: 200px;
} }
&__main { &__main {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
min-height: 534px; min-height: 480px;
margin-left: 10px; margin-left: 0;
padding: 26px 28px 22px; padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -153,13 +153,18 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="container after-sales"> <div class="after-sales">
<div class="container after-sales__inner">
<nav class="after-sales__breadcrumb"> <nav class="after-sales__breadcrumb">
<RouterLink to="/project" class="link"> <RouterLink to="/project" class="link">
项目中心 项目中心
</RouterLink> </RouterLink>
<span> &gt; </span> <span> &gt; </span>
<span>项目详情</span> <RouterLink :to="`/project/${projectId}`" class="link">
项目详情
</RouterLink>
<span> &gt; </span>
<span>申请售后</span>
</nav> </nav>
<header class="after-sales__header"> <header class="after-sales__header">
@@ -300,51 +305,55 @@ onMounted(() => {
</AppButton> </AppButton>
</div> </div>
</footer> </footer>
</div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.after-sales { .after-sales {
.flex(column, flex-start, stretch); min-height: 100%;
padding-bottom: @spacing-page-bottom; padding-bottom: @spacing-page-bottom;
background: @white; background: @bg-color;
&__inner {
.flex(column, flex-start, stretch);
}
// 设计稿 el40 面包屑 y=116,header 底 72 → padding-top 44px。
&__breadcrumb { &__breadcrumb {
padding-top: 44px; padding-top: @spacing-lg;
font-size: 24px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 27px; line-height: 1.5;
color: @project-breadcrumb; color: @project-breadcrumb;
} }
&__header { &__header {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
padding-top: 52px; padding-top: @spacing-base;
} }
&__title { &__title {
margin: 0; margin: 0;
font-size: 22px; font-size: @font-size-title;
font-weight: 700; font-weight: 600;
line-height: 29px; line-height: 1.3;
color: @project-ink; color: @project-ink;
} }
// 设计稿 #26:副标题文字 x=728~1185,中心 ≈ 957 ≈ 内容区中心 960(居中)。
&__subtitle { &__subtitle {
margin-top: 8px; margin-top: @spacing-sm;
font-size: 13px; font-size: @font-size-base;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.5;
color: @project-label; color: @project-label;
text-align: center; text-align: left;
} }
&__card { &__card {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
margin-top: 30px; margin-top: @spacing-base;
padding: 22px 0 40px; padding: @spacing-lg;
border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -237,54 +237,54 @@ function onClearChips(): void {
<style lang="less" scoped> <style lang="less" scoped>
.project-filter { .project-filter {
flex-shrink: 0; flex-shrink: 0;
width: 516px; width: @project-filter-width;
// 字体/尺寸/间距/面板宽度对齐案例库 CasesFilter(老大要求),颜色保持不变。 padding: @spacing-lg 0 @spacing-xl;
padding: 15px 0 40px;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
&__title, &__title,
&__group, &__group,
&__actions { &__actions {
padding: 0 50px 0 35px; padding: 0 @spacing-lg;
} }
&__title { &__title {
font-size: 28px; font-size: @font-size-xl;
font-weight: 500; font-weight: 600;
line-height: 41px; line-height: 1.4;
color: @heading-color; color: @heading-color;
} }
&__group { &__group {
margin-top: 24px; margin-top: @spacing-lg;
} }
&__title + &__group { &__title + &__group {
margin-top: 11px; margin-top: @spacing-base;
} }
&__group-title { &__group-title {
font-size: 24px; font-size: @font-size-md;
font-weight: 500; font-weight: 500;
line-height: 35px; line-height: 1.4;
color: @heading-color; color: @heading-color;
} }
&__list { &__list {
margin-top: 19px; margin-top: @spacing-sm;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px;
} }
&__row { &__row {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 12px; gap: @spacing-sm;
width: 100%; width: 100%;
height: 50px; height: 40px;
padding: 0; padding: 0 @spacing-sm;
border-radius: @radius-sm; border-radius: @radius-sm;
font-size: 20px; font-size: @font-size-base;
color: @heading-color; color: @heading-color;
transition: background-color 0.2s, color 0.2s; transition: background-color 0.2s, color 0.2s;
@@ -301,8 +301,8 @@ function onClearChips(): void {
&__box { &__box {
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 14px;
height: 16px; height: 14px;
border: 1px solid @border-color-dark; border: 1px solid @border-color-dark;
border-radius: 2px; border-radius: 2px;
background: @white; background: @white;
@@ -312,9 +312,9 @@ function onClearChips(): void {
content: ''; content: '';
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 4px; left: 3px;
width: 5px; width: 4px;
height: 8px; height: 7px;
border: solid @white; border: solid @white;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0); transform: rotate(45deg) scale(0);
@@ -340,25 +340,25 @@ function onClearChips(): void {
&__row-count { &__row-count {
flex-shrink: 0; flex-shrink: 0;
margin-left: @spacing-sm; margin-left: @spacing-sm;
font-size: 16px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
color: @text-color; color: @text-color-secondary;
} }
&__grid { &__grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px 33px; gap: @spacing-sm;
margin-top: 21px; margin-top: @spacing-sm;
} }
&__pill { &__pill {
height: 53px; height: 36px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-base; border-radius: @radius-base;
background: @pill-bg; background: @project-pill-bg;
color: @text-color; color: @text-color;
font-size: 20px; font-size: @font-size-sm;
.ellipsis(); .ellipsis();
transition: background-color 0.2s, color 0.2s; transition: background-color 0.2s, color 0.2s;
@@ -367,8 +367,9 @@ function onClearChips(): void {
} }
&.is-active { &.is-active {
background: @primary-color; background: @filter-selected-bg;
color: @white; color: @primary-color;
font-weight: 500;
} }
&--wide { &--wide {
@@ -378,20 +379,20 @@ function onClearChips(): void {
&__range { &__range {
.flex(row, space-between, center); .flex(row, space-between, center);
margin-top: 9px; margin-top: @spacing-sm;
gap: @spacing-xs; gap: @spacing-xs;
} }
&__date { &__date {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 53px; height: 36px;
padding: 0 @spacing-xs; padding: 0 @spacing-sm;
border: 1px solid @border-color; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @white; background: @white;
color: @text-color; color: @text-color;
font-size: 20px; font-size: @font-size-sm;
} }
&__range-sep { &__range-sep {
@@ -399,8 +400,8 @@ function onClearChips(): void {
} }
&__chips { &__chips {
margin: 23px 50px 0 35px; margin: @spacing-lg @spacing-lg 0;
padding: 16px 24px 21px; padding: @spacing-base;
border-radius: @radius-base; border-radius: @radius-base;
background: @chips-panel-bg; background: @chips-panel-bg;
} }
@@ -410,13 +411,13 @@ function onClearChips(): void {
} }
&__chips-title { &__chips-title {
font-size: 20px; font-size: @font-size-base;
font-weight: 500; font-weight: 500;
color: @primary-color; color: @primary-color;
} }
&__chips-clear { &__chips-clear {
font-size: 16px; font-size: @font-size-sm;
color: @primary-color; color: @primary-color;
&:hover { &:hover {
@@ -427,18 +428,18 @@ function onClearChips(): void {
&__chips-list { &__chips-list {
.flex(row, flex-start, center); .flex(row, flex-start, center);
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 16px; margin-top: @spacing-sm;
gap: 14px 13px; gap: @spacing-sm;
} }
&__chip { &__chip {
.flex(row, center, center); .flex(row, center, center);
height: 32px; height: 28px;
padding: 0 @spacing-sm; padding: 0 @spacing-sm;
border-radius: @radius-round; border-radius: @radius-round;
background: @white; background: @white;
color: @primary-color; color: @primary-color;
font-size: 14px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
&:hover { &:hover {
@@ -452,17 +453,17 @@ function onClearChips(): void {
&__actions { &__actions {
.flex(row, flex-start, center); .flex(row, flex-start, center);
margin-top: 30px; margin-top: @spacing-lg;
gap: 13px; gap: @spacing-sm;
} }
&__reset { &__reset {
flex: 0 0 135px; flex: 0 0 88px;
height: 30px; height: 32px;
border-radius: @radius-round; border-radius: @radius-round;
background: @pill-bg; background: @project-pill-bg;
color: @text-color; color: @text-color;
font-size: 14px; font-size: @font-size-sm;
&:hover { &:hover {
color: @primary-color; color: @primary-color;
@@ -471,11 +472,11 @@ function onClearChips(): void {
&__submit { &__submit {
flex: 1; flex: 1;
height: 30px; height: 32px;
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-color; background: @primary-color;
color: @white; color: @white;
font-size: 14px; font-size: @font-size-sm;
&:hover { &:hover {
background: @primary-hover; background: @primary-hover;

View File

@@ -69,19 +69,20 @@ function go(page: number): void {
<style lang="less" scoped> <style lang="less" scoped>
.project-pager { .project-pager {
.flex(row, flex-end, center); .flex(row, flex-end, center);
gap: 4px; gap: @spacing-xs;
margin-top: 15px; margin-top: @spacing-lg;
&__btn { &__btn {
.flex(row, center, center); .flex(row, center, center);
width: 20px; min-width: 28px;
height: 20px; height: 28px;
border: 1px solid @border-color-dark; padding: 0 6px;
border-radius: 2px; border: 1px solid @border-color;
border-radius: @radius-sm;
background: @white; background: @white;
color: @heading-color; color: @text-color;
font-size: @font-size-sm; font-size: @font-size-sm;
transition: border-color 0.2s, color 0.2s; transition: border-color 0.2s, color 0.2s, background-color 0.2s;
&:hover:not(:disabled):not(.is-ellipsis) { &:hover:not(:disabled):not(.is-ellipsis) {
border-color: @primary-color; border-color: @primary-color;
@@ -90,7 +91,9 @@ function go(page: number): void {
&.is-active { &.is-active {
border-color: @primary-color; border-color: @primary-color;
background: @primary-light;
color: @primary-color; color: @primary-color;
font-weight: 500;
} }
&.is-ellipsis { &.is-ellipsis {

View File

@@ -54,7 +54,7 @@ function progressLabel(value: number | null | undefined): string {
<th>业务类型</th> <th>业务类型</th>
<th>进度</th> <th>进度</th>
<th>状态</th> <th>状态</th>
<th /> <th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -108,7 +108,8 @@ function progressLabel(value: number | null | undefined): string {
th, th,
td { td {
border: 1px solid @project-table-line; border: none;
border-bottom: 1px solid @project-table-line;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
font-size: @font-size-sm; font-size: @font-size-sm;
@@ -117,22 +118,27 @@ function progressLabel(value: number | null | undefined): string {
th { th {
height: 44px; height: 44px;
background: @project-table-head-bg; background: @project-table-head-bg;
color: @heading-color; color: @text-color-secondary;
font-weight: 600; font-weight: 500;
} }
td { td {
height: 76px; height: 64px;
color: @heading-color; color: @heading-color;
transition: background-color 0.15s;
} }
th:nth-child(1) { width: 6.09%; } tbody tr:hover td {
th:nth-child(2) { width: 29.34%; } background: @bg-color-light;
th:nth-child(3) { width: 21.76%; } }
th:nth-child(4) { width: 10.68%; }
th:nth-child(5) { width: 14.37%; } th:nth-child(1) { width: 6%; }
th:nth-child(6) { width: 8.98%; } th:nth-child(2) { width: 18%; }
th:nth-child(7) { width: 8.78%; } th:nth-child(3) { width: 24%; }
th:nth-child(4) { width: 14%; }
th:nth-child(5) { width: 10%; }
th:nth-child(6) { width: 12%; }
th:nth-child(7) { width: 16%; }
} }
&__name { &__name {
@@ -142,7 +148,7 @@ function progressLabel(value: number | null | undefined): string {
&__ops { &__ops {
.flex(row, center, center); .flex(row, center, center);
gap: 10px; gap: @spacing-base;
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -76,21 +76,28 @@ const keyword = computed({
<style lang="less" scoped> <style lang="less" scoped>
.project-toolbar { .project-toolbar {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 24px; gap: @spacing-base;
&__field { &__field {
.flex(row, flex-start, center); .flex(row, flex-start, center);
position: relative; position: relative;
flex: 0 1 380px; flex: 0 1 360px;
min-width: 0; min-width: 0;
height: 37px; height: 36px;
border: 1px solid @border-color;
border-radius: @radius-round; border-radius: @radius-round;
background: @input-bg; background: @project-input-bg;
transition: border-color 0.2s, background-color 0.2s;
&:focus-within {
border-color: @primary-color;
background: @white;
}
} }
&__icon { &__icon {
position: absolute; position: absolute;
left: 16px; left: @spacing-base;
color: @text-color-disabled; color: @text-color-disabled;
pointer-events: none; pointer-events: none;
} }
@@ -113,10 +120,10 @@ const keyword = computed({
&__btn { &__btn {
flex-shrink: 0; flex-shrink: 0;
height: 26px; height: 36px;
padding: 0 22px; padding: 0 @spacing-lg;
border-radius: @radius-round; border-radius: @radius-round;
background: @pill-bg; background: @project-pill-bg;
color: @text-color; color: @text-color;
font-size: @font-size-base; font-size: @font-size-base;
transition: color 0.2s, background-color 0.2s; transition: color 0.2s, background-color 0.2s;

View File

@@ -92,75 +92,81 @@ onMounted(load)
</script> </script>
<template> <template>
<div class="container project-detail"> <div class="project-detail">
<nav class="project-detail__breadcrumb text-secondary"> <div class="container project-detail__inner">
<RouterLink to="/project" class="link"> <nav class="project-detail__breadcrumb text-secondary">
项目中心 <RouterLink to="/project" class="link">
</RouterLink> 项目中心
<span> &gt; </span> </RouterLink>
<span>项目详情</span> <span> &gt; </span>
</nav> <span>项目详情</span>
</nav>
<template v-if="current"> <template v-if="current">
<div class="project-detail__info"> <div class="project-detail__panel">
<ProjectInfoCard :project="current" /> <div class="project-detail__info">
</div> <ProjectInfoCard :project="current" />
</div>
<div class="project-detail__tabs"> <div class="project-detail__tabs">
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.value" :key="tab.value"
class="project-detail__tab" class="project-detail__tab"
:class="{ :class="{
'is-active': activeTab === tab.value, 'is-active': activeTab === tab.value,
'is-loading': loadingTab === tab.value, 'is-loading': loadingTab === tab.value,
}" }"
@click="onTabClick(tab.value)" @click="onTabClick(tab.value)"
> >
{{ tab.label }} {{ tab.label }}
</button> </button>
</div> </div>
<template v-if="activeTab === 'progress'"> <div class="project-detail__body">
<ProjectProgress <template v-if="activeTab === 'progress'">
:progress="current.progress" <ProjectProgress
:start-time="current.startTime" :progress="current.progress"
:end-time="current.endTime" :start-time="current.startTime"
/> :end-time="current.endTime"
<ProjectMilestones :milestones="current.milestones" /> />
<ProjectTeam :team="current.team" /> <ProjectMilestones :milestones="current.milestones" />
<ProjectTeam :team="current.team" />
</template>
<ProjectFiles
v-if="activeTab === 'files'"
:files="current.files"
@delete="onDeleteFile"
@preview="onPreviewFile"
@download="onDownloadFile"
/>
<ProjectFinance
v-if="activeTab === 'finance'"
:payment-plans="current.paymentPlans"
:invoice-records="current.invoiceRecords"
:contract-file="current.contractFile"
@pay="onPay"
@preview="onFinancePreview"
@download="onFinanceDownload"
/>
<ProjectAfterSales
v-if="activeTab === 'afterSales'"
:after-sales="current.afterSales"
:project-status="current.status"
@apply="onApplyAfterSales"
@cancel="cancelVisible = true"
/>
</div>
</div>
</template> </template>
<ProjectFiles <p v-else class="project-detail__empty text-secondary">
v-if="activeTab === 'files'" 项目不存在或已取消
:files="current.files" </p>
@delete="onDeleteFile" </div>
@preview="onPreviewFile"
@download="onDownloadFile"
/>
<ProjectFinance
v-if="activeTab === 'finance'"
:payment-plans="current.paymentPlans"
:invoice-records="current.invoiceRecords"
:contract-file="current.contractFile"
@pay="onPay"
@preview="onFinancePreview"
@download="onFinanceDownload"
/>
<ProjectAfterSales
v-if="activeTab === 'afterSales'"
:after-sales="current.afterSales"
:project-status="current.status"
@apply="onApplyAfterSales"
@cancel="cancelVisible = true"
/>
</template>
<p v-else class="project-detail__empty text-secondary">
项目不存在或已取消
</p>
<ConfirmDialog <ConfirmDialog
v-model:visible="cancelVisible" v-model:visible="cancelVisible"
@@ -175,45 +181,52 @@ onMounted(load)
<style lang="less" scoped> <style lang="less" scoped>
.project-detail { .project-detail {
.flex(column, flex-start, stretch); min-height: 100%;
gap: 0;
padding-bottom: @spacing-page-bottom; padding-bottom: @spacing-page-bottom;
background: @white; background: @bg-color;
&__inner {
.flex(column, flex-start, stretch);
gap: 0;
}
// 设计稿 el55 面包屑 y=116,header 底 72 → padding-top 44px。
&__breadcrumb { &__breadcrumb {
padding-top: 44px; padding-top: @spacing-lg;
font-size: 24px; font-size: @font-size-sm;
font-weight: 500; font-weight: 400;
line-height: 27px; line-height: 1.5;
color: @project-breadcrumb; color: @project-breadcrumb;
} }
// 设计稿 el51 logo 盒 y=195(面包屑盒底 = 72+44+27 = 143)→ 间距 52px。 &__panel {
&__info { margin-top: @spacing-base;
margin-top: 52px; padding: @spacing-lg;
border-radius: @radius-base;
background: @white;
}
&__info {
margin-top: 0;
} }
// 设计稿 el42 tab 文字 y=264,meta 文字底 231+15=246。
&__tabs { &__tabs {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 16.45px; gap: @spacing-lg;
height: 41px; height: 40px;
// 老大要求:信息卡与 tabs 栏之间留 20px 间距(原设计稿 2px 覆盖)。 margin-top: @spacing-lg;
margin-top: 20px;
box-sizing: border-box; box-sizing: border-box;
border-bottom: 1px solid @project-divider; border-bottom: 1px solid @border-color;
} }
&__tab { &__tab {
position: relative; position: relative;
padding: 0 0 24px; padding: 0 0 @spacing-base;
border: none; border: none;
background: transparent; background: transparent;
color: @project-cell; color: @project-cell;
font-size: 13px; font-size: @font-size-sm;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 1.4;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
@@ -242,6 +255,10 @@ onMounted(load)
} }
} }
&__body {
padding-top: @spacing-lg;
}
&__empty { &__empty {
padding: @spacing-xxl 0; padding: @spacing-xxl 0;
text-align: center; text-align: center;

View File

@@ -111,42 +111,44 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.project { .project {
min-height: 100%;
padding-bottom: @spacing-page-bottom; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__head { &__head {
padding: 16px 0 32px; padding: @spacing-lg 0;
background: @white; background: @white;
border-bottom: 1px solid @border-color;
} }
&__title { &__title {
font-size: @font-size-xxl; font-size: @font-size-title;
font-weight: 600; font-weight: 600;
color: @heading-color; color: @heading-color;
line-height: 1.2; line-height: 1.3;
} }
&__slogan { &__slogan {
margin-top: 14px; margin-top: @spacing-sm;
font-size: @font-size-base; font-size: @font-size-base;
color: @text-color-secondary; color: @text-color-secondary;
} }
&__body { &__body {
.flex(row, flex-start, stretch); .flex(row, flex-start, stretch);
gap: 17px; gap: @spacing-lg;
padding-top: 18px; padding-top: @spacing-lg;
} }
&__main { &__main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 18px 12px 20px; padding: @spacing-lg;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
// 搜索表单(ProjectToolbar)与下方项目表格间距 35px(老大指定)
.project-toolbar + .project-table { .project-toolbar + .project-table {
margin-top: 35px; margin-top: @spacing-lg;
} }
} }
} }

View File

@@ -31,45 +31,47 @@ function extOf(name: string): string {
<style lang="less" scoped> <style lang="less" scoped>
.chat-file-card { .chat-file-card {
.flex(row, center); .flex(row, center);
gap: 12px; gap: @spacing-base;
min-width: 300px; min-width: 240px;
max-width: 420px; max-width: 420px;
padding: 14px 16px; padding: @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-shadow: @shadow-base;
&__icon { &__icon {
flex-shrink: 0; flex-shrink: 0;
.flex(row, center); .flex(row, center);
width: 37px; width: 40px;
height: 37px; height: 40px;
border-radius: @radius-sm; border-radius: @radius-sm;
background: @service-scroll-bg; background: @primary-light;
color: @primary-strong; color: @primary-strong;
font-size: 11px; font-size: @font-size-xs;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
} }
&__info { &__info {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 6px; gap: @spacing-xs;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
&__name { &__name {
font-size: 12px; font-size: @font-size-sm;
font-weight: 700; font-weight: 600;
line-height: 16px; line-height: 1.4;
color: @service-bubble-text; color: @heading-color;
.ellipsis(); .ellipsis();
} }
&__size { &__size {
font-size: 12px; font-size: @font-size-xs;
line-height: 16px; line-height: 1.4;
color: @project-muted; color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, ref } from 'vue' import { nextTick, ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
const MAX_FILE_SIZE = 1024 * 1024 * 1024 const MAX_FILE_SIZE = 1024 * 1024 * 1024
const content = ref('') const content = ref('')
const fileRef = ref<HTMLInputElement | null>(null)
const emit = defineEmits<{ const emit = defineEmits<{
send: [content: string] send: [content: string]
@@ -26,6 +27,10 @@ function onKeydown(event: KeyboardEvent): void {
} }
} }
function onPickFile(): void {
fileRef.value?.click()
}
function onFileChange(event: Event): void { function onFileChange(event: Event): void {
const target = event.target const target = event.target
if (!(target instanceof HTMLInputElement)) return if (!(target instanceof HTMLInputElement)) return
@@ -45,27 +50,35 @@ function onFileChange(event: Event): void {
<textarea <textarea
v-model="content" v-model="content"
class="chat-input__textarea" class="chat-input__textarea"
placeholder="输入消息,按Enter发送,Shift+Enter换行..." placeholder="输入消息,按 Enter 发送,Shift + Enter 换行"
rows="3" rows="3"
@keydown="onKeydown" @keydown="onKeydown"
/> />
<div class="chat-input__bar"> <div class="chat-input__bar">
<span class="chat-input__hint"> <div class="chat-input__tools">
按 Enter 发送 | 支持拖拽上传文件 <button
</span> type="button"
<div class="chat-input__actions"> class="chat-input__attach"
<input @click="onPickFile"
type="file"
class="chat-input__file"
@change="onFileChange"
> >
<el-button 附件
class="chat-input__send" </button>
@click="doSend" <span class="chat-input__hint">Enter 发送</span>
>
发送
</el-button>
</div> </div>
<input
ref="fileRef"
type="file"
class="chat-input__file"
@change="onFileChange"
>
<button
type="button"
class="chat-input__send"
:disabled="!content.trim()"
@click="doSend"
>
发送
</button>
</div> </div>
</div> </div>
</template> </template>
@@ -73,34 +86,34 @@ function onFileChange(event: Event): void {
<style lang="less" scoped> <style lang="less" scoped>
.chat-input { .chat-input {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: @spacing-base;
padding: 16px 22px 20px; padding: @spacing-base @spacing-lg @spacing-lg;
border-top: 1px solid @service-card-line; border-top: 1px solid @border-color;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
&__textarea { &__textarea {
width: 100%; width: 100%;
height: 72px; min-height: 84px;
padding: 12px 14px; padding: @spacing-base;
border: none; border: 1px solid @border-color;
border-radius: @radius-sm; border-radius: @radius-base;
background: @service-input-bg; background: @bg-color-light;
color: @service-bubble-text; color: @service-bubble-text;
font-size: 13px; font-size: @font-size-sm;
line-height: 1.6; line-height: 1.6;
resize: none; resize: none;
outline: none; outline: none;
transition: background 0.2s; transition: border-color 0.2s, background 0.2s;
box-sizing: border-box; box-sizing: border-box;
&:focus { &:focus {
border: none; border-color: @primary-color;
background: @service-input-bg; background: @white;
} }
&::placeholder { &::placeholder {
color: @project-muted; color: @text-color-secondary;
} }
} }
@@ -108,24 +121,35 @@ function onFileChange(event: Event): void {
.flex(row, space-between, center); .flex(row, space-between, center);
gap: @spacing-base; gap: @spacing-base;
width: 100%; width: 100%;
margin-top: 0;
padding-top: 14px;
background: @service-bar-bg;
} }
&__hint { &__tools {
font-size: 11px; .flex(row, flex-start, center);
line-height: 16px; gap: @spacing-base;
color: @project-muted; min-width: 0;
}
&__actions {
.flex(row, flex-end, center);
gap: @spacing-sm;
} }
&__attach { &__attach {
display: none; height: 32px;
padding: 0 @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-round;
background: @white;
color: @text-color;
font-size: @font-size-sm;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: @primary-strong;
color: @primary-strong;
}
}
&__hint {
font-size: @font-size-xs;
line-height: 1.4;
color: @text-color-secondary;
} }
&__file { &__file {
@@ -133,20 +157,26 @@ function onFileChange(event: Event): void {
} }
&__send { &__send {
width: 76px; flex-shrink: 0;
height: 30px; min-width: 80px;
padding: 0; height: 32px;
border-color: @primary-strong; padding: 0 @spacing-lg;
border-radius: @radius-sm; border: none;
border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 12px; font-size: @font-size-sm;
font-weight: 500; font-weight: 500;
cursor: pointer;
transition: background 0.2s;
&:hover { &:hover:not(:disabled) {
border-color: @primary-strong; background: @primary-hover;
background: @primary-strong; }
color: @white;
&:disabled {
opacity: 0.45;
cursor: not-allowed;
} }
} }
} }

View File

@@ -29,14 +29,14 @@ withDefaults(defineProps<Props>(), { unread: false })
<span class="chat-message-item__time">{{ message.time }}</span> <span class="chat-message-item__time">{{ message.time }}</span>
</div> </div>
<span v-if="message.role === 'user'" class="chat-message-item__avatar chat-message-item__avatar--user">王</span> <span v-if="message.role === 'user'" class="chat-message-item__avatar chat-message-item__avatar--user">我</span>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.chat-message-item { .chat-message-item {
.flex(row, flex-start, flex-start); .flex(row, flex-start, flex-start);
gap: 16px; gap: @spacing-base;
&__avatar { &__avatar {
flex-shrink: 0; flex-shrink: 0;
@@ -46,55 +46,54 @@ withDefaults(defineProps<Props>(), { unread: false })
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
font-size: 14px; font-size: @font-size-sm;
font-weight: 400; font-weight: 500;
&--user { &--user {
background: @service-user-avatar; background: @service-user-avatar;
color: @white;
} }
} }
&__main { &__main {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
gap: 6px; gap: @spacing-xs;
max-width: 420px; max-width: min(480px, 70%);
} }
&__bubble { &__bubble {
position: relative; position: relative;
padding: 12px 16px; padding: @spacing-base;
border-radius: @radius-base; border-radius: @radius-base @radius-base @radius-base @radius-sm;
background: @white; background: @white;
border: 1px solid @border-color;
color: @service-bubble-text; color: @service-bubble-text;
font-size: 13px; font-size: @font-size-sm;
line-height: 1.7; line-height: 1.7;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
box-shadow: @shadow-base;
} }
&__bubble:has(.chat-file-card) { &__bubble:has(.chat-file-card) {
padding: 0; padding: 0;
border: none;
background: transparent; background: transparent;
box-shadow: none;
} }
&__time { &__time {
font-size: 11px; font-size: @font-size-xs;
line-height: 16px; line-height: 1.4;
color: @project-muted; color: @text-color-secondary;
} }
&__unread { &__unread {
position: absolute; position: absolute;
top: 100%; top: calc(100% + @spacing-xs);
left: 0; left: 0;
margin-top: 8px; color: @text-color-secondary;
padding: 0; font-size: @font-size-xs;
background: transparent; line-height: 1.4;
color: @project-muted;
font-size: 10px;
font-weight: 400;
line-height: 14px;
} }
&--user { &--user {
@@ -105,8 +104,11 @@ withDefaults(defineProps<Props>(), { unread: false })
} }
.chat-message-item__bubble { .chat-message-item__bubble {
border: none;
border-radius: @radius-base @radius-base @radius-sm @radius-base;
background: @primary-strong; background: @primary-strong;
color: @white; color: @white;
box-shadow: none;
} }
} }
} }

View File

@@ -37,14 +37,17 @@ watch(
<template> <template>
<div class="chat-window"> <div class="chat-window">
<div class="chat-window__header"> <div class="chat-window__header">
<span class="chat-window__header-avatar" /> <span class="chat-window__header-avatar" aria-hidden="true">客</span>
<span class="chat-window__header-name">专属客服</span> <div class="chat-window__header-meta">
<span class="chat-window__header-name">专属客服</span>
<span class="chat-window__header-status">在线 · 通常几分钟内回复</span>
</div>
</div> </div>
<div ref="scrollRef" class="chat-window__scroll"> <div ref="scrollRef" class="chat-window__scroll">
<template v-for="(item, index) in messages" :key="item.id"> <template v-for="(item, index) in messages" :key="item.id">
<div v-if="index === 0" class="chat-window__divider"> <div v-if="index === 0" class="chat-window__divider">
—— 历史消息 —— <span class="chat-window__divider-text">历史消息</span>
</div> </div>
<ChatMessageItem <ChatMessageItem
:message="item" :message="item"
@@ -60,7 +63,7 @@ watch(
<style lang="less" scoped> <style lang="less" scoped>
.chat-window { .chat-window {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
border: 1px solid @service-card-line; border: 1px solid @border-color;
border-radius: @radius-base; border-radius: @radius-base;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
@@ -68,47 +71,75 @@ watch(
&__header { &__header {
.flex(row, flex-start, center); .flex(row, flex-start, center);
gap: 16px; gap: @spacing-base;
height: 66px; min-height: 64px;
padding: 0 22px; padding: @spacing-base @spacing-lg;
border-bottom: 1px solid @service-card-line; border-bottom: 1px solid @border-color;
background: @white; background: @white;
box-sizing: border-box; box-sizing: border-box;
} }
&__header-avatar { &__header-avatar {
flex-shrink: 0; flex-shrink: 0;
width: 33px; .flex(row, center);
height: 33px; width: 40px;
height: 40px;
border-radius: @radius-round; border-radius: @radius-round;
background: @primary-strong; background: @primary-strong;
color: @white;
font-size: @font-size-base;
font-weight: 600;
}
&__header-meta {
.flex(column, flex-start, flex-start);
gap: 2px;
min-width: 0;
} }
&__header-name { &__header-name {
font-size: 13px; font-size: @font-size-base;
font-weight: 500; font-weight: 600;
line-height: 18px; line-height: 1.4;
color: @service-header-name; color: @heading-color;
}
&__header-status {
font-size: @font-size-xs;
line-height: 1.4;
color: @text-color-secondary;
} }
&__scroll { &__scroll {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 32px; gap: @spacing-lg;
height: 450px; height: 460px;
padding: 24px 22px; padding: @spacing-lg;
background: @service-scroll-bg; background: @bg-color-light;
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
.scrollbar(); .scrollbar();
} }
&__divider { &__divider {
.flex(row, center, center);
flex-shrink: 0; flex-shrink: 0;
margin-bottom: -18px; gap: @spacing-base;
font-size: 12px;
line-height: 18px; &::before,
color: @project-muted; &::after {
text-align: center; content: '';
flex: 1;
height: 1px;
background: @border-color;
}
}
&__divider-text {
flex-shrink: 0;
font-size: @font-size-xs;
line-height: 1.4;
color: @text-color-secondary;
} }
} }
</style> </style>

View File

@@ -29,7 +29,7 @@ onMounted(() => {
slogan="在线客服为您提供需求咨询、项目进度查询、业务答疑,快速协助解决平台操作与项目相关问题" slogan="在线客服为您提供需求咨询、项目进度查询、业务答疑,快速协助解决平台操作与项目相关问题"
/> />
<div class="service__wrap"> <div class="container service__wrap">
<ChatWindow <ChatWindow
:messages="messages" :messages="messages"
@send="onSend" @send="onSend"
@@ -44,12 +44,16 @@ onMounted(() => {
.flex(column, flex-start, stretch); .flex(column, flex-start, stretch);
gap: 0; gap: 0;
width: 100%; width: 100%;
padding-bottom: 60px; min-height: 100%;
background: @message-page-bg; padding-bottom: @spacing-page-bottom;
background: @bg-color;
&__wrap { &__wrap {
width: 100%; width: 100%;
padding: 0 70px; max-width: 960px;
margin-left: auto;
margin-right: auto;
padding: @spacing-lg 0 0;
box-sizing: border-box; box-sizing: border-box;
} }
} }