fix skill
This commit is contained in:
@@ -1,382 +1,335 @@
|
||||
---
|
||||
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。
|
||||
version: 2.0.0
|
||||
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: 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/` 下的现行目录结构(如某目录尚不存在,应在对应职责目录创建,而不是自创新顶层目录)。
|
||||
2. 读取目标文件及其同级文件,确认现有命名风格与组织方式(页面名、store 名、api 名必须一致)。
|
||||
3. 只有在目录树确认完毕后,才允许创建/修改文件。
|
||||
### 0.1 读取证据
|
||||
|
||||
## 核心原则
|
||||
| 要看什么 | 怎么看 |
|
||||
| ---------- | --------------------------------------------------------------------------- |
|
||||
| 依赖清单 | `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**。
|
||||
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>`。
|
||||
### 0.2 判定形态
|
||||
|
||||
## 数据流架构(强约束)
|
||||
```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
|
||||
页面组件 (views/home/index.vue)
|
||||
│ ① 页面只调用 store 的 action / 读取 store 的 state(模板渲染数据)
|
||||
页面组件 (views/news/index.vue 或 pages/news/index.vue)
|
||||
│ ① 只读 store state / 调 store action
|
||||
▼
|
||||
Pinia store (stores/modules/home.ts) —— 数据层
|
||||
│ ② store 的 action 内部调用 api 接口函数 → await → 写入 state / 返回结果
|
||||
状态层 (stores/modules/news.ts) —— 数据层:取数 + 状态 + 缓存 + 跨页共享
|
||||
│ ② action 内部调用 api 函数 → await → 写 state
|
||||
▼
|
||||
API (api/home.ts) —— 接口层
|
||||
│ ③ 经 utils/request.ts 发 HTTP 请求
|
||||
API (api/news.ts) —— 接口层:只发请求、只返回带类型的数据
|
||||
│ ③ request.ts(axios / uni.request 封装)
|
||||
▼
|
||||
后端服务器
|
||||
后端
|
||||
```
|
||||
|
||||
铁律:
|
||||
|
||||
- **页面禁止 `import` api 层文件**;获取数据必须调对应页面 store 的方法。
|
||||
- **api 层是纯接口层**:只发请求、返回数据(带类型),不做业务状态处理、不缓存。
|
||||
- **store 承担取数 + 数据状态 + 页面间共享 + 缓存**;与页面同名的 store 责任边界即该页面。
|
||||
- 调用链必须是 `页面 → store → api` 单向,禁止跳过 store 或反向依赖。
|
||||
- 页面**禁止** import api;取数必须走 store。
|
||||
- api 是**纯接口层**:不发业务判断、不缓存、不 toast。
|
||||
- store 承担取数、状态、缓存、跨页共享;本地缓存用 `storage.ts`,不直接写 `localStorage` / `uni.setStorageSync`。
|
||||
- 依赖方向单向:`页面 → store → api`,禁止反向与跨层直连。
|
||||
|
||||
## src/ 目录结构(强制划分)
|
||||
---
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # ★ 接口层:按页面命名,一页一文件(home.ts、news.ts、product.ts、about.ts)
|
||||
├── assets/ # 参与构建的静态资源(images/、icons/、fonts/)
|
||||
├── components/ # ★ 公共组件:仅跨页面复用的全站通用组件(禁含具体页面业务)
|
||||
│ ├── AppHeader.vue # 全站头部(导航)
|
||||
│ ├── AppFooter.vue # 全站页脚
|
||||
│ ├── AppButton.vue # 通用按钮
|
||||
│ ├── AppSwiper.vue # 通用轮播
|
||||
│ └── index.ts # 统一导出
|
||||
├── composables/ # 通用组合式逻辑(useScroll、useLoading、useCountDown、useIntersection)
|
||||
├── directives/ # 自定义指令(lazy 图片懒加载、debounce、throttle + index.ts 统一注册)
|
||||
├── layout/ # 全站布局骨架:index.vue = <AppHeader/> + <router-view/> + <AppFooter/>
|
||||
├── router/ # 路由:index.ts(入口)、routes.ts(静态路由表)、guard.ts(守卫,可选)
|
||||
├── stores/ # ★ 数据层(Pinia):按页面命名,一页一 store
|
||||
│ ├── index.ts # pinia 实例(main.ts 中 app.use(pinia))
|
||||
│ └── modules/
|
||||
│ ├── home.ts # useHomeStore:首页数据(banner、推荐、公告)
|
||||
│ ├── news.ts # useNewsStore:新闻列表/分页/详情
|
||||
│ ├── product.ts # useProductStore:产品列表/详情
|
||||
│ └── site.ts # useSiteStore:站点公共数据(站点配置、全局 loading),非页面级
|
||||
├── styles/ # ★ 全局样式(less)
|
||||
│ ├── index.less # 样式入口(main.ts 引入),组织以下文件
|
||||
│ ├── variables.less # ★ less 变量唯一来源(颜色/间距/字号/圆角),禁止组件内魔法色值
|
||||
│ ├── 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/ # 登录(仅存在会员体系时)
|
||||
## 各层职责速查
|
||||
|
||||
| 层 | 目录 | 一句话职责 | 禁止 |
|
||||
| ------------ | --------------------------------------------------------- | ---------------------------------------- | ------------------------------ |
|
||||
| 页面 | `views/<page>/index.vue`(uni:`pages/<page>/index.vue`) | 渲染 + 交互 + 编排 | 直接调 api、直接 axios/request |
|
||||
| 页面私有组件 | `views/<page>/cpns/`(uni 同名) | 仅本页用 | 被其他页 import |
|
||||
| 公共组件 | `components/` | ≥2 处复用 | 含单页业务逻辑 |
|
||||
| 状态层 | `stores/modules/<domain>.ts` | 取数 + 状态 + 缓存 | 拼 URL、写 UI 逻辑 |
|
||||
| 接口层 | `api/<domain>.ts` | 请求分发 + 类型 | 状态、弹窗提示 |
|
||||
| 请求层 | `utils/request.ts` | 唯一实例/封装:token、统一解包、错误提示 | 被页面直接 import axios |
|
||||
| 工具 | `utils/`(纯函数) / `composables/`(响应式) | — | 互相混放 |
|
||||
|
||||
> **页面私有组件目录名统一用 `cpns/`**(不再用 `components/`,避免与全局 `components/` 混淆)。
|
||||
|
||||
### 组件归属判定(唯一判据)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[新组件] --> B{被几个页面/分包使用?}
|
||||
B -->|1 个| C[放该页面 cpns/]
|
||||
B -->|≥2 个| D{业务无关还是业务相关?}
|
||||
D -->|业务无关<br/>按钮/弹窗/上传| E[全局 components/]
|
||||
D -->|业务相关| F[全局 components/<domain>/]
|
||||
F --> G[差异用 props 注入]
|
||||
```
|
||||
|
||||
**命名一致性(可分层互查)**:页面 `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 页面使用才放这里
|
||||
- 页面独有组件 `views/<page>/cpns/`:**严禁被其他页面 import**;语义化命名(Head.vue、Banner.vue)
|
||||
- 页面结构三件套:`index.vue`(页面) + `index.ts`(出口 barrel) + `cpns/`(独有组件)
|
||||
- 组件统一 `<script setup lang="ts">`;props 用 `defineProps<Props>()` + `withDefaults`;emits 用 `defineEmits<{ 事件名: [参数类型] }>()`
|
||||
- 组件事件 camelCase 命名;模板内不放复杂表达式;样式默认 `<style lang="less" scoped>`
|
||||
- 单文件超 400 行即拆分到 `cpns/`
|
||||
- 统一 `<script setup lang="ts">`。
|
||||
- props:`defineProps<Props>()` + `withDefaults`;emits:`defineEmits<{ change: [val: number] }>()`,事件名 camelCase。
|
||||
- 模板内不写复杂表达式(超过一次三元/逻辑运算即抽方法或 computed)。
|
||||
- 单文件 > 400 行 → 拆到同页面 `cpns/`。
|
||||
- uni-app 组件模板用 `view/text/image`。
|
||||
|
||||
### 样式规范(less)
|
||||
### 样式规范
|
||||
|
||||
- 全项目使用 **less**,禁止新增 `.scss/.css` 业务样式
|
||||
- 全局变量统一在 `styles/variables.less`;组件内禁止魔法色值
|
||||
- vite `css.preprocessorOptions.less` 配置 `additionalData` 自动注入 variables
|
||||
- 依赖:`pnpm add -D less`(vite 内置 less 编译支持)
|
||||
- **跟随项目既有方案**(less 或 scss),不混用两套预处理器。
|
||||
- 变量单一来源:Vue 项目 `src/styles/variables.*`;uni-app 项目 `uni.scss`。
|
||||
- 组件内禁止魔法色值/魔法间距。
|
||||
- Vue 项目:`<style scoped lang="less|scss">`;uni-app 页面样式**不加 `scoped`**(小程序不支持),靠类名隔离。
|
||||
|
||||
### 工程化硬性约定
|
||||
### 类型规范
|
||||
|
||||
- **导入一律 `@/` 别名**;禁止跨目录相对路径(`../` 超过一层即违规)
|
||||
- utils 与 composables 判据:**需要响应式状态或生命周期 → composables;否则 → utils**
|
||||
- 类型归位:实体类型 → `types/business/<page>.d.ts`(与 api 同名);接口结构 → `types/api.d.ts`
|
||||
- **禁止 `as any` / `@ts-ignore` / `@ts-expect-error`**
|
||||
- 实体类型 → `types/business/<domain>.d.ts`;接口通用结构 → `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>/ 等)
|
||||
- [ ] **页面是否直接 import 了 api 层?(违规——数据必须经 store)**
|
||||
- [ ] **store 名 / api 文件名 / 页面目录名 / 类型文件名 是否与页面一致(可分层互查)**
|
||||
- [ ] 导入是否使用 `@/` 别名
|
||||
- [ ] 组件归属是否正确:≥2 页面共用 → components/;单页面独有 → views/<page>/cpns/
|
||||
- [ ] 不在组件里直接 new axios 实例 / 直接调用原生 fetch
|
||||
- [ ] 样式是否使用 **less**(`<style lang="less" scoped>` / \*.less),颜色是否取自 variables.less
|
||||
- [ ] 类型是否齐全(无 `as any`、无 `@ts-ignore`)
|
||||
- [ ] 新页面是否有完整三件套(index.vue + index.ts + cpns/)并在 router 注册
|
||||
- [ ] 运行时 `pnpm build`(含 vue-tsc 类型检查)零错误
|
||||
- [ ] 文件位于其职责对应目录(接口在 api/、状态在 stores/、页面在 views|pages/)
|
||||
- [ ] **页面是否直接 import 了 api?**(违规)
|
||||
- [ ] api / store / 页面 / 类型 是否同业务域命名一致
|
||||
- [ ] 页面目录是否只有 `index.vue`(+ 同模块次级页面)+ `cpns/`,**没有多余 `index.ts`**
|
||||
- [ ] 私有组件是否在 `cpns/` 且无其他页面 import
|
||||
- [ ] 导入使用 `@/` 别名,无多级跨层相对路径
|
||||
- [ ] 未在组件内直接 `axios` / `uni.request` / `fetch`
|
||||
- [ ] 样式沿用项目既有预处理器,色值取自变量文件
|
||||
- [ ] uni-app:新页面已在 `pages.json` 注册;未把非首屏页放进主包;页面样式未加 `scoped`
|
||||
- [ ] 无 `as any` / `@ts-ignore`
|
||||
- [ ] 构建/类型检查零错误
|
||||
|
||||
> 本规范为项目强约束:**新增与修改的代码必须严格遵循**。发现不符合结构约定的代码,应指出问题并要求按规范调整,而不是放任通过。
|
||||
> 发现不符合规范的历史代码:**指出问题并给出改法**,除非本次需求正好涉及,否则不擅自批量重构。
|
||||
|
||||
Reference in New Issue
Block a user