From e33761381deb846bb3d709dfa0edf52af8d5b083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Thu, 28 May 2026 23:09:14 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20add=20comprehensive=20page=20developmen?= =?UTF-8?q?t=20standards=20(CODING=5FSTANDARDS.md=20=C2=A711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 §11 页面开发规范(10 个子节),覆盖目录结构分4级、入口组件命名统一为 Index、业务 Hook 内部结构顺序、常量/子组件/存储键/模式切换/布局约定等规范 - 新增 §11.9 页面开发检查清单(10 步逐项确认) - 更新 AGENTS.md 和 copilot-instructions.md 同步引用新规范章节 - 修正 useLazyTranslation → useI18n 与实际代码保持一致 --- .github/CODING_STANDARDS.md | 333 +++++++++++++++++++++++++++++++- .github/copilot-instructions.md | 4 +- AGENTS.md | 14 +- 3 files changed, 334 insertions(+), 17 deletions(-) diff --git a/.github/CODING_STANDARDS.md b/.github/CODING_STANDARDS.md index 46c6bee..6cacda1 100644 --- a/.github/CODING_STANDARDS.md +++ b/.github/CODING_STANDARDS.md @@ -762,22 +762,335 @@ const [themeMode, setThemeMode, isInitialized] = useStorageState( --- -## 11. 文件组织 +## 11. 页面开发规范 -### 11.1 页面组件结构 +### 11.1 目录结构(按复杂度分级) + +#### 简单页面(单一功能,无子模式) + +适用于 Timestamp、Jwt、TextStatistics、RightClickRestorer 等: ``` src/pages/FeatureName/ -├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件) -├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑) -├── constants.ts # 常量定义(可选) -├── LiveClock.tsx # 子组件(可选) -├── ResultView.tsx # 子组件(可选) -└── __tests__/ # 测试文件 - └── index.test.tsx +├── index.tsx # 页面入口组件(default export) +├── useFeatureName.ts # 业务逻辑 Hook(命名导出) +├── constants.ts # 常量定义(可选,命名导出) +├── SubComponent.tsx # 子组件(可选,default export) +└── __tests__/ + └── index.test.tsx # 页面集成测试 ``` -### 11.2 目录职责 +#### 中等页面(含多个子模式/标签页切换) + +适用于 Base64Converter、StorageCleaner 等: + +``` +src/pages/FeatureName/ +├── index.tsx # 页面入口(模式路由 + 顶层布局) +├── useFeatureName.ts # 业务逻辑 Hook(命名导出) +├── SubModeA.tsx # 子模式组件 +├── SubModeB.tsx # 子模式组件 +├── SubComponent.tsx # 可复用子组件 +└── __tests__/ + ├── index.test.tsx + └── SubModeA.test.tsx +``` + +#### 复杂页面(Context + 多组件协作) + +适用于 QrCode、JsonTools 等: + +``` +src/pages/FeatureName/ +├── index.tsx # 页面入口(Provider + 布局) +├── types.ts # 页面专属类型定义 +├── constants.ts # 常量(可选) +├── contexts/ # React Context 定义 +│ └── FeatureContext.ts +├── hooks/ # 页面专属 Hooks +│ └── useFeature.ts +├── components/ # 页面专属子组件 +│ ├── PanelA.tsx +│ └── PanelB.tsx +└── __tests__/ + ├── index.test.tsx + └── useFeature.test.ts +``` + +#### 特殊情况(单个文件即可) + +功能极简的页面(如 Dashboard),仅需 `index.tsx` 一个文件。当 `index.tsx` 超过 **150 行**时,应拆分为 UI + Hook 模式。 + +--- + +### 11.2 页面入口组件(`index.tsx`)规范 + +#### 组件命名 + +- 页面入口组件**统一使用 `Index` 作为函数名**,通过 `export default` 导出 +- 使用 `export default function Index()` 而非匿名默认导出 +- **禁止**混用 `XxxPage` 命名(当前 `RightClickRestorerPage`、`DashboardPage` 不合规范,应统一为 `Index`) + +```typescript +// ✅ 正确 +export default function Index() { ... } + +// ❌ 错误 — 命名不一致 +export default function RightClickRestorerPage() { ... } +export default function DashboardPage() { ... } +``` + +#### 组件职责 + +`index.tsx` 只负责三件事: + +1. **获取翻译函数**(`useI18n`) +2. **调用业务 Hook** 获取状态和操作方法 +3. **渲染 UI 布局**(纯展示,无业务逻辑) + +```typescript +// ✅ 标准页面入口模板 +import { useI18n } from '@/utils/chromeI18n'; +import { useFeatureName } from './useFeatureName'; + +export default function Index() { + const { t } = useI18n('featureName'); + const { state, actions } = useFeatureName(); + + return ( +
+ {/* 纯 UI 渲染 */} +
+ ); +} +``` + +#### 禁止在 `index.tsx` 中编写的内容 + +- ❌ `useState` / `useMemo` / `useCallback`(应放在 Hook 中) +- ❌ 数据转换/格式化逻辑 +- ❌ 异步请求/副作用 +- ❌ 超过 3 行的条件判断逻辑 + +--- + +### 11.3 业务 Hook 规范(`useFeatureName.ts`) + +#### 命名 + +- 文件名:`useXxx.ts`(驼峰命名) +- Hook 函数名:`useXxx()` +- 返回值接口:`UseXxxReturn` + +```typescript +// ✅ 标准 Hook 结构 +export interface UseTimestampConverterReturn { + mode: 'ts2dt' | 'dt2ts'; + input: string; + result: string; + error: string; + setMode: (mode: 'ts2dt' | 'dt2ts') => void; + setInput: (value: string) => void; + handleUseNow: (now: number) => void; +} + +export function useTimestampConverter(): UseTimestampConverterReturn { + // 所有业务逻辑在此 +} +``` + +#### Hook 内部结构(推荐顺序) + +```typescript +export function useFeatureName(): UseFeatureNameReturn { + // 1. i18n + const { t } = useI18n('featureName'); + + // 2. 基础 state(useState) + const [mode, setMode] = useState('default'); + const [input, setInput] = useState(''); + + // 3. 持久化 state(useStorageState) + const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'default', isValidMode); + + // 4. 衍生数据(useMemo)— 响应式计算管线 + const result = useMemo(() => { + // 自动计算,无需手动点击"转换"按钮 + }, [input, mode]); + + // 5. 事件处理(useCallback) + const handleAction = useCallback(() => { ... }, [deps]); + + // 6. 副作用(useEffect)— 防抖、初始化、清理 + useEffect(() => { ... }, [deps]); + + // 7. 右键菜单数据(页面需要时) + useContextMenuData({ featureKey: 'featureName', onData: handleContextMenuData }); + + // 8. 返回 + return { mode, input, result, setMode, setInput, handleAction }; +} +``` + +#### 防抖模式 + +当输入框需要防抖时,在 Hook 中实现: + +```typescript +// ✅ 防抖管道 — 在 useMemo 前定义 +const [input, setInput] = useState(''); +const [debouncedInput, setDebouncedInput] = useState(''); + +useEffect(() => { + const handle = setTimeout(() => setDebouncedInput(input), 250); + return () => clearTimeout(handle); +}, [input]); + +// 后续 useMemo 使用 debouncedInput 而非 input +const result = useMemo(() => compute(debouncedInput), [debouncedInput]); +``` + +--- + +### 11.4 常量文件规范(`constants.ts`) + +- 仅在常量超过 **3 个**或需要**导出类型**时创建 +- 使用 `as const` 确保字面量类型 +- 从 `as const` 数组派生联合类型 + +```typescript +// ✅ 标准常量文件 +export const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss'; + +export const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const; + +export type UnitType = 'ms' | 's'; +export type ZoneType = (typeof ZONES)[number]; +``` + +--- + +### 11.5 子组件规范 + +#### 何时拆分子组件 + +- `index.tsx` 超过 **150 行** +- 存在可复用的 UI 片段(如卡片、面板、结果展示区) +- 需要 `React.memo` 优化的高频渲染区域 + +#### 子组件 Props 模式 + +```typescript +// ✅ 继承 HTML 属性 + 业务 Props +interface ResultViewProps extends React.HTMLAttributes { + result: string; + mode: 'ts2dt' | 'dt2ts'; + showEmptyPlaceholder?: boolean; +} + +// ✅ 使用 React.memo + displayName +const ResultView = React.memo( + ({ result, mode, showEmptyPlaceholder = false, className, ...props }: ResultViewProps) => { + const { t } = useI18n('featureName'); + // ... + }, +); +ResultView.displayName = 'ResultView'; +export default ResultView; +``` + +#### 子组件内可以使用 Hook + +子组件可以独立调用 `useI18n`、`useSnackbar` 等全局 Hook,**不需要**通过 props 从父组件传递翻译函数或 toast 方法。 + +--- + +### 11.6 存储键命名规范 + +页面使用的 Storage 键必须遵循 kebab-case 格式:`{功能名}/{用途}`。 + +```typescript +// ✅ 正确 +'base64Converter/pageMode'; +'base64Converter/fileMode/direction'; +'jsonTools/pageMode'; +'qrCode/urlExpanded'; + +// ❌ 错误 +'base64ConverterPageMode'; +'json_tools_page_mode'; +``` + +在 `types/storage.d.ts` 的 `StorageSchema` 中声明所有键。 + +--- + +### 11.7 模式切换通用模式 + +当页面有多个子模式(标签页切换),统一使用以下模式: + +```typescript +// ✅ 标准模式切换 +const VALID_MODES = ['modeA', 'modeB'] as const; +type PageMode = (typeof VALID_MODES)[number]; + +const isValidMode = (val: unknown): val is PageMode => + typeof val === 'string' && (VALID_MODES as readonly string[]).includes(val); + +export default function Index() { + const { t } = useI18n('featureName'); + const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'modeA', isValidMode); + + return ( +
+ setPageMode(v)} + size="small" + /> + {pageMode === 'modeA' ? : } +
+ ); +} +``` + +--- + +### 11.8 页面布局约定 + +- 所有页面根元素使用统一的外层容器: + ``` +
+ ``` +- 不需要 `
` — 该样式已由 `AppRoot` 提供 +- 不需要 `min-h-[500px]` 或固定高度(除非确有必要) +- 卡片容器:`rounded-xl border border-border bg-card text-card-foreground shadow-sm` +- 使用 `space-y-4` 管理纵向间距,不要手动 `mb-4` + +--- + +### 11.9 页面开发检查清单 + +新增功能页面时,逐项确认: + +1. ✅ 在 `types/storage.d.ts` 添加 `PageType` 联合类型 +2. ✅ 在 `config/features.tsx` 注册 `FEATURES` 配置(key、labelKey、icon、三种渲染模式组件) +3. ✅ 创建页面目录,使用 `Index` 作为组件名 +4. ✅ 业务逻辑提取到 `useXxx.ts` Hook(index.tsx 不超过 150 行) +5. ✅ 需要持久化的 UI 状态使用 `useStorageState` +6. ✅ 常量 ≥3 个时提取到 `constants.ts` +7. ✅ 在 `i18n/locales/{zh,en}/` 添加翻译 +8. ✅ 创建 `__tests__/index.test.tsx` 测试文件 +9. ✅ 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions` +10. ✅ 运行 `npm run lint && npm run typecheck && npm run test` 全部通过 + +--- + +### 11.10 目录职责总览 | 目录 | 职责 | | -------------------- | ------------------------------------------------------------ | diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4fbef56..ad75f01 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -56,7 +56,7 @@ Router 同时使用 `chrome.storage.local` 持久化和 `localStorage` 快照来 ### 页面组件模式 -功能页面遵循 **UI + Hook 分离** 模式: +功能页面遵循 **UI + Hook 分离** 模式,详见 [CODING_STANDARDS.md § 11](./CODING_STANDARDS.md#11-页面开发规范): ``` pages/FeatureName/ @@ -65,7 +65,7 @@ pages/FeatureName/ └── constants.ts # 常量定义(可选) ``` -- 页面组件调用 `useLazyTranslation('featureName')` 获取翻译函数 +- 页面组件调用 `useI18n('featureName')` 获取翻译函数 - Hook 负责所有状态管理,通过返回值暴露给页面 - 子组件可进一步拆分(如 `LiveClock.tsx`、`ResultView.tsx`) diff --git a/AGENTS.md b/AGENTS.md index 82efa64..739d438 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,18 +59,22 @@ public/ # 静态资源(图标、_locales 等) ### 页面组件模式 -典型功能页面遵循 **UI + Hook 分离** 模式: +典型功能页面遵循 **UI + Hook 分离** 模式。详见 [CODING_STANDARDS.md § 11](./.github/CODING_STANDARDS.md#11-页面开发规范)。 ``` src/pages/FeatureName/ -├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件) +├── index.tsx # 页面 UI(纯展示,仅负责渲染布局) ├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑) -└── constants.ts # 常量定义 +├── constants.ts # 常量定义(可选,≥3 个常量时创建) +└── __tests__/ + └── index.test.tsx ``` -- 页面组件调用 `useLazyTranslation('featureName')` 获取翻译函数 +- 页面入口组件统一命名为 `Index`,通过 `export default function Index()` 导出 - Hook 负责所有状态管理和业务逻辑,通过返回值暴露给页面 -- 子组件可进一步拆分(如 `LiveClock.tsx`、`ResultView.tsx`) +- 子组件可以独立调用 `useI18n`、`useSnackbar` 等全局 Hook +- 当 `index.tsx` 超过 150 行时,必须拆分为 UI + Hook 模式 +- 复杂页面可增加 `contexts/`、`hooks/`、`components/` 子目录 ## 关键架构决策