* fix(ci): fix release workflow artifact upload issue - Split artifact upload into 3 separate steps (Chrome, Firefox, Source) - Update download steps to match new artifact names - Add if-no-files-found: error for better error handling - Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern * docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明 * docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明 * fix: remove unnecessary animations from StorageCleaner page - Remove entrance animations (animate-in, fade-in, zoom-in) from all components - Remove scale effect (active:scale-[0.99]) from clean button - Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle - Remove bouncing animation from error icon - Remove pulsing animation from warning banner - Keep animate-spin on button loading spinner as functional indicator * fix: remove unnecessary animations from RightClickRestorer page * fix: remove all unnecessary animations from all pages - Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed) - Remove decorative transition effects (transition-all, transition-colors) from all page components - Remove scale effects (active:scale-95) from buttons - Remove bounce animations (animate-bounce) from icons - Keep functional animate-spin on loading spinners as they provide essential loading feedback Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics * docs: 添加项目文档(copilot-instructions、编码规范、各目录 README) - 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范 - 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节) - 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils - 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes Remove hand-written variantClasses/sizeClasses that duplicated shadcn's buttonVariants. Now extends ButtonProps and imports buttonVariants from components/ui/button for consistent styling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors Replace hardcoded red-200/red-50/red-600 with border-destructive, bg-destructive/5, text-destructive. Use variant='destructive' on Button. Align error log styling with PageErrorBoundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls Use existing translation keys for clear success, copy success, and copy error messages instead of hardcoded Chinese text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(QrCodePreview): use shadcn Button instead of manual button styles Replace hand-written button class strings with shadcn Button component using outline and default variants. Removes ~15 lines of duplicated Tailwind classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(GlobalSnackbar): clarify relationship with sonner toast Add note explaining when to use GlobalSnackbar (provider context, severity levels, custom positioning) vs sonner toast (simple one-off messages). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ImageUploader): wrap handleClearFile with useCallback Prevents unnecessary re-renders by memoizing the callback, consistent with handleFileChange which already uses useCallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(SwitchButtonGroup): remove redundant containerPadding conditional Both branches of the ternary returned 'p-1'. Inline the constant directly into the className string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add missing openExtensionPage import The function was called in handleOpenInTab but never imported from utils/chromeTabs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add selectedIndex boundary protection in keyboard nav Guard against out-of-bounds access when search results change during keyboard navigation. Check selectedIndex < totalItems before accessing arrays. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): remove unnecessary empty-dep useMemo getEntryPointType() is a simple config getter. Replace useMemo(() => ..., []) with a direct call — the empty dep array made the memo pointless. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(i18n): add missing translation keys for error boundaries and copy messages Add keys for errorBoundary, pageErrorBoundary, router, and messages.copyEmpty in both zh and en locales. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls Use useTranslation('common') for tooltip, copy empty/success/error toast messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and refresh button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and retry button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): replace hardcoded Chinese with i18n t() calls Use useTranslation('common') for 404 page title and description with entryPointType interpolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码 - 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm) - 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232) - 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools) - MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式 - 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码 (FileMode.tsx, ImageMode.tsx 及其测试文件) - 更新 README.md 移除对已删除文件的引用 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置 * refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑 * fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常 * refactor: 迁移 i18n 系统从 react-i18next 到 chrome.i18n - 移除 react-i18next、i18next 及相关依赖 - 删除旧的 i18n/ 目录和 useLazyTranslation 工具 - 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage) - 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key) - 批量更新 39+ 组件文件的导入和翻译调用 - 转换翻译键格式:namespace:key → namespace_key - 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage - 更新 vitest.setup.ts mock 加载实际翻译文本 - 修复 11 个测试文件的断言以匹配中文翻译 - TypeScript、ESLint、547 项测试全部通过 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: 移除语言切换按钮 chrome.i18n 语言由浏览器设置决定,不支持运行时切换,移除 TopBar 中的语言切换按钮及相关逻辑。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: 移除英文翻译,仅保留中文 删除 public/_locales/en/ 目录,清理 chromeI18n.ts 中未使用的语言切换相关导出。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: 清理未使用的 i18n key(322 → 245) 移除 77 个未被源码引用的翻译 key,包括: - jsonFormat_* (17): 未接入的子模式标题和按钮 - buttons_* (15): 代码使用 common: 命名空间格式,直接 key 未引用 - qrCode_* (14): 未使用的页面标题和状态提示 - timestamp/common/storageCleaner/jsonDiff 等零散未用 key 保留了通过模板字面量动态引用的 key(storageCleaner_options_*、common_buttons_themeMode_*、jsonFormat_*InputPlaceholder 等)。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: reorganize directory structure into src/ Move all source code directories into src/ for cleaner project structure: - pages/, components/, utils/, config/, providers/, types/, lib/, assets/, entrypoints/ → src/ - Use WXT srcDir config to resolve @/ alias to src/ - Update tsconfig, vitest, eslint, tailwind configs - Remove scattered README.md files from subdirectories - Update documentation (AGENTS.md, CODING_STANDARDS.md, README.md) - Fix pre-existing lint error in RouterProvider.tsx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove meaningless comments Remove 50+ noise comments across the codebase: - AI-generated verbose prose (💡 emoji, '超进化', '大闸', etc.) - Comments that restate what the code obviously does - Comments about deleted code - Import-level noise comments - Overly verbose Chinese section markers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: clean up unused code and simplify imports - Remove unused imports and variables across 35 files - Simplify component logic and remove dead code - Clean up test files by removing unnecessary setup - Streamline CI workflow configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: remove HTML-Markdown conversion features (htmlToMarkdown & markdownToHtml) Removed both conversion tools and all related code: - Deleted page components: HtmlToMarkdown, MarkdownToHtml - Deleted utility functions: htmlToMarkdown.ts, markdownToHtml.ts - Deleted unit tests for both utilities - Removed feature configs and PageType entries from storage.d.ts - Removed preview mode types and StorageSchema keys - Removed all i18n translation keys from messages.json - Removed vendor-markdown chunk from wxt.config.ts - Removed marked dependency from package.json - Updated feature count in tests (11 -> 9, page order 10 -> 8) - Updated README, AGENTS.md, and copilot-instructions.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: redesign Dashboard layout with search and recently used tools - Add search input at top to filter tools by name/description - Add 'recently used' section showing last 3 used tools as compact chips - Add 'all tools' section header for the tool card grid - Track tool usage via new 'app/recentlyUsedTools' storage key - Update navigateTo to record recently used tools (max 3, LRU order) - Add i18n keys: dashboard_searchPlaceholder, dashboard_recentlyUsed, dashboard_allTools Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: replace ToolCard grid with compact icon+name grid on Dashboard - Replace large ToolCard layout with dense grid (3-6 columns responsive) - Each item shows icon + tool name in a compact tile - Hover highlights icon and text with smooth transitions - Remove ToolCard import from Dashboard (component retained for reuse) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove redundant search input from Dashboard (header already has one) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: optimize TopBar search with Cmd+K shortcut and improved styling - Add global Cmd/Ctrl+K keyboard shortcut to focus search input - Show keyboard shortcut hint (⌘K) in search input when empty - Improve input styling: rounded-lg, softer borders, focus transitions - Improve dropdown: rounded-lg, larger shadow, better spacing - Larger result icons (h-8 w-8) with rounded-lg containers - Refined history section with uppercase label and muted icon - Search icon fades in on focus via group-focus-within Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: improve search history to show recently used tools with direct navigation - Store feature keys (PageType) in history instead of translated labels - Show icon + name + description in history items (same as search results) - Click or Enter on history item navigates directly to the tool page - Remove History icon import (no longer needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: useI18n fallback for empty string translations from chrome.i18n * refactor: 移除设置页面 - 删除 options 入口点目录及相关文件 - 移除 wxt.config.ts 中 options_ui 配置 - 移除 TopBar 组件中的设置按钮和 onOpenOptions prop - 移除 popup/sidepanel App.tsx 中的 handleOpenOptions - 移除相关测试中的设置页面引用 - 移除 messages.json 中设置相关翻译键 - 通过 lint、typecheck、test 验证 * docs: 更新文档,移除设置页面引用 - README.md: 移除 entrypoints/options/ 目录说明 - AGENTS.md: 更新项目结构描述,移除 options/ 入口点 * docs: add comprehensive page development standards (CODING_STANDARDS.md §11) - 新增 §11 页面开发规范(10 个子节),覆盖目录结构分4级、入口组件命名统一为 Index、业务 Hook 内部结构顺序、常量/子组件/存储键/模式切换/布局约定等规范 - 新增 §11.9 页面开发检查清单(10 步逐项确认) - 更新 AGENTS.md 和 copilot-instructions.md 同步引用新规范章节 - 修正 useLazyTranslation → useI18n 与实际代码保持一致 * refactor(pages): apply UI+Hook separation and unify component naming - Dashboard, RightClickRestorer: rename page component to Index - Jwt: extract useJwt.ts hook + JwtSection.tsx sub-component (127→76 lines) - TextStatistics: extract useTextStatistics.ts hook (63→48 lines) - JsonTools: extract useJsonTools.ts hook + constants.ts (199→109 lines) - All pages now follow CODING_STANDARDS.md §11 pattern * refactor(StorageCleaner): improve style consistency and checkbox alignment - Group storage options, auto-refresh toggle, and clean button into unified card - Remove card wrappers from StorageOptionsGrid and AutoRefreshToggle (now sections within parent card) - Align OptionItem and Select All checkboxes with consistent px-3.5 padding - Distinguish indeterminate vs checked checkbox states visually - Standardize spacing (space-y-4), add select-none to page root - Fix import path for StorageCleanerConfirm to relative ./ * fix(StorageCleaner, Base64Converter): fix layout padding and checkbox alignment - StorageCleaner: add px-3.5 to grid container to prevent content sticking to border - StorageCleaner: adjust select-all bar to pl-3.5 pr-7 for checkbox alignment with grid items - Base64Converter: add px-2 to TextMode and Base64ConverterSection for consistent spacing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add initial CLAUDE.md file with reference to @AGENT.md * docs: add comprehensive visual style guide Add VISUAL_STYLE_GUIDE.md to standardize UI development: - Design principles and keywords - Complete color system (CSS variables, semantic usage) - Typography hierarchy and patterns - Spacing, layout, and container specs - Border radius and shadow system - Component specs (Button, Input, Card, Dialog, etc.) - Interaction and animation guidelines - Dark mode implementation rules - Tool color palette and assignment - Code organization standards - Anti-patterns checklist - Quick reference appendix Update .gitignore to allow tracking the style guide. * docs(AGENTS): update i18n section to reflect chrome.i18n migration - Replace react-i18next docs with chrome.i18n API documentation - Update translation file paths from i18n/locales/ to public/_locales/ - Update test mock description for chromeI18n - Remove references to useLazyTranslation, vendor-i18n chunk, and snackbar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(content): simplify content script structure - Remove messageHandler.ts (6-line pass-through facade) - Import contextMenuHandler directly from content.ts - Replace local getI18nText() with shared getMessage() from utils/chromeI18n Reduces content script files from 3 to 2 and eliminates i18n logic duplication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(RightClickRestorer): simplify setDomain logic Replace nested if/try/catch with a single ternary expression. isUnsupportedPage() already handles invalid URLs, so new URL() is safe here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(background): extract main world injection script Move the 62-line inline main world injection function from background.ts to a dedicated utils/rightClickInjection.ts file. This separates the RightClickRestorer's DOM patching logic from the service worker's message routing concerns. - background.ts: -62 lines, now delegates to imported function - utils/rightClickInjection.ts: new file with mainWorldInjectionScript() Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(background): simplify RELOAD_TAB with setTimeout Replace the 30-line chrome.alarms API implementation with a simple setTimeout for delayed tab reloads. Rationale: The only usage is a 1-second delay after storage cleaning. Service workers don't go idle within 1 second, so alarms' persistence benefit is unnecessary here. - Removed: alarm naming, create/clear, listener register/unregister - Added: 4-line setTimeout conditional Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(storageCleaner): extract executeScript helpers Extract two generic helpers to eliminate repetitive executeScript boilerplate: - runScript<T>(): for size/query operations (5 functions) - runCleanScript(): for cleanup operations (5 functions) Also add CLEAN_OPTION_KEYS constant shared by formatCleaningResult and isEmptyResult. - 428 lines → 379 lines (-49 lines) - Eliminates ~100 lines of duplicated try/catch/result parsing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(base64Converter): simplify utility functions - Mark formatFileSize() as @deprecated, delegate to formatBytes - Replace manual for-loop in base64ToBytes() with Uint8Array.from() - Replace nested loops in sniffMimeFromBytes() with Array.every() 5 insertions(+), 23 deletions(-) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: replace GlobalSnackbar with sonner toast Remove the custom 373-line GlobalSnackbar component and its Provider/Hook in favor of the already-installed sonner library. Changes: - Delete src/components/GlobalSnackbar.tsx (+ tests) - Remove SnackbarProvider from popup/App.tsx and sidepanel/App.tsx - Replace useSnackbar() calls with toast.success()/toast.error() in: - ImageUploader.tsx - useQrCode.ts - ParsePanel.tsx - LiveClock.tsx - Update test mocks to use sonner instead of GlobalSnackbar -554 lines, +43 lines (net -511 lines) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(RouterContainer): remove unnecessary useMemo and hoist constant - Replace useMemo with direct expression for animationClass - Move entryPointType to module scope (value is fixed after mount) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: remove unnecessary useMemo calls across components Replace over-cached simple computations with direct expressions: - popup/App.tsx: getEntryPointType() and routerConfig (values are stable) - Dashboard/index.tsx: visibleSet, visibleFeatures, recentFeatures (Set creation + array filter/map are cheaper than useMemo overhead) - useTextStatistics.ts: getTextStats(text) (simple string processing) - ThemeModeProvider.tsx: contextValue object (setMode is stable via useCallback) -39 lines, +28 lines (net -11 lines) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: unify formatBytes usage and simplify useContextMenuData 1. Remove formatSize/formatByteSize delegation functions: - storageCleaner.ts:formatSize (was just formatBytes wrapper) - textStatistics.ts:formatByteSize (was just formatBytes wrapper) - Update all callers to import formatBytes directly from @/utils/format 2. Simplify useContextMenuData hook: - Remove unnecessary useCallback wrapping - Inline checkAndConsumeData logic directly in useEffect - Eliminate callback->effect dependency cycle -69 lines, +67 lines (net -2 lines, but removes 2 indirection layers) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(storageCleaner): simplify architecture and improve type semantics - Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly - Rename IndexedDB label to '站点存储' for accuracy - Introduce StorageSizeInfo type to distinguish bytes vs count - Rename totalSize to totalBytes for clarity - Merge runCleanScript into runScript to reduce duplication - Rename CleaningResult.success to overallSuccess to avoid confusion - Remove unused domain state and setDomain call Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: replace require with import for JSON module in vitest.setup.ts * refactor: simplify Timestamp and RightClickRestorer Timestamp: - Extract shared utilities (msToUnit, dayjsFromTimestamp, ModeType) - Remove unnecessary useCallback/useMemo/React.memo - Remove extra info section (relative time, ISO 8601, UTC) - Fix missing translation key timestamp_unitS RightClickRestorer: - Remove website badge (右键已解锁 overlay) - Remove mouse penetration logic for media elements - Remove unnecessary useCallback - Remove redundant setIsLoading call Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(StorageCleanerConfirm.test.tsx): update IndexedDB label to Chinese translation --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
34 KiB
代码编写规范
本文档定义了 Testing Tools 浏览器扩展项目的编码规范和最佳实践。所有代码贡献者应遵循这些规范以保持代码库的一致性和可维护性。
1. TypeScript 规范
1.1 类型定义:interface vs type
interface:用于组件 Props、对象结构、Context 类型等可扩展结构type:用于联合类型、工具类型、不可扩展的类型别名
// ✅ interface — 组件 Props / 对象结构
export interface GlobalSnackbarProps {
message: string;
open: boolean;
onClose: () => void;
severity?: SnackbarSeverity;
}
// ✅ interface — 继承 HTML 属性
interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
unit: UnitType;
onUseNow: (val: number) => void;
}
// ✅ type — 联合类型
export type ThemeMode = 'light' | 'dark' | 'system';
export type PageType = 'dashboard' | 'timestamp' | 'storageCleaner' | ...;
// ✅ type — 工具类型
export type ResolvedThemeMode = 'light' | 'dark';
1.2 泛型使用
广泛使用泛型约束,结合 extends 进行类型守卫:
// ✅ 泛型 + extends 约束
export interface SwitchOption<T extends string | number = string> {
value: T;
label: React.ReactNode;
}
// ✅ 泛型 + StorageSchema 键约束
async get<K extends keyof StorageSchema>(
key: K,
defaultValue?: StorageSchema[K],
): Promise<StorageSchema[K] | undefined> { ... }
// ✅ 泛型 Hook
export const useStorageState = <K extends keyof StorageSchema>(
key: K,
defaultValue: StorageSchema[K],
validator?: (val: unknown) => val is StorageSchema[K],
) => { ... }
1.3 类型守卫
优先使用类型守卫函数(val is Type 谓词),避免 as 强转:
// ✅ 类型守卫谓词函数
const isValidMode = (v: unknown): v is ThemeMode => VALID_MODES.includes(v as ThemeMode);
const isValidPage = (page: unknown): page is PageType => {
return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page);
};
// ✅ 安全的 as 断言,仅在类型守卫验证后使用
export function isSupportedImageType(mimeType: string): boolean {
return (SUPPORTED_IMAGE_TYPES as readonly string[]).includes(mimeType);
}
1.4 导出模式
| 场景 | 导出方式 | 示例 |
|---|---|---|
| 页面组件 | export default function ComponentName() |
src/pages/Timestamp/index.tsx |
| 业务组件 | const X = React.memo(...) + export default X |
LiveClock.tsx, ResultView.tsx |
| UI 原子组件 | React.forwardRef(...) + export { X } |
src/components/ui/button.tsx |
| 工具函数 | export function xxx() |
src/utils/clipboard.ts |
| 自定义 Hook | export function useXxx() |
src/utils/useStorageState.ts |
| 类型/接口 | export interface / export type |
src/types/storage.d.ts |
// ✅ 页面组件 — default export
export default function Index() { ... }
// ✅ 需要 memo 的组件 — 箭头函数 + React.memo + displayName
const LiveClock = React.memo(({ ... }: LiveClockProps) => { ... });
LiveClock.displayName = 'LiveClock';
export default LiveClock;
// ✅ UI 组件 — forwardRef + 命名导出
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(...);
Button.displayName = 'Button';
export { Button, buttonVariants };
2. React 组件规范
2.1 组件定义方式
- 标准组件:使用
function声明 - 需要 memo 的组件:使用箭头函数 +
React.memo - 需要 ref 的组件:使用
React.forwardRef - 错误边界:使用 Class 组件(React 要求)
// ✅ 标准页面组件
export default function Index() { ... }
// ✅ 需要 memo 的组件
const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => {
...
});
LiveClock.displayName = 'LiveClock';
export default LiveClock;
// ✅ 需要 ref 的组件
const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props, ref) => {
...
});
TextInputArea.displayName = 'TextInputArea';
export default TextInputArea;
// ✅ Class 组件(仅用于 ErrorBoundary)
export class ErrorBoundary extends Component<Props, State> { ... }
2.2 Props 模式
- 使用
interface定义 Props - 继承
React.HTMLAttributes以支持原生属性透传 - 使用
Omit排除冲突属性 - 解构
className和...rest props
// ✅ 继承 HTML 属性 + className 透传
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
result: string;
mode: 'ts2dt' | 'dt2ts';
unit: UnitType;
zone: string;
showEmptyPlaceholder?: boolean;
}
// 使用时解构 className 和 rest props
const ResultView = React.memo(({
result, mode, unit, zone,
showEmptyPlaceholder = false,
className, ...props
}: ResultViewProps) => {
return <div className={cn('flex flex-col w-full', className)} {...props}>...</div>;
});
// ✅ Omit 排除冲突属性
export interface TextInputAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'
> { ... }
2.3 状态管理
- 本地状态:
useState+ 惰性初始化 - 衍生状态:
useMemo响应式计算管线 - 持久化状态:Chrome Storage +
localStorage快照 - 全局状态:React Context
// ✅ useState + 惰性初始化
const [input, setInput] = useState(() => String(Date.now()));
// ✅ useMemo 响应式计算管线(零延迟,无需手动 convert 按钮)
const conversionPipeline = useMemo(() => {
const rawInput = input.trim();
if (!rawInput) return { result: '', error: '' };
// ... 自动计算结果
}, [input, mode, unit, zone, t]);
// ✅ Chrome Storage 持久化状态
export const useStorageState = <K extends keyof StorageSchema>(
key: K, defaultValue: StorageSchema[K], validator?: ...
) => { ... }
2.4 副作用模式
- 取消标志:防止异步竞态
- ref 回调指针:保持回调最新避免依赖膨胀
- 事件监听 cleanup:始终在 cleanup 中移除监听器
- 定时器 cleanup:始终在 cleanup 中清除定时器
// ✅ 取消标志模式 — 防止异步竞态
useEffect(() => {
let cancelled = false;
storageUtil.get(THEME_MODE_KEY, 'system').then((saved) => {
if (cancelled) return;
if (isValidMode(saved)) { ... }
});
return () => { cancelled = true; };
}, [updateResolved]);
// ✅ ref 回调指针 — 保持回调最新避免依赖膨胀
const onUseNowRef = useRef(onUseNow);
useEffect(() => { onUseNowRef.current = onUseNow; }, [onUseNow]);
// ✅ setInterval + cleanup
useEffect(() => {
const tickId = setInterval(tick, 200);
return () => clearInterval(tickId);
}, [unit]);
// ✅ 事件监听 cleanup
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { ... };
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
2.5 memo / useCallback / useMemo 使用
| 场景 | 使用方式 |
|---|---|
| 高频渲染组件(列表子项、实时时钟) | React.memo |
| 事件处理函数、回调引用 | useCallback |
| 响应式计算管线、衍生数据 | useMemo |
| 避免重复创建对象/集合 | useMemo |
// ✅ React.memo — 高频更新组件
const LiveClock = React.memo(({ ... }) => { ... });
const ResultView = React.memo(({ ... }) => { ... });
// ✅ useCallback — 事件处理
const handleUseNow = useCallback((now: number) => {
if (mode === 'ts2dt') {
setInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
} else {
setInput(dayjs(now).tz(zone).format(DATE_FORMAT));
}
}, [mode, unit, zone]);
// ✅ useMemo — 避免重复创建集合
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]);
3. 导入规范
3.1 导入顺序
按来源分组,顺序如下:
- React 核心
- 第三方库(图标、UI 库等)
- 业务 Provider / Context
- 配置 / 存储
- i18n
- 本地页面组件
- UI 组件
- 工具函数 / Hook
- 类型
- 常量
// 1. React 核心
import React, { useEffect, useMemo, useRef, useState } from 'react';
// 2. 第三方库
import { ArrowLeft, ExternalLink, Globe } from 'lucide-react';
import { toast } from 'sonner';
// 3. 业务 Provider
import { useRouter } from '@/providers/RouterProvider';
import { useThemeMode } from '@/providers/ThemeModeProvider';
// 4. 配置 / 存储
import { FeatureConfig, FEATURES } from '@/config/features';
import { storageUtil } from '@/utils/chromeStorage';
// 5. i18n
import { useTranslation } from 'react-i18next';
import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/i18n';
// 6. 本地组件
import TextMode from './TextMode';
import { ZONES } from './constants';
// 7. UI 组件
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { Button } from '@/components/ui/button';
// 8. 工具函数 / Hook
import { cn } from '@/lib/utils';
import { useLazyTranslation } from '@/utils/useLazyTranslation';
// 9. 类型
import type { PageType, StorageSchema } from '@/types/storage';
3.2 路径别名
@/映射到项目根目录- 跨目录导入:使用
@/绝对别名 - 同目录导入:使用相对路径
./
// ✅ 绝对别名导入 — 跨目录
import { cn } from '@/lib/utils';
import { storageUtil } from '@/utils/chromeStorage';
import type { StorageSchema } from '@/types/storage';
import { Button } from '@/components/ui/button';
// ✅ 相对导入 — 仅限同目录
import TextMode from './TextMode';
import { ZONES } from './constants';
import { useTimestampConverter } from './useTimestampConverter';
4. 样式规范
4.1 cn() 工具函数
统一使用 cn() 合并 Tailwind 类名(来自 clsx + tailwind-merge),导入自 @/lib/utils:
import { cn } from '@/lib/utils';
// ✅ 条件类名 + 合并外部 className
<div className={cn(
'flex items-center gap-3 px-3 h-10 rounded-lg border border-border/80 bg-secondary/50',
className, // 外部传入的覆盖
)} {...props}>
// ✅ 错误状态变体
<Input className={cn(
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60',
error && 'border-destructive focus-visible:ring-destructive',
)} />
// ✅ 选中/未选中状态
className={cn(
'flex-1 inline-flex items-center justify-center font-medium whitespace-nowrap transition-all',
sizeClasses[size],
isSelected
? 'bg-background text-foreground shadow-sm font-semibold'
: 'hover:bg-background/50 hover:text-foreground/80',
buttonClassName,
)}
4.2 主题 / 暗色模式
使用 shadcn/ui 的 CSS 变量语义化类名,禁止硬编码颜色值:
// ✅ 语义化颜色 token — 亮/暗模式自适应
<div className="min-h-screen bg-background text-foreground antialiased selection:bg-primary/20">
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm">
// ✅ 暗色模式特殊处理
'fixed ... bg-white dark:bg-gray-900 p-6 ...'
// ✅ 需要固定颜色的特殊场景(如二维码白色背景保护)
<div className="p-3 bg-white rounded-lg shadow-sm border border-border/40">
常用语义化 token:
| 用途 | 类名 |
|---|---|
| 背景 | bg-background, bg-card, bg-muted, bg-secondary |
| 文字 | text-foreground, text-card-foreground, text-muted-foreground |
| 边框 | border-border, border-border/80 |
| 主色 | text-primary, bg-primary, border-primary |
| 危险 | text-destructive, bg-destructive, border-destructive |
4.3 响应式设计
移动优先,使用 sm: / md: / lg: 断点:
// ✅ Grid 自适应布局
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
// ✅ Dashboard 自动填充网格
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5 p-3.5 w-full h-auto">
// ✅ 弹性方向切换
<div className="flex flex-col sm:flex-row items-stretch gap-3 w-full">
// ✅ 内边距响应式
<div className="p-4 sm:p-6 space-y-4">
5. 错误处理
5.1 工具函数:结果对象模式
工具函数不抛异常,返回包含 hasError 和 error 字段的结果对象:
// ✅ 结果对象模式
export function markdownToHtml(markdown: string): MarkdownToHtmlResult {
try {
...
return { html, originalLength, htmlLength, hasError: false };
} catch (error) {
return {
html: '', ...
hasError: true,
error: error instanceof Error ? error.message : 'Markdown 解析失败',
};
}
}
5.2 UI 层:try-catch + Toast
UI 层异步操作使用 try-catch,通过 sonner 的 toast 显示错误:
import { toast } from 'sonner';
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(value);
toast.success('复制成功');
} catch {
toast.error('复制失败');
}
}, [value]);
5.3 Promise 异常隔离
对不关心返回值的异步操作,使用 void + .catch() 隔离异常:
// ✅ void + .catch 模式
void storageUtil.set(THEME_MODE_KEY, next).catch((err) => {
console.error('[Theme Storage Error] Failed to persistent theme state:', err);
});
// ✅ async 函数调用 + .catch
loadConfig().catch(console.error);
5.4 ErrorBoundary
在应用顶层使用 ErrorBoundary 组件捕获子组件树异常:
import ErrorBoundary from '@/components/ErrorBoundary';
<ErrorBoundary>
<RouterContainer />
</ErrorBoundary>
6. 命名规范
6.1 文件命名
| 类型 | 命名模式 | 示例 |
|---|---|---|
| 页面目录 | PascalCase | Timestamp/, Base64Converter/, StorageCleaner/ |
| 页面入口 | index.tsx |
src/pages/Timestamp/index.tsx |
| 组件文件 | PascalCase .tsx |
TopBar.tsx, LiveClock.tsx, ResultView.tsx |
| 自定义 Hook | camelCase .ts |
useTimestampConverter.ts, useStorageCleaner.ts |
| 工具函数 | camelCase .ts |
chromeStorage.ts, base64Converter.ts, clipboard.ts |
| 测试文件 | 与源文件同名 .test.ts(x) |
jwt.test.ts, SwitchButtonGroup.test.tsx |
| 类型文件 | camelCase .d.ts |
storage.d.ts |
| 常量文件 | camelCase .ts |
constants.ts |
6.2 变量 / 函数命名
// ✅ camelCase — 变量、函数、Hook
const conversionPipeline = useMemo(...);
const handleUseNow = useCallback(...);
export function useTimestampConverter(): UseTimestampConverterReturn { ... }
export function textToBase64(text: string): TextToBase64Result { ... }
// ✅ PascalCase — 组件、类型、接口
const LiveClock = React.memo(...);
export interface GlobalSnackbarProps { ... }
export type ThemeMode = 'light' | 'dark' | 'system';
// ✅ SCREAMING_SNAKE_CASE — 常量
const SEARCH_HISTORY_LIMIT = 10;
const THEME_MODE_KEY = 'app/themeMode' as const;
export const MAX_FILE_SIZE = 10 * 1024 * 1024;
export const SUPPORTED_IMAGE_TYPES = [...] as const;
// ✅ 布尔值 — is/has/should 前缀
const isControlled = controlledValue !== undefined;
const isDashboard = currentPage === 'dashboard';
const hasError = true;
6.3 事件处理函数
使用 handle 前缀命名组件内事件处理函数:
const handleUseNow = useCallback((now: number) => { ... }, []);
const handleSelectFeature = (feature: FeatureConfig) => { ... };
const handleFileChange = useCallback((file: File) => { ... }, []);
const handleClean = useCallback(async () => { ... }, []);
7. 测试规范
7.1 文件组织
测试文件放在源代码同级的 __tests__/ 目录下:
src/utils/__tests__/jwt.test.ts
src/utils/__tests__/base64Converter.test.ts
src/utils/__tests__/useStorageState.test.ts
src/components/__tests__/SwitchButtonGroup.test.tsx
src/components/__tests__/ErrorBoundary.test.tsx
src/pages/Timestamp/__tests__/index.test.tsx
7.2 describe / it 命名
describe 使用模块/函数名,it 使用中文描述行为("应该..."):
// ✅ 中文 describe + 中文 it
describe('textToBase64', () => {
it('应该编码 ASCII 文本', () => { ... });
it('应该编码中文文本', () => { ... });
it('应该编码空字符串', () => { ... });
});
// ✅ 中文 describe + 中文 it(组件测试)
describe('SwitchButtonGroup 组件', () => {
it('应渲染所有选项按钮', () => { ... });
it('应高亮当前选中的按钮', () => { ... });
it('点击未选中按钮时应触发 onChange 并传入选中值', () => { ... });
});
7.3 Mock 模式
- 使用
vi.mock()进行模块级 Mock - 使用
vi.fn()进行函数级 Mock - 使用
vi.useFakeTimers()控制时间 - 避免重复 mock
vitest.setup.ts中已有的内容(chrome API、i18n、matchMedia 等)
// ✅ 模块级 Mock
vi.mock('@/utils/chromeStorage', () => ({
storageUtil: {
get: vi.fn(),
set: vi.fn(() => Promise.resolve()),
},
}));
// ✅ 函数级 Mock + 断言
const handleChange = vi.fn();
render(<SwitchButtonGroup value="a" options={options} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /选项B/i }));
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith('b');
// ✅ 定时器 Mock
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
7.4 断言模式
使用 Testing Library 的 DOM 查询 + Vitest 匹配器:
// ✅ 语义化查询
expect(screen.getByRole('button', { name: /选项A/i })).toBeInTheDocument();
expect(screen.getByTestId('normal-content')).toHaveTextContent('正常内容');
expect(screen.queryByText('糟糕,出了点问题')).not.toBeInTheDocument();
// ✅ CSS 类断言
expect(button).toHaveClass('bg-background', 'text-foreground', 'shadow-sm');
// ✅ 异步断言
await waitFor(() => {
expect(result.current[0]).toBe(false);
expect(result.current[2]).toBe(true);
});
// ✅ renderHook 测试自定义 Hook
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
expect(result.current[0]).toBe(true);
8. 自定义 Hook 规范
8.1 命名和结构
- 使用
use前缀命名 - 定义返回值接口类型
- 添加 JSDoc 注释
// ✅ 完整的 Hook 结构
/**
* 自定义 Hook:处理右键菜单传递的数据
*
* @param options - 配置选项
* @example
* useContextMenuData({ featureKey: 'jwt', onData: handlePayload });
*/
export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void {
const checkAndConsumeData = useCallback(async () => { ... }, [featureKey, onData]);
useEffect(() => { checkAndConsumeData(); }, [checkAndConsumeData]);
}
// ✅ 返回值接口定义
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 { ... }
8.2 Hook 存放位置
- 全局通用 Hook:放在
src/utils/目录下 - 页面专属 Hook:与页面组件同目录
src/utils/useStorageState.ts — Chrome Storage 状态持久化
src/utils/useLazyTranslation.ts — i18n 懒加载
src/utils/useContextMenuData.ts — 右键菜单数据
src/utils/useDebounce.ts — 防抖
src/pages/Timestamp/useTimestampConverter.ts — 页面级 Hook
src/pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook
9. 国际化规范
9.1 翻译键格式
- 命名空间:
common(默认)、features - 翻译键格式:
namespace:key(如features:timestamp.title) - 语言:
zh(默认)、en
9.2 翻译文件结构
i18n/locales/{zh,en}/common.json — 全局通用翻译
i18n/locales/{zh,en}/features.json — 功能模块标题和描述
i18n/locales/{zh,en}/{功能名}.json — 各功能独立翻译
9.3 使用方式
// ✅ 页面组件 — 使用 useLazyTranslation
import { useLazyTranslation } from '@/utils/useLazyTranslation';
export default function Index() {
const { t } = useLazyTranslation('timestamp');
return <h1>{t('timestamp:title')}</h1>;
}
// ✅ 全局组件 — 使用 useTranslation
import { useTranslation } from 'react-i18next';
export function TopBar() {
const { t } = useTranslation(['common', 'features']);
return <span>{t('common:settings')}</span>;
}
9.4 添加新翻译
- 在
i18n/locales/{zh,en}/features.json添加功能标题和描述 - 创建
i18n/locales/{zh,en}/{功能名}.json添加功能专属翻译 - 在
utils/useLazyTranslation.ts的localeModules中注册新命名空间
10. 存储规范
10.1 StorageSchema
所有 Chrome Storage 键必须在 types/storage.d.ts 的 StorageSchema 中声明:
export interface StorageSchema {
'app/currentRoute': PageType;
'app/popupRoute': PageType;
'app/theme': string;
'app/themeMode': 'light' | 'dark' | 'system';
'storageCleaner/preferences': StorageCleanerPreferences;
// ...
}
10.2 存储操作
使用 utils/chromeStorage.ts 的类型安全封装:
import { storageUtil } from '@/utils/chromeStorage';
import type { StorageSchema } from '@/types/storage';
// ✅ 读取
const theme = await storageUtil.get('app/theme', 'default');
// ✅ 写入
await storageUtil.set('app/theme', 'dark');
// ✅ 删除
await storageUtil.remove('app/theme');
10.3 持久化状态 Hook
使用 useStorageState 自动同步 Chrome Storage:
import { useStorageState } from '@/utils/useStorageState';
const [themeMode, setThemeMode, isInitialized] = useStorageState(
'app/themeMode',
'system',
isValidMode, // 可选的类型守卫
);
11. 页面开发规范
11.1 目录结构(按复杂度分级)
简单页面(单一功能,无子模式)
适用于 Timestamp、Jwt、TextStatistics、RightClickRestorer 等:
src/pages/FeatureName/
├── index.tsx # 页面入口组件(default export)
├── useFeatureName.ts # 业务逻辑 Hook(命名导出)
├── constants.ts # 常量定义(可选,命名导出)
├── SubComponent.tsx # 子组件(可选,default export)
└── __tests__/
└── index.test.tsx # 页面集成测试
中等页面(含多个子模式/标签页切换)
适用于 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)
// ✅ 正确
export default function Index() { ... }
// ❌ 错误 — 命名不一致
export default function RightClickRestorerPage() { ... }
export default function DashboardPage() { ... }
组件职责
index.tsx 只负责三件事:
- 获取翻译函数(
useI18n) - 调用业务 Hook 获取状态和操作方法
- 渲染 UI 布局(纯展示,无业务逻辑)
// ✅ 标准页面入口模板
import { useI18n } from '@/utils/chromeI18n';
import { useFeatureName } from './useFeatureName';
export default function Index() {
const { t } = useI18n('featureName');
const { state, actions } = useFeatureName();
return (
<div className="p-4 w-full flex flex-col space-y-4 select-none">
{/* 纯 UI 渲染 */}
</div>
);
}
禁止在 index.tsx 中编写的内容
- ❌
useState/useMemo/useCallback(应放在 Hook 中) - ❌ 数据转换/格式化逻辑
- ❌ 异步请求/副作用
- ❌ 超过 3 行的条件判断逻辑
11.3 业务 Hook 规范(useFeatureName.ts)
命名
- 文件名:
useXxx.ts(驼峰命名) - Hook 函数名:
useXxx() - 返回值接口:
UseXxxReturn
// ✅ 标准 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 内部结构(推荐顺序)
export function useFeatureName(): UseFeatureNameReturn {
// 1. i18n
const { t } = useI18n('featureName');
// 2. 基础 state(useState)
const [mode, setMode] = useState<Mode>('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 中实现:
// ✅ 防抖管道 — 在 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数组派生联合类型
// ✅ 标准常量文件
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 模式
// ✅ 继承 HTML 属性 + 业务 Props
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
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 格式:{功能名}/{用途}。
// ✅ 正确
'base64Converter/pageMode';
'base64Converter/fileMode/direction';
'jsonTools/pageMode';
'qrCode/urlExpanded';
// ❌ 错误
'base64ConverterPageMode';
'json_tools_page_mode';
在 types/storage.d.ts 的 StorageSchema 中声明所有键。
11.7 模式切换通用模式
当页面有多个子模式(标签页切换),统一使用以下模式:
// ✅ 标准模式切换
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 (
<div className="p-4 w-full flex flex-col space-y-4 select-none">
<SwitchButtonGroup
value={pageMode}
options={[
{ value: 'modeA', label: t('feature:modeA') },
{ value: 'modeB', label: t('feature:modeB') },
]}
onChange={(v: PageMode) => setPageMode(v)}
size="small"
/>
{pageMode === 'modeA' ? <PanelA /> : <PanelB />}
</div>
);
}
11.8 页面布局约定
- 所有页面根元素使用统一的外层容器:
<div className="p-4 w-full flex flex-col space-y-4 select-none"> - 不需要
<div className="min-h-screen bg-background ...">— 该样式已由AppRoot提供 - 不需要
min-h-[500px]或固定高度(除非确有必要) - 卡片容器:
rounded-xl border border-border bg-card text-card-foreground shadow-sm - 使用
space-y-4管理纵向间距,不要手动mb-4
11.9 页面开发检查清单
新增功能页面时,逐项确认:
- ✅ 在
types/storage.d.ts添加PageType联合类型 - ✅ 在
config/features.tsx注册FEATURES配置(key、labelKey、icon、三种渲染模式组件) - ✅ 创建页面目录,使用
Index作为组件名 - ✅ 业务逻辑提取到
useXxx.tsHook(index.tsx 不超过 150 行) - ✅ 需要持久化的 UI 状态使用
useStorageState - ✅ 常量 ≥3 个时提取到
constants.ts - ✅ 在
i18n/locales/{zh,en}/添加翻译 - ✅ 创建
__tests__/index.test.tsx测试文件 - ✅ 如需新权限,更新
wxt.config.ts的manifest.permissions - ✅ 运行
npm run lint && npm run typecheck && npm run test全部通过
11.10 目录职责总览
| 目录 | 职责 |
|---|---|
src/config/ |
应用配置(功能定义、路由映射) |
src/entrypoints/ |
扩展入口点(popup、options、sidepanel、background、content) |
src/pages/ |
功能页面组件(懒加载) |
src/components/ |
可复用 UI 组件 |
src/components/ui/ |
shadcn/ui 基础组件(button、dialog、select 等) |
src/providers/ |
React Context(Router、Theme 等) |
src/hooks/ |
自定义 React Hooks |
src/utils/ |
工具函数与服务抽象 |
src/types/ |
TypeScript 类型声明 |
src/lib/ |
通用工具函数(cn、utils) |
public/ |
静态资源 |
12. 代码风格
12.1 Prettier 配置
- 行宽:100 字符
- 引号:单引号
- 尾逗号:all
- 换行符:LF
- 分号:是
12.2 ESLint 规则
- 禁止使用
any(测试文件除外) - 未使用变量/参数:使用
_前缀(如_unused) - React 19 JSX Runtime:无需手动导入 React
- 使用
typescript-eslint的projectService: true
12.3 注释规范
- 文件级注释:使用 JSDoc
@module格式(如GlobalSnackbar.tsx) - 函数注释:使用 JSDoc,包含
@param、@returns、@example - 行内注释:仅在需要澄清复杂逻辑时使用
- 禁止注释显而易见的代码