18 KiB
18 KiB
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 已存在)
探测不明确时先问用户,不要猜。
第一步:读取目标区域(探测后必做)
- 列出将要操作目录的现行结构(目录不存在则新建,不要自创新顶层目录)。
- 读取目标文件 + 同级 2 个文件,确认既有命名/导出/样式风格。
- 确认该业务域"页面 / api / store / 类型"在本项目中的实际路径。
核心原则(形态无关)
- 三层数据流:页面 → store → api。页面只通过 store 取数;store 调用 api 并维护状态;api 只发请求。页面禁止直接 import api。
- 业务同名单点:同一业务域的
页面目录/api 文件/store 文件/类型文件同名对应。 - 页面两件套(无 barrel):页面目录 =
index.vue+cpns/。不建index.ts,路由直接指向@/views/<page>/index.vue。 - 组件两级归属:跨 ≥2 个页面/分包复用 → 全局
components/;单页面独有 → 该页面的cpns/。 - 技术选型跟随现状:用 Pinia 就不引 Vuex;用 scss 就不写 less;用 TS 就不落
.js。不擅自更换技术栈。 - 最小改动:只改与本次需求相关的行;无关死代码只报告、不删除。
骨架 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 为例)
- 类型
types/business/news.d.ts - 接口
api/news.ts - 状态
stores/modules/news.ts(action 内调 api) - 页面
views/news/index.vue(或pages/news/index.vue)+cpns/ - 注册
router/routes.ts(component: () => import('@/views/news/index.vue'))或pages.json - 复用检查 ≥2 处复用的上移
components/ - 验证 类型检查 + 构建零错误(
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 - 构建/类型检查零错误
发现不符合规范的历史代码:指出问题并给出改法,除非本次需求正好涉及,否则不擅自批量重构。