--- 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//index.vue`。 4. **组件两级归属**:跨 ≥2 个页面/分包复用 → 全局 `components/`;单页面独有 → 该页面的 `cpns/`。 5. **技术选型跟随现状**:用 Pinia 就不引 Vuex;用 scss 就不写 less;用 TS 就不落 `.js`。**不擅自更换技术栈**。 6. **最小改动**:只改与本次需求相关的行;无关死代码只报告、不删除。 --- ## 骨架 A:Vue 3 网页端 / 中后台 ``` src/ ├── api/.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/.ts ├── styles/ # variables / mixins / reset / common + 入口 ├── types/ # api.d.ts / router.d.ts / business/.d.ts ├── utils/ # request.ts(唯一 axios 实例)/ storage / validate / format └── views/ └── / # ★ 页面 = 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 缓存 + │ └── components/ # Sider / Header / Breadcrumb / TagsView ├── router/ │ ├── routes.ts # constantRoutes(无权限) │ └── modules.ts # asyncRoutes(按角色过滤) ├── permission.ts # 守卫:登录态 → 白名单 → 动态路由注入 ├── stores/modules/ # user(token/角色/权限)、permission(动态路由)、app(侧栏/主题) └── views/// # 按模块再分页面,如 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/.ts # 接口层(uni.request 封装为函数) ├── stores/modules/.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//index.vue`(uni:`pages//index.vue`) | 渲染 + 交互 + 编排 | 直接调 api、直接 axios/request | | 页面私有组件 | `views//cpns/`(uni 同名) | 仅本页用 | 被其他页 import | | 公共组件 | `components/` | ≥2 处复用 | 含单页业务逻辑 | | 状态层 | `stores/modules/.ts` | 取数 + 状态 + 缓存 | 拼 URL、写 UI 逻辑 | | 接口层 | `api/.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 -->|业务无关
按钮/弹窗/上传| E[全局 components/] D -->|业务相关| F[全局 components//] 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` | --- ## 规范速查(形态无关的硬性项) ### 组件规范 - 统一 `