Commit 9c433a9f by chunhong.mu

docs(root): 为每个子包补充目录级 CLAUDE.md 并加作用域提醒 hook

根 CLAUDE.md 一份大文件不够精准:改 site 时不需要 SDK 规则,改 mini 时
需要的 pages.json/域名白名单等约定又放不进去。拆成 7 份目录级文件,
各自只写该包独有且容易写错的内容,不重复根文件。

新增:
- packages/{admin,web,mini,site}/CLAUDE.md
- common/{sdk,utils,vue-kit}/CLAUDE.md

根 CLAUDE.md 加「动手前必读」索引表(路径 → 必读文件 → 覆盖要点),
并在仓库结构树里标出这些文件。

为保证实际生效(不只依赖文档里的一句约定),加 PreToolUse hook:
scripts/claude-scope-hint.mjs 在 Edit/Write/MultiEdit 命中子包文件时,
通过 hookSpecificOutput.additionalContext 注入「该包有独立 CLAUDE.md」
与一句红线摘要。异常输入静默放行,不阻塞开发。

eslint 忽略范围由 CLAUDE.md 扩为 **/CLAUDE.md:markdown 里的 ```ts
片段会被当成真实模块校验(no-unused-expressions 等),与 ARCHITECTURE.md
早先的处理方式一致。

顺带修正根 CLAUDE.md 里 admin 的技术栈描述:antd 是声明未使用,
不应写成技术栈的一部分。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parent 93364cbc
......@@ -51,5 +51,18 @@
"Bash(curl -sI http://localhost:*)"
],
"defaultMode": "acceptEdits"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node scripts/claude-scope-hint.mjs"
}
]
}
]
}
}
......@@ -38,15 +38,23 @@
```
frontend-hub/
├── CLAUDE.md # 本文件:全局规则 + 各包 CLAUDE.md 索引
├── common/ # 跨项目复用代码(包名 @common/*)
│ ├── utils/ # 纯函数:common / date-time / mask / money / regex / constants
│ │ └── CLAUDE.md # ← 改动前必读
│ ├── vue-kit/ # Vue 相关:composables(useLoading、useWindowWidth)
│ │ └── CLAUDE.md # ← 改动前必读
│ └── sdk/ # 统一业务平台 SDK 封装(uniplat-sdk + HTTP 适配 + Token)
│ └── CLAUDE.md # ← 改动前必读
├── packages/ # 具体项目
│ ├── admin/ # 管理后台(Vue 3 + Vite SPA + Ant Design Vue)
│ ├── admin/ # 管理后台(Vue 3 + Vite SPA)
│ │ └── CLAUDE.md # ← 改动前必读
│ ├── web/ # H5 / 客户端(Vue 3 + Vite SPA + UnoCSS)
│ │ └── CLAUDE.md # ← 改动前必读
│ ├── mini/ # 小程序(UniApp,微信/支付宝/H5)
│ └── site/ # 官网(Nuxt 3 SSR)
│ │ └── CLAUDE.md # ← 改动前必读
│ └── site/ # 官网(Nuxt 3 SSR,不用 SDK)
│ └── CLAUDE.md # ← 改动前必读
├── scripts/ # 构建与 Git 工作流脚本(不参与 lint)
├── .claude/ # Claude Code 配置与 skills
└── ARCHITECTURE.md # 架构与开发规范(权威文档)
......@@ -56,13 +64,29 @@ frontend-hub/
| 包 | 技术栈 | 路由 | 页面目录 | SDK 初始化 |
|----|--------|------|---------|-----------|
| `admin` | Vue3 + Vite + Pinia + Vue Router + Ant Design Vue | `src/router/index.ts` | `src/pages/` | `src/api/request.ts`(webAdapter) |
| `admin` | Vue3 + Vite + Pinia + Vue Router(antd 已声明未使用) | `src/router/index.ts` | `src/pages/` | `src/api/request.ts`(webAdapter) |
| `web` | Vue3 + Vite + Pinia + Vue Router + UnoCSS + SCSS | `src/router/index.ts` | `src/pages/` | `src/api/request.ts`(webAdapter) |
| `mini` | UniApp + Vue3 + Pinia + SCSS | `src/pages.json` | `src/pages/` | `src/utils/sdk.ts`(uniAdapter,懒加载单例) |
| `site` | Nuxt 3 + Pinia + UnoCSS + axios | 文件路由 | `pages/` | 无 SDK,用 `api/request.ts`(axios + runtimeConfig) |
| `site` | Nuxt 3 + Pinia + UnoCSS + **axios(不用 SDK)** | 文件路由 | `pages/` | 无 SDK,用 `api/request.ts`(axios + runtimeConfig) |
`admin` / `web` / `mini``@` 别名指向各自的 `src/``site` 用 Nuxt 的 `~/`
## ⚠️ 动手前必读:每个子包有自己的 CLAUDE.md
**修改任何子包的文件之前,先读该包根目录的 `CLAUDE.md`**(用 Read 工具显式读一遍,不要假设已加载)。里面写的是该包独有的约定和踩过的坑,与本文件不重复:
| 要改的路径 | 必读 | 该文件覆盖的关键点 |
|-----------|------|------------------|
| `packages/admin/**` | [packages/admin/CLAUDE.md](packages/admin/CLAUDE.md) | `@``src/`、路由 `meta.requiresAuth`、自实现 Toast、antd 未使用 |
| `packages/web/**` | [packages/web/CLAUDE.md](packages/web/CLAUDE.md) | 与 admin 极易混淆、UnoCSS attributify、`virtual:uno.css` |
| `packages/mini/**` | [packages/mini/CLAUDE.md](packages/mini/CLAUDE.md) | `pages.json` 登记、SDK 懒加载单例、`<image>` 域名白名单、vite 锁 5.2.8、孤儿 `base.scss` |
| `packages/site/**` | [packages/site/CLAUDE.md](packages/site/CLAUDE.md) | **环境变量是 `NUXT_PUBLIC_*` 不是 `VITE_*`**、别名 `~/`、SSR 无 window、不用 SDK |
| `common/sdk/**` | [common/sdk/CLAUDE.md](common/sdk/CLAUDE.md) | 改动要验三个包、不许硬编码、根入口不导出 mini-program、跨端守卫 |
| `common/utils/**` | [common/utils/CLAUDE.md](common/utils/CLAUDE.md) | 零框架依赖、加文件三步、导出名不许撞 |
| `common/vue-kit/**` | [common/vue-kit/CLAUDE.md](common/vue-kit/CLAUDE.md) | 需平台无关(mini 也在用)、`useWindowWidth` 是 Web 专属 |
**跨包改动**(例如改 `common/sdk` 后要动 `mini`):两份都要读。
## 分层约定(硬规则)
```
......
# @common/sdk — 统一业务平台 SDK 封装
> 通用规则见仓库根 [CLAUDE.md](../../CLAUDE.md)。本文件是改动本包时的额外约束。
## 改这里要验四个包
本包被 `admin` / `web` / `mini` 同时依赖(`site` 不依赖)。任何改动后至少跑:
```bash
pnpm -F admin type-check && pnpm -F web type-check && pnpm -F mini type-check
```
## 硬约束
### 1. 不许写死任何环境相关值
不要在本包出现具体域名、appid、client id、上报地址、凭证。需要环境相关值就加到 `SdkConfig` 上,由各包的 `config/index.ts``.env.*` 注入。
历史上这里曾硬编码过 15 个真实 `ClientId`、通行证域名、以及监控上报地址 + authorization 凭证,已全部移除:
- `ClientId` / `Product` 现在是 `type X = string`,不是枚举
- `passportUrl` / `wxDistributerUrl` 改为 `SdkConfig` 的可选字段
- 监控地址改为 `webMonitor.setupEndpoint({ url, authorization })`,未配置则不上报
### 2. 根入口不导出小程序模块
`src/index.ts` **不能** `export * from "./mini-program"`——否则 Web 包会被动引入 `uni.*` 类型,`vue-tsc` 直接失败。小程序专属能力只从子路径暴露:
```ts
import { WechatLoginService } from "@common/sdk/mini-program"
```
`package.json``exports` 已声明 `.` / `./core` / `./adapters` / `./mini-program` / `./services/*` 五个入口,加新子路径要同步登记。
### 3. core 里不许裸用平台全局量
`core/` 下的代码同时跑在浏览器与小程序里,`window` / `document` / `localStorage` / `uni` / `getCurrentPages` 都要守卫或参数化:
```ts
typeof window !== "undefined" ? window.location.href : ""
```
已修过的点:`token-manager.ts``hasToken`/`checkTokenExit``web-monitor.ts``userAgent`/`Location``sdk.ts``register()`(改成接受可选 `redirectUrl` 参数,不再直接摸 `window`)。
### 4. 不引 lodash
已刻意去掉 lodash 依赖(`forEach` 用本地实现、`isArray``Array.isArray`),保持本包依赖只有 `axios` + `uniplat-sdk`。不要为了图方便重新引入。
## 平台接口名是 camelCase(写错不会报 404)
| 功能 | 正确 | 错误 |
|------|------|------|
| 密码登录 | `loginWithPassword` | ~~`login_with_password`~~ |
| 验证码登录 | `loginWithVerifyCode` | ~~`login_with_mobile`~~ |
| 发短信 | `sendVerifyCode` | ~~`send_verifycode`~~ |
| 微信绑手机号 | `bind_phone`(在 `anonymous/system.wechat`) | — |
写错时后端返回 **HTTP 200 + `rescode: 9999` + `No signature of method`**,不是 404,Network 面板看起来是成功的。接口名收敛在 `services/login-service.ts``services/verify-service.ts` 的枚举里,业务包不要自己拼。
## 图形验证码不是接口
`verifyService.generateImage()`**同步**方法,只用随机 seed 拼出 `{uniplatApi}/general/imageToVerify/{seed}` 图片地址,加载由 `<img>` / `<image>` 完成。`uniplatApi` 为空时 SDK 的 baseUrl 会退化成 `/`,拼出同源相对路径,dev server 用 index.html 兜底返回 200 的 HTML → 破图。已加防护:地址不是 `http(s)://` 开头就返回空 `img``console.error`
## 错误契约:业务错误 reject 的是字符串
uniplat-sdk 的响应拦截器:
- 业务错误(HTTP 200 但 `rescode !== 0`)→ `Promise.reject(errorMsg)`**纯字符串**
- 网络/HTTP 错误 → `Promise.reject(error.response.data || error)`**对象**
所以 `err?.message || "失败"` 会吞掉后端真实原因。统一用 `resolveSdkError(err, fallback)``core/error.ts`)。
## 目录
```
src/
├── core/ # sdk.ts(Sdk 类 + SdkConfig + Environment)、token-manager、
│ # web-monitor、image-builder、error.ts
├── adapters/ # uni-adapter(uni.request→axios)、web-adapter(XHR)
├── services/ # createLoginService / createVerifyService 工厂
└── mini-program/ # 小程序专属:微信登录、更新管理、文件操作、导航
```
# @common/utils — 纯工具函数
> 通用规则见仓库根 [CLAUDE.md](../../CLAUDE.md)。
## 唯一硬约束:不许有任何框架/平台依赖
本包必须能在浏览器、小程序、Node 里原样运行。**禁止** import:
- `vue`(要用响应式就放 `@common/vue-kit`
- `uni` / `wx` / 任何小程序 API
- `window` / `document` / `localStorage`
- `@common/sdk`(依赖方向反了)
`package.json` 里刻意没有 `dependencies`。加依赖前先想清楚是不是真的需要——大多数情况自己写十行比引一个包好。
## 加新文件的三步
1.`src/` 下建文件(按主题分:`common` / `date-time` / `mask` / `money` / `regex` / `constants`
2.`src/index.ts``export * from "./xxx"`
3.`package.json``exports` 加子路径 `"./xxx": "./src/xxx.ts"`
漏第 3 步会导致 `import { x } from "@common/utils/xxx"` 解析失败(但从根入口 `@common/utils` 引仍能用),漏第 2 步则反之。
## 导出名不要撞
当前六个文件的导出是**全部平铺**到根入口的,所以跨文件不能同名。已有的命名边界:
- `common.ts` 占用了通用类型判断:`isArray` / `isObject` / `isString` / `isNumber` / `isEmpty` / `isNil`
- `regex.ts` 占用了格式校验:`isPhone` / `isEmail` / `isIdCard` / `isNumberString` / `isDate`
新增判断函数时注意区分这两组语义,别写出第二个 `isDate`
## 被谁依赖
四个业务包全部依赖本包。改动已有函数的行为前先全仓搜调用点:
```bash
rg "formatMoney|maskPhone|isPhone" packages/
```
# @common/vue-kit — Vue 相关复用代码
> 通用规则见仓库根 [CLAUDE.md](../../CLAUDE.md)。
## 定位
**依赖 Vue 响应式、但跨项目复用**的东西:composables、通用组件、指令。
- 纯函数(不依赖 vue)→ 放 `@common/utils`
- SDK / HTTP 相关 → 放 `@common/sdk`
- 只有一个包用得上 → 留在那个包里,不要提前下沉
`vue``peerDependencies`,本包不锁定具体版本,由使用方提供。
## 平台无关性:本包被小程序也引用
`mini` 通过 `@common/vue-kit/composables` 复用 `useLoading`。所以新增内容默认要能在小程序里跑:
- 不要用 `window` / `document` / `localStorage`
- 不要用 `uni.*`(那是 mini 自己的事)
- 要用浏览器 API 的,必须 `typeof window !== "undefined"` 守卫,并在文档里注明「仅 Web」
⚠️ 现有的 `useWindowWidth` 直接用了 `window.innerWidth` / `addEventListener`**只能在 Web 端用**。它在小程序里会崩。它没被 mini 引用,但如果有人从 `@common/vue-kit` 根入口引整包再调用它就会出问题——新增 Web 专属 composable 时请沿用「函数体内才碰 window」的写法(模块顶层不要执行),并明确标注。
## 加新 composable 的三步
1. `src/composables/use-xxx.ts`**kebab-case 文件名**,与包内既有风格一致)
2.`src/composables/index.ts` 加具名导出
3.`src/index.ts``export * from "./composables"`,通常不用改
`package.json``exports` 已声明 `.``./composables`,加新子目录(如 `./components`)要同步登记。
## 当前内容很薄,这是已知短板
只有 `useLoading``useWindowWidth` 两个。而 `admin``web` 各自维护了一份 DOM `Toast`、各自写了一遍登录表单校验与短信倒计时逻辑——这些是真正该下沉到本包的候选。下沉时注意平台无关性(`Toast` 的 DOM 实现是 Web 专属,要么留在各包,要么设计成可注入渲染层的形式)。
......@@ -90,9 +90,10 @@ export default antfu(
'**/package-lock.json',
'**/pnpm-lock.yaml',
'**/yarn.lock',
// 手工维护的中文文档:prettier 的表格对齐对 CJK 宽度处理不佳
// 手工维护的中文文档:prettier 的表格对齐对 CJK 宽度处理不佳,
// 且 markdown 里的 ```ts 代码片段会被当成真实模块校验(片段本就不完整)
'ARCHITECTURE.md',
'CLAUDE.md',
'**/CLAUDE.md',
'.claude/**',
// 由 scripts/generate-commitlint-config.ts 自动生成
'commitlint.config.mjs',
......
# admin — 管理后台(Vue 3 + Vite SPA)
> 本文件只写 admin 独有的约定。通用规则(分层、代码风格、环境变量、SDK 接口名/错误处理)见仓库根 [CLAUDE.md](../../CLAUDE.md)。
## 技术栈与入口
| 项 | 值 |
|----|----|
| 构建 | Vite 5(`vite.config.ts`) |
| 路由 | Vue Router,配置在 `src/router/index.ts` |
| 状态 | Pinia,`src/stores/` |
| 样式 | **纯手写 CSS**(scoped),无 UnoCSS、无 CSS 框架 |
| SDK | `src/api/request.ts``webAdapter` + `src/config/index.ts` |
启动:`pnpm -F admin dev`,或根目录 `pnpm dev:project --project=admin --env=test`
## `@` 别名指向 `src/`,不是包根
```ts
import { sdk } from "@/api/request" // ✅
import { sdk } from "@/src/api/request" // ❌ 改造前的历史写法,已全部修正
```
`vite.config.ts` 的 alias 与 `tsconfig.json``paths` 必须同时改,否则类型能过但运行时挂(或反之)。
## 新增页面的两步
1.`src/pages/<feature>/index.vue` 建页面
2.`src/router/index.ts``routes` 里登记,并写 `meta.title``meta.requiresAuth`
`router.beforeEach` 会用 `meta.title``document.title`(兜底取 `config.appName`),用 `meta.requiresAuth` + `TokenManager.hasToken()` 做鉴权跳转。**漏写 `requiresAuth` 等于该页免登录**
## Toast 是自己实现的,不是组件库的
`src/utils/toast.ts` 是一个注入 DOM 的轻量实现,导出 `Toast.success/error/warning/info`
⚠️ `package.json` 里声明了 `ant-design-vue`,但 `src/`**一处都没用到**(登录页是手写 CSS)。要么真正引入它并统一 UI,要么把这个依赖删掉——不要看到依赖就假设可以直接写 `<a-button>`,当前没有注册组件。
## 分层落点
```
src/
├── api/request.ts # SDK 实例 + loginService/verifyService + initToken()
├── api/auth.ts # 登录相关接口,纯委托给 SDK 服务
├── services/ # handleXxx,组合 api + store + Toast
├── stores/auth.ts # token 与 userInfo
├── config/index.ts # 唯一读 import.meta.env 的地方
└── utils/toast.ts # DOM Toast
```
`main.ts` 在挂载前调 `initToken()` 恢复本地登录态。
# mini — 小程序(UniApp + Vue 3)
> 本文件只写 mini 独有的约定。通用规则(分层、代码风格、环境变量、SDK 接口名/错误处理)见仓库根 [CLAUDE.md](../../CLAUDE.md)。
## 平台与启动
支持 `mp-weixin`(默认)、`mp-alipay``mp-baidu``mp-toutiao``h5`
```bash
pnpm -F mini dev # 微信小程序,产物在 dist/dev/mp-weixin
pnpm -F mini dev:h5 # 想在浏览器里看就用这个
pnpm -F mini dev:test # 微信 + test 环境
pnpm build:project --project=mini --env=production -p mp-weixin
```
`dev`**编译 + watch**,不是 HTTP 服务;产物要用微信开发者工具导入 `dist/dev/mp-weixin`
## 路由在 `src/pages.json`,不是 vue-router
新增页面**必须**同时做两件事,漏第二步页面无法跳转且构建不报错:
1.`src/pages/<feature>/index.vue`
2.`src/pages.json``pages` 数组里加一项
当前 7 个页面(home / login/index / login/login / settings / profile / verify / password),**无分包、无 tabBar**。要加分包用 `subPackages`
## SDK 必须走 `utils/sdk.ts` 的懒加载单例
```ts
import { getSdk, getLoginService, getVerifyService, getWechatLoginService } from "@/utils/sdk"
```
- **不要 `new Sdk()`**,也不要在模块顶层实例化——小程序启动时序下会拿到未配置 storage 的实例
- `initSdk()``main.ts``createApp()` 里调用,幂等;它负责 `TokenManager.setupStorage(new UniStorage())`(用 `uni.getStorageSync` 顶替 `localStorage`
- 小程序专属能力从子路径引:`import { WechatLoginService, chooseImage } from "@common/sdk/mini-program"`
## `<image>` 加载外域图片需要域名白名单
图形验证码是 `{VITE_APP_UNIPLAT}/general/imageToVerify/{seed}` 这样的**外域图片**。真机/开发者工具下必须:
- 在微信公众平台把该域名加入 **downloadFile 合法域名**,或
- 开发者工具里勾选「不校验合法域名」
这与 H5 不同(H5 无此限制),验证码在浏览器里能显示不代表小程序里能显示。
## 样式用 `@use`,不要 `@import`
`App.vue` 里引全局样式已改为 `@use`(Dart Sass 3.0 将移除 `@import`):
```scss
@use "@/assets/styles/common.scss";
@use "@/assets/styles/style.scss";
```
⚠️ `src/assets/styles/base.scss`**孤儿文件**——从未被任何地方 import,其 SCSS 变量也无人使用,但含 21 个 `common.scss` 里没有的选择器(`.chat-item-width``.common-dialog`、swiper 圆点覆盖等,偏业务风格)。改样式前先确认你要动的类到底在 `common.scss` 还是这个没生效的文件里。
## vite 锁定 5.2.8,不要升
`devDependencies.vite` 是精确版本 `5.2.8`(uni-app 工具链要求)。因此编译时会有 `legacy-js-api` 的 Sass 废弃警告——**这是工具链约束,不要为了消警告去升 vite 或改 `preprocessorOptions`**
## Toast 用 uni 原生
`src/composables/useToast.ts``uni.showToast` / `uni.showLoading` 的封装,导出 `show/success/error/loading/hide``useLoading``@common/vue-kit/composables` 复用。
## 分层落点
```
src/
├── api/auth.ts # 纯委托 @common/sdk 服务,不要自己拼 domainService
├── api/home.ts # 业务接口示例
├── services/ # handleXxx,组合 api + store + toast,登录三步在这里
├── stores/useUserInfo.ts
├── utils/sdk.ts # SDK 懒加载单例(本包最关键的文件)
├── config/index.ts # 唯一读 import.meta.env 的地方,含 CoreEnvir
└── pages.json # 路由
```
# site — 官网(Nuxt 3 SSR)
> 本文件只写 site 独有的约定。通用规则(分层、代码风格)见仓库根 [CLAUDE.md](../../CLAUDE.md)。
> **注意:根 CLAUDE.md 里关于 `@common/sdk`、`VITE_*` 环境变量的规则对本包不适用**,见下。
## 本包不用 @common/sdk
site 是唯一走 **axios + Nuxt runtimeConfig** 的包,只依赖 `@common/utils``@common/vue-kit`
```ts
import { request } from "~/api/request" // axios 实例,不是 SDK
```
所以根 CLAUDE.md 里「登录必须三步」「接口名 camelCase」「`resolveSdkError`」这些 SDK 相关规则在这里**不适用**
## 环境变量前缀是 `NUXT_PUBLIC_`,不是 `VITE_`
这是本包最容易踩的坑:
```bash
NUXT_PUBLIC_API_BASE_URL=http://... # ✅ 会注入 runtimeConfig.public.apiBaseUrl
VITE_APP_UNIPLAT=http://... # ❌ Nuxt 不认,永远读不到
```
读取方式也不同——不用 `import.meta.env`,用 `useRuntimeConfig()`
```ts
const runtime = useRuntimeConfig()
runtime.public.apiBaseUrl
```
`nuxt.config.ts``runtimeConfig.public` 里声明了键名与默认值,**加新变量必须先在这里登记**,否则运行时拿不到。
切环境用 `--dotenv`(不是 `--mode`):`nuxt build --dotenv .env.test`
## 别名是 `~/`,没有 `@/`
```ts
import { SITE_NAME } from "~/config" // ✅
import { SITE_NAME } from "@/config" // ❌ 本包没配这个别名
```
## SSR 环境没有 window / localStorage
服务端渲染阶段这些全局量不存在,任何浏览器 API 都要用 `import.meta.client` 守卫:
```ts
if (import.meta.client) {
const token = localStorage.getItem(TOKEN_STORAGE_KEY)
}
```
`api/request.ts` 的请求拦截器已经这么做了,注入 token 只在客户端发生。写新代码时同理。
## 目录是 Nuxt 约定,没有 src/
页面是**文件路由**——`pages/about.vue` 自动就是 `/about`,不需要注册路由表。
`composables/``utils/``components/` 会被 Nuxt **自动导入**,不用手写 `import`。这意味着:
- 新建 composable 后直接在模板里用函数名即可
-`api/``services/``config/` **不在**自动导入范围,需要显式 `import ... from "~/..."`
`config/index.ts` 是普通常量集合(`SITE_NAME``TOKEN_STORAGE_KEY``noNeedAuthPages`),不是 `SdkConfig`
## dev 残留会导致启动失败
Nuxt 会在 `.nuxt/nuxt.lock` 留锁文件。上一次 dev 没正常退出时,再启动会报 `Another Nuxt dev is already running (PID xxx)`。处理:结束该进程,并删掉 `packages/site/.nuxt/nuxt.lock`
## 依赖版本约束
- `unocss` / `@unocss/nuxt` 必须 **66.x**:Nuxt 3.21 内置 Vite 7,unocss 0.x 会在构建时报 `cssPlugins.get(...).transform.call is not a function`
- `@nuxt/schema` 的 peer 警告(`@nuxt/cli` 要 4.x,nuxt 是 3.21.8)是 Nuxt 自身版本区间问题,不影响构建与运行,不用处理
# web — H5 / 客户端(Vue 3 + Vite SPA)
> 本文件只写 web 独有的约定。通用规则(分层、代码风格、环境变量、SDK 接口名/错误处理)见仓库根 [CLAUDE.md](../../CLAUDE.md)。
## 与 admin 的区别(两者结构几乎一致,别改错包)
| | web | admin |
|---|-----|-------|
| 定位 | 面向 C 端的 H5 / 客户端 | 内部管理后台 |
| 原子化 CSS | **有 UnoCSS**`uno.config.ts`) | 无 |
| SCSS | **有**,全局样式 `src/assets/styles/index.scss` | 无 |
| 组件库 | 无 | 声明了 antd 但未使用 |
两个包的 `api/` `services/` `stores/` `config/` 文件名高度重合,改动前先确认路径前缀是 `packages/web/` 还是 `packages/admin/`
## UnoCSS
`uno.config.ts` 启用了 `presetUno()` + `presetAttributify()`,所以两种写法都可用:
```vue
<div class="flex items-center p-4" />
<div flex items-center p-4 /> <!-- attributify -->
```
`main.ts` 里必须保留 `import "virtual:uno.css"`,删了工具类会全部失效(构建不报错,只是样式没了)。
## 样式约定
- 全局样式写在 `src/assets/styles/index.scss`,由 `main.ts` 引入
- 组件内样式用 `<style scoped lang="scss">`
- 能用 UnoCSS 工具类表达的布局/间距优先用工具类,不要再写一遍 SCSS
## `@` 别名指向 `src/`
与 admin 相同,`vite.config.ts` 的 alias 和 `tsconfig.json``paths` 要同步改。
## 新增页面的两步
1. `src/pages/<feature>/index.vue`
2.`src/router/index.ts` 登记,写 `meta.title``meta.requiresAuth`(漏写 `requiresAuth` 等于免登录)
## Toast
`src/utils/toast.ts`,与 admin 是**各自一份**的 DOM 实现(尚未下沉到 `@common/vue-kit`)。改其中一个不会影响另一个。
/**
* Claude Code PreToolUse hook —— 目录级 CLAUDE.md 作用域提醒
*
* 在 Claude 修改任何子包文件之前,把「该包有自己的 CLAUDE.md」这件事注入上下文,
* 避免只依赖根 CLAUDE.md 里的一句约定(模型可能没读、或读了但已滑出上下文)。
*
* 契约:stdin 收到 PreToolUse 的 JSON(含 tool_name / tool_input),
* stdout 输出 JSON,用 hookSpecificOutput.additionalContext 注入文本。
* 任何异常都静默放行——hook 不该阻塞正常开发。
*/
import fs from "node:fs"
import path from "node:path"
/** 各包的一句话红线,避免模型即使没点开文件也至少知道最容易踩的坑 */
const REDLINES = {
"packages/admin": "`@` 指向 src/(不是包根);新增页面必须在 src/router/index.ts 登记并写 meta.requiresAuth;Toast 是 src/utils/toast.ts 自实现,antd 声明了但未使用。",
"packages/web": "结构与 admin 极易混淆,先确认路径;有 UnoCSS(attributify 已开启),main.ts 的 `virtual:uno.css` 不能删。",
"packages/mini": "路由在 src/pages.json,新增页面必须登记;SDK 只能走 utils/sdk.ts 的懒加载单例,禁止 new Sdk();小程序能力从 @common/sdk/mini-program 引;样式用 @use 不用 @import;vite 锁 5.2.8 不要升。",
"packages/site": "环境变量前缀是 NUXT_PUBLIC_(不是 VITE_),用 useRuntimeConfig() 读;别名是 ~/ 没有 @/;SSR 无 window/localStorage,需 import.meta.client 守卫;本包不用 @common/sdk。",
"common/sdk": "不许硬编码域名/appid/clientId/凭证;根入口不能导出 mini-program;core/ 内禁止裸用 window/uni,要 typeof 守卫;改完至少验 admin+web+mini 三个包的 type-check。",
"common/utils": "禁止 import vue / uni / window 等任何框架平台依赖;新增文件要同步 src/index.ts 与 package.json 的 exports。",
"common/vue-kit": "mini 也在用本包,新增内容默认要平台无关(不许裸用 window/document/uni);useWindowWidth 是 Web 专属。",
}
function main() {
let raw = ""
try {
raw = fs.readFileSync(0, "utf8")
} catch {
return
}
let payload
try {
payload = JSON.parse(raw)
} catch {
return
}
const input = payload?.tool_input || {}
// Edit/Write 用 file_path;MultiEdit 也是 file_path
const target = input.file_path || input.notebook_path || ""
if (!target) return
// 统一成仓库相对路径 + 正斜杠
const cwd = process.cwd().replace(/\\/g, "/")
let rel = target.replace(/\\/g, "/")
if (rel.toLowerCase().startsWith(cwd.toLowerCase())) {
rel = rel.slice(cwd.length).replace(/^\/+/, "")
}
// 命中哪个包
const scope = Object.keys(REDLINES).find(p => rel.startsWith(`${p}/`))
if (!scope) return
// 该包确实有 CLAUDE.md 才提醒(统一用正斜杠,Windows 下也保持可点击)
const doc = `${scope}/CLAUDE.md`
if (!fs.existsSync(path.join(process.cwd(), doc))) return
// 正在编辑的就是这份 CLAUDE.md 自身,不用提醒
if (rel === doc) return
const text = [
`[作用域提醒] 你正在修改 ${scope}/ 下的文件。`,
`该包有独立的约定文件:${doc}(若本轮尚未读过,请先用 Read 读一遍)。`,
`红线摘要:${REDLINES[scope]}`,
].join("\n")
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: text,
},
}))
}
try {
main()
} catch {
// 静默放行
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment