336 lines
18 KiB
Markdown
336 lines
18 KiB
Markdown
---
|
||
name: vue-project-structure
|
||
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
|
||
---
|
||
|
||
# Vue 3项目目录结构规范(通用 · 先探测后套用)
|
||
|
||
> 适用范围:**Vue 3**(含 uni-app 的 Vue 3 版本)。不涉及 Vue 2。
|
||
> 本 skill 与具体仓库无关:执行顺序永远是 **① 探测现状 → ② 选定骨架变体 → ③ 按规范落位**。项目内已有显式文档(`AGENTS.md`/`CLAUDE.md`、`docs/` 下的结构说明)优先级高于本 skill。
|
||
|
||
---
|
||
|
||
## 第零步:探测项目形态(每次加载必做)
|
||
|
||
### 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` |
|
||
|
||
### 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/news/index.vue 或 pages/news/index.vue)
|
||
│ ① 只读 store state / 调 store action
|
||
▼
|
||
状态层 (stores/modules/news.ts) —— 数据层:取数 + 状态 + 缓存 + 跨页共享
|
||
│ ② action 内部调用 api 函数 → await → 写 state
|
||
▼
|
||
API (api/news.ts) —— 接口层:只发请求、只返回带类型的数据
|
||
│ ③ request.ts(axios / uni.request 封装)
|
||
▼
|
||
后端
|
||
```
|
||
|
||
铁律:
|
||
|
||
- 页面**禁止** import api;取数必须走 store。
|
||
- api 是**纯接口层**:不发业务判断、不缓存、不 toast。
|
||
- store 承担取数、状态、缓存、跨页共享;本地缓存用 `storage.ts`,不直接写 `localStorage` / `uni.setStorageSync`。
|
||
- 依赖方向单向:`页面 → store → api`,禁止反向与跨层直连。
|
||
|
||
---
|
||
|
||
## 各层职责速查
|
||
|
||
| 层 | 目录 | 一句话职责 | 禁止 |
|
||
| ------------ | --------------------------------------------------------- | ---------------------------------------- | ------------------------------ |
|
||
| 页面 | `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 注入]
|
||
```
|
||
|
||
### 命名一致性
|
||
|
||
同一业务域 `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` |
|
||
|
||
---
|
||
|
||
## 规范速查(形态无关的硬性项)
|
||
|
||
### 组件规范
|
||
|
||
- 统一 `<script setup lang="ts">`。
|
||
- props:`defineProps<Props>()` + `withDefaults`;emits:`defineEmits<{ change: [val: number] }>()`,事件名 camelCase。
|
||
- 模板内不写复杂表达式(超过一次三元/逻辑运算即抽方法或 computed)。
|
||
- 单文件 > 400 行 → 拆到同页面 `cpns/`。
|
||
- uni-app 组件模板用 `view/text/image`。
|
||
|
||
### 样式规范
|
||
|
||
- **跟随项目既有方案**(less 或 scss),不混用两套预处理器。
|
||
- 变量单一来源:Vue 项目 `src/styles/variables.*`;uni-app 项目 `uni.scss`。
|
||
- 组件内禁止魔法色值/魔法间距。
|
||
- Vue 项目:`<style scoped lang="less|scss">`;uni-app 页面样式**不加 `scoped`**(小程序不支持),靠类名隔离。
|
||
|
||
### 类型规范
|
||
|
||
- 实体类型 → `types/business/<domain>.d.ts`;接口通用结构 → `types/api.d.ts`。
|
||
- **禁止** `as any` / `@ts-ignore` / `@ts-expect-error`。
|
||
|
||
### 导入规范
|
||
|
||
- 有别名(`@/`)的项目一律用别名;禁止多级跨层 `../../../`。
|
||
- uni-app 需在 `vite.config` 的 `resolve.alias` 或 `tsconfig` 的 `paths` 配置 `@/`。
|
||
|
||
### 请求规范
|
||
|
||
- 全项目**唯一**请求实例/封装:`utils/request.ts`;页面与组件禁止直接 `axios` / `uni.request` / `fetch`。
|
||
- baseURL 走环境变量(`import.meta.env.VITE_*`)。
|
||
- 错误统一由拦截器兜底,业务侧仅在需要特殊处理时 `catch`。
|
||
|
||
---
|
||
|
||
## 工作流
|
||
|
||
### 场景速查:想做什么 → 改哪些文件
|
||
|
||
| 想做的事 | 动作 |
|
||
| ---------------------- | ------------------------------------------------------------------------------------------ |
|
||
| 给已有页面加一块新数据 | 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/、页面在 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`
|
||
- [ ] 构建/类型检查零错误
|
||
|
||
> 发现不符合规范的历史代码:**指出问题并给出改法**,除非本次需求正好涉及,否则不擅自批量重构。
|