Files
cz-web/.claude/skills/vue-project-structure/SKILL.md
2026-09-16 11:06:56 +08:00

383 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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
---
# cz-web 项目目录结构规范(强制 · 网页端)
> 完整版调试文档见 `docs/vue-project-guide.md`(其中中后台相关部分已废弃),**本 skill 为唯一现行规范,以其为准**;未覆盖场景参照 Vue 3 官方目录约定。
## 第一步:读取项目目录(每次加载必做)
动手写任何代码前,先用工具映射当前项目结构,确认正在操作的正确层级:
1. 用 `codegraph_files` 或 `glob` 列出 `src/` 下的现行目录结构(如某目录尚不存在,应在对应职责目录创建,而不是自创新顶层目录)。
2. 读取目标文件及其同级文件,确认现有命名风格与组织方式(页面名、store 名、api 名必须一致)。
3. 只有在目录树确认完毕后,才允许创建/修改文件。
## 核心原则
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>`。
## 数据流架构(强约束)
```text
页面组件 (views/home/index.vue)
│ ① 页面只调用 store 的 action / 读取 store 的 state(模板渲染数据)
▼
Pinia store (stores/modules/home.ts) —— 数据层
│ ② store 的 action 内部调用 api 接口函数 → await → 写入 state / 返回结果
▼
API (api/home.ts) —— 接口层
│ ③ 经 utils/request.ts 发 HTTP 请求
▼
后端服务器
```
铁律:
- **页面禁止 `import` api 层文件**;获取数据必须调对应页面 store 的方法。
- **api 层是纯接口层**:只发请求、返回数据(带类型),不做业务状态处理、不缓存。
- **store 承担取数 + 数据状态 + 页面间共享 + 缓存**;与页面同名的 store 责任边界即该页面。
- 调用链必须是 `页面 → store → api` 单向,禁止跳过 store 或反向依赖。
## 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/ # 登录(仅存在会员体系时)
```
**命名一致性(可分层互查)**:页面 `home` → `api/home.ts` + `stores/modules/home.ts`(`useHomeStore`) + `types/business/home.d.ts` + `views/home/`,四者同名。
## src/ 每个文件的作用(新建/修改文件前先对照本节定位)
### 入口与根文件
| 文件 | 作用 |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `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/`
### 样式规范(less)
- 全项目使用 **less**,禁止新增 `.scss/.css` 业务样式
- 全局变量统一在 `styles/variables.less`;组件内禁止魔法色值
- vite `css.preprocessorOptions.less` 配置 `additionalData` 自动注入 variables
- 依赖:`pnpm add -D less`(vite 内置 less 编译支持)
### 工程化硬性约定
- **导入一律 `@/` 别名**;禁止跨目录相对路径(`../` 超过一层即违规)
- utils 与 composables 判据:**需要响应式状态或生命周期 → composables;否则 → utils**
- 类型归位:实体类型 → `types/business/<page>.d.ts`(与 api 同名);接口结构 → `types/api.d.ts`
- **禁止 `as any` / `@ts-ignore` / `@ts-expect-error`**
## 后续使用方式(工作流)
### 场景速查:想做什么 → 去找哪个文件
| 想做的事 | 动哪个文件 | 注意 |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| 给首页加一块新数据 | `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"为例)
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/、数据在 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 类型检查)零错误
> 本规范为项目强约束:**新增与修改的代码必须严格遵循**。发现不符合结构约定的代码,应指出问题并要求按规范调整,而不是放任通过。