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

28 KiB
Raw Blame History

name, description, version
name description version
vue-project-structure cz-web 项目(Vue 3 + TypeScript + Vite 网页端站点)的目录结构与编码规范强制 skill。当在本项目中新增、创建、移动、重构或修改任何 src/ 下的代码文件(.vue/.ts/.less)、路由、store、API、组件、页面时,必须加载本 skill,先读取项目目录、再按本规范确定文件归属与写法。本项目是**网页端站点(非中后台)**:页面按名称组织(home/news/product…),接口层在 api/、数据层在 Pinia、页面只经 store 取数,样式使用**less**。凡是涉及"新文件放哪"、"页面数据怎么拿"、"页面/公共组件归属"、"命名"、"功能对应哪些文件"的任务也必须使用本 skill。 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>。

数据流架构(强约束)

页面组件 (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
// 路由加载页面(指向 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(仅会员体系需要时)
// 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/ 新闻页独有组件(筛选栏、列表项卡片)
// 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// 等)
  • 页面是否直接 import 了 api 层?(违规——数据必须经 store)
  • store 名 / api 文件名 / 页面目录名 / 类型文件名 是否与页面一致(可分层互查)
  • 导入是否使用 @/ 别名
  • 组件归属是否正确:≥2 页面共用 → components/;单页面独有 → views//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 类型检查)零错误

本规范为项目强约束:新增与修改的代码必须严格遵循。发现不符合结构约定的代码,应指出问题并要求按规范调整,而不是放任通过。