Files
2026-10-14 14:19:12 +08:00

18 KiB
Raw Permalink Blame History

name, description, version
name description version
vue-project-structure 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 项目中直接复用。 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 判定形态

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):

{
  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。

数据流架构(强约束,两形态通用)

页面组件 (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/ 混淆)。

组件归属判定(唯一判据)

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
  • 构建/类型检查零错误

发现不符合规范的历史代码:指出问题并给出改法,除非本次需求正好涉及,否则不擅自批量重构。