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>
This commit is contained in:
@@ -0,0 +1,821 @@
|
|||||||
|
# 代码编写规范
|
||||||
|
|
||||||
|
本文档定义了 Testing Tools 浏览器扩展项目的编码规范和最佳实践。所有代码贡献者应遵循这些规范以保持代码库的一致性和可维护性。
|
||||||
|
|
||||||
|
## 1. TypeScript 规范
|
||||||
|
|
||||||
|
### 1.1 类型定义:`interface` vs `type`
|
||||||
|
|
||||||
|
- **`interface`**:用于组件 Props、对象结构、Context 类型等可扩展结构
|
||||||
|
- **`type`**:用于联合类型、工具类型、不可扩展的类型别名
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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` 进行类型守卫:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 泛型 + 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` 强转:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 类型守卫谓词函数
|
||||||
|
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()` | `pages/Timestamp/index.tsx` |
|
||||||
|
| 业务组件 | `const X = React.memo(...)` + `export default X` | `LiveClock.tsx`, `ResultView.tsx` |
|
||||||
|
| UI 原子组件 | `React.forwardRef(...)` + `export { X }` | `components/ui/button.tsx` |
|
||||||
|
| 工具函数 | `export function xxx()` | `utils/clipboard.ts` |
|
||||||
|
| 自定义 Hook | `export function useXxx()` | `utils/useStorageState.ts` |
|
||||||
|
| 类型/接口 | `export interface` / `export type` | `types/storage.d.ts` |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 页面组件 — 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 要求)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 标准页面组件
|
||||||
|
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`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 继承 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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 中清除定时器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 取消标志模式 — 防止异步竞态
|
||||||
|
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` |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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 导入顺序
|
||||||
|
|
||||||
|
按来源分组,顺序如下:
|
||||||
|
|
||||||
|
1. React 核心
|
||||||
|
2. 第三方库(图标、UI 库等)
|
||||||
|
3. 业务 Provider / Context
|
||||||
|
4. 配置 / 存储
|
||||||
|
5. i18n
|
||||||
|
6. 本地页面组件
|
||||||
|
7. UI 组件
|
||||||
|
8. 工具函数 / Hook
|
||||||
|
9. 类型
|
||||||
|
10. 常量
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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 路径别名
|
||||||
|
|
||||||
|
- `@/` 映射到项目根目录
|
||||||
|
- **跨目录导入**:使用 `@/` 绝对别名
|
||||||
|
- **同目录导入**:使用相对路径 `./`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 绝对别名导入 — 跨目录
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 变量语义化类名,**禁止硬编码颜色值**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 语义化颜色 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:` 断点:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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` 字段的结果对象:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 结果对象模式
|
||||||
|
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` 显示错误:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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()` 隔离异常:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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` 组件捕获子组件树异常:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RouterContainer />
|
||||||
|
</ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 命名规范
|
||||||
|
|
||||||
|
### 6.1 文件命名
|
||||||
|
|
||||||
|
| 类型 | 命名模式 | 示例 |
|
||||||
|
| ----------- | -------------------------- | -------------------------------------------------------- |
|
||||||
|
| 页面目录 | PascalCase | `Timestamp/`, `Base64Converter/`, `StorageCleaner/` |
|
||||||
|
| 页面入口 | `index.tsx` | `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 变量 / 函数命名
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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` 前缀命名组件内事件处理函数:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleUseNow = useCallback((now: number) => { ... }, []);
|
||||||
|
const handleSelectFeature = (feature: FeatureConfig) => { ... };
|
||||||
|
const handleFileChange = useCallback((file: File) => { ... }, []);
|
||||||
|
const handleClean = useCallback(async () => { ... }, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试规范
|
||||||
|
|
||||||
|
### 7.1 文件组织
|
||||||
|
|
||||||
|
测试文件放在源代码同级的 `__tests__/` 目录下:
|
||||||
|
|
||||||
|
```
|
||||||
|
utils/__tests__/jwt.test.ts
|
||||||
|
utils/__tests__/base64Converter.test.ts
|
||||||
|
utils/__tests__/useStorageState.test.ts
|
||||||
|
components/__tests__/SwitchButtonGroup.test.tsx
|
||||||
|
components/__tests__/ErrorBoundary.test.tsx
|
||||||
|
pages/Timestamp/__tests__/index.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 describe / it 命名
|
||||||
|
|
||||||
|
`describe` 使用模块/函数名,`it` 使用中文描述行为("应该..."):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 中文 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 等)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 模块级 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 匹配器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 语义化查询
|
||||||
|
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 注释
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 完整的 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**:放在 `utils/` 目录下
|
||||||
|
- **页面专属 Hook**:与页面组件同目录
|
||||||
|
|
||||||
|
```
|
||||||
|
utils/useStorageState.ts — Chrome Storage 状态持久化
|
||||||
|
utils/useLazyTranslation.ts — i18n 懒加载
|
||||||
|
utils/useContextMenuData.ts — 右键菜单数据
|
||||||
|
utils/useDebounce.ts — 防抖
|
||||||
|
pages/Timestamp/useTimestampConverter.ts — 页面级 Hook
|
||||||
|
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 使用方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 页面组件 — 使用 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 添加新翻译
|
||||||
|
|
||||||
|
1. 在 `i18n/locales/{zh,en}/features.json` 添加功能标题和描述
|
||||||
|
2. 创建 `i18n/locales/{zh,en}/{功能名}.json` 添加功能专属翻译
|
||||||
|
3. 在 `utils/useLazyTranslation.ts` 的 `localeModules` 中注册新命名空间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 存储规范
|
||||||
|
|
||||||
|
### 10.1 StorageSchema
|
||||||
|
|
||||||
|
所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中声明:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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` 的类型安全封装:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useStorageState } from '@/utils/useStorageState';
|
||||||
|
|
||||||
|
const [themeMode, setThemeMode, isInitialized] = useStorageState(
|
||||||
|
'app/themeMode',
|
||||||
|
'system',
|
||||||
|
isValidMode, // 可选的类型守卫
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 文件组织
|
||||||
|
|
||||||
|
### 11.1 页面组件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/FeatureName/
|
||||||
|
├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件)
|
||||||
|
├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑)
|
||||||
|
├── constants.ts # 常量定义(可选)
|
||||||
|
├── LiveClock.tsx # 子组件(可选)
|
||||||
|
├── ResultView.tsx # 子组件(可选)
|
||||||
|
└── __tests__/ # 测试文件
|
||||||
|
└── index.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.2 目录职责
|
||||||
|
|
||||||
|
| 目录 | 职责 |
|
||||||
|
| ---------------- | ------------------------------------------------------------ |
|
||||||
|
| `config/` | 应用配置(功能定义、路由映射) |
|
||||||
|
| `entrypoints/` | 扩展入口点(popup、options、sidepanel、background、content) |
|
||||||
|
| `pages/` | 功能页面组件(懒加载) |
|
||||||
|
| `components/` | 可复用 UI 组件 |
|
||||||
|
| `components/ui/` | shadcn/ui 基础组件(button、dialog、select 等) |
|
||||||
|
| `providers/` | React Context(Router、Theme 等) |
|
||||||
|
| `hooks/` | 自定义 React Hooks |
|
||||||
|
| `utils/` | 工具函数与服务抽象 |
|
||||||
|
| `types/` | TypeScript 类型声明 |
|
||||||
|
| `lib/` | 通用工具函数(cn、utils) |
|
||||||
|
| `i18n/` | 国际化资源 |
|
||||||
|
| `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`
|
||||||
|
- **行内注释**:仅在需要澄清复杂逻辑时使用
|
||||||
|
- **禁止注释显而易见的代码**
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Copilot 指令
|
||||||
|
|
||||||
|
基于 WXT 框架的浏览器扩展项目(React 19 + TypeScript),为开发者和测试人员提供效率工具:时间戳转换、存储清理、JWT 解析、JSON 工具、二维码、Base64、Markdown 等。
|
||||||
|
|
||||||
|
## 核心命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Chrome 开发模式(支持 HMR)
|
||||||
|
npm run dev:firefox # Firefox 开发模式
|
||||||
|
npm run build # Chrome 生产构建
|
||||||
|
npm run build:firefox # Firefox 生产构建
|
||||||
|
npm run zip # 打包 Chrome 扩展(.output/*.zip)
|
||||||
|
npm run zip:firefox # 打包 Firefox 扩展
|
||||||
|
npm run lint # ESLint 检查(--max-warnings=0)
|
||||||
|
npm run typecheck # TypeScript 类型检查(tsc --noEmit)
|
||||||
|
npm run test # 运行全部单元测试(vitest run)
|
||||||
|
npm run test:watch # Vitest 监视模式
|
||||||
|
npm run test:coverage # 带覆盖率的测试
|
||||||
|
```
|
||||||
|
|
||||||
|
运行单个测试文件:`npx vitest run path/to/file.test.ts`
|
||||||
|
|
||||||
|
修改 `package.json` 后需运行 `npm install`(会自动触发 `postinstall` → `wxt prepare` 重新生成 `.wxt/` 类型声明)。
|
||||||
|
|
||||||
|
## CI 流水线(GitHub Actions)
|
||||||
|
|
||||||
|
严格顺序门控,任一步骤失败则终止:
|
||||||
|
|
||||||
|
1. **setup** — 安装依赖,缓存 `node_modules`
|
||||||
|
2. **lint**、**typecheck**、**test** — 三者并行运行,全部通过才继续
|
||||||
|
3. **build** — Chrome + Firefox 矩阵构建(仅当步骤 2 全部通过时执行)
|
||||||
|
|
||||||
|
Pre-commit 钩子(`.husky/pre-commit` → `lint-staged`):
|
||||||
|
|
||||||
|
1. 代码文件(`*.{ts,tsx,js,jsx,mjs}`):运行 `eslint --fix --max-warnings=0`
|
||||||
|
2. 同一代码文件:运行 `prettier --write`
|
||||||
|
3. 其他文件(`*.{json,css,scss,md}`):运行 `prettier --write`
|
||||||
|
|
||||||
|
## 项目架构
|
||||||
|
|
||||||
|
### 路由(不使用 React Router)
|
||||||
|
|
||||||
|
路由完全通过 `config/features.tsx` 中的 `FEATURES` 数组管理。每个功能定义一个 `key`(类型为 `types/storage.d.ts` 中的 `PageType`)和三个懒加载组件,分别对应 `popup`、`sidepanel`、`tab` 三种渲染模式。`providers/RouterProvider.tsx` 中的 `RouterProvider` 根据存储状态渲染当前页面。
|
||||||
|
|
||||||
|
存在三套独立的路由作用域:`app/popupRoute`、`app/sidepanelRoute`、`app/tabRoute`,各自维护独立的可见页面列表和页面排序。
|
||||||
|
|
||||||
|
### 存储
|
||||||
|
|
||||||
|
所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中声明,键名使用 kebab-case 格式(如 `app/currentRoute`)。使用 `utils/chromeStorage.ts` 中的类型安全封装(`storageUtil.get/set/remove`)。
|
||||||
|
|
||||||
|
Router 同时使用 `chrome.storage.local` 持久化和 `localStorage` 快照来消除首屏闪烁。
|
||||||
|
|
||||||
|
### 扩展通信
|
||||||
|
|
||||||
|
使用 `@webext-core/messaging`。通信协议在 `utils/messages.ts` 中通过 `ProtocolMap` 定义。使用该模块导出的 `sendMessage` / `onMessage`,不要直接使用原生 `chrome.runtime.sendMessage`。
|
||||||
|
|
||||||
|
### 页面组件模式
|
||||||
|
|
||||||
|
功能页面遵循 **UI + Hook 分离** 模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/FeatureName/
|
||||||
|
├── index.tsx # UI 组件(纯展示,使用 shadcn/ui 组件)
|
||||||
|
├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑)
|
||||||
|
└── constants.ts # 常量定义(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 页面组件调用 `useLazyTranslation('featureName')` 获取翻译函数
|
||||||
|
- Hook 负责所有状态管理,通过返回值暴露给页面
|
||||||
|
- 子组件可进一步拆分(如 `LiveClock.tsx`、`ResultView.tsx`)
|
||||||
|
|
||||||
|
### 新功能开发清单
|
||||||
|
|
||||||
|
1. 在 `types/storage.d.ts` 的 `PageType` 联合类型中添加新成员
|
||||||
|
2. 在 `config/features.tsx` 的 `FEATURES` 数组中添加配置(key、翻译键、图标、三种渲染模式组件)
|
||||||
|
3. 在 `pages/` 目录创建页面组件(懒加载):
|
||||||
|
- `index.tsx` — 使用 `useLazyTranslation` 的 UI 组件
|
||||||
|
- `useFeatureName.ts` — 业务逻辑 Hook
|
||||||
|
- `constants.ts` — 常量(可选)
|
||||||
|
4. 在 `i18n/locales/{zh,en}/features.json` 添加翻译(复杂功能可新建独立 JSON 文件)
|
||||||
|
5. 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`
|
||||||
|
6. 添加对应的单元测试
|
||||||
|
|
||||||
|
## 关键规范
|
||||||
|
|
||||||
|
> 完整的代码编写规范详见 [CODING_STANDARDS.md](./CODING_STANDARDS.md)。
|
||||||
|
|
||||||
|
### 浏览器 API
|
||||||
|
|
||||||
|
始终使用 `wxt/browser` 导出的 `browser` 对象,而非原生 `chrome` API,以确保跨浏览器兼容性。
|
||||||
|
|
||||||
|
### 路径别名
|
||||||
|
|
||||||
|
`@/` 映射到项目根目录(已在 tsconfig 和 vitest.config 中配置)。跨目录导入使用 `@/` 绝对别名,同目录导入使用 `./` 相对路径。
|
||||||
|
|
||||||
|
### UI 组件
|
||||||
|
|
||||||
|
- 使用 `components/ui/` 下的 shadcn/ui 组件(button、dialog、select、input 等)
|
||||||
|
- 图标:`lucide-react`
|
||||||
|
- 样式:Tailwind CSS + `@/lib/utils` 中的 `cn()` 工具函数(clsx + tailwind-merge)
|
||||||
|
- 主题:使用 shadcn/ui 语义化 token(`bg-background`、`text-foreground`、`border-border` 等),禁止硬编码颜色
|
||||||
|
|
||||||
|
### 代码分割
|
||||||
|
|
||||||
|
`wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组 vendor 依赖(vendor-react、vendor-i18n、vendor-qr、vendor-dnd、vendor-markdown),无需手动配置。
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
|
||||||
|
- 禁止使用 `any`(测试文件除外)
|
||||||
|
- 未使用的变量/参数:使用 `_` 前缀(如 `_unused`)
|
||||||
|
- Prettier:100 字符宽、单引号、尾逗号 all、LF 换行
|
||||||
|
- ESLint 使用 `typescript-eslint` 的 `projectService: true`
|
||||||
|
- 导出模式:页面组件 default export,工具函数/Hook 命名导出,UI 组件 forwardRef + 命名导出
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- 环境:jsdom
|
||||||
|
- 全局变量:`vitest/globals`(describe、it、expect 等无需导入)
|
||||||
|
- Setup 文件:`vitest.setup.ts` 自动 mock 以下内容:
|
||||||
|
- `chrome.*` / `browser.*` API(storage、tabs、runtime、cookies 等)
|
||||||
|
- `react-i18next`(返回 key 作为翻译)
|
||||||
|
- `@/utils/useLazyTranslation`(返回 `ns:key` 格式)
|
||||||
|
- `window.matchMedia`
|
||||||
|
- 测试文件命名:`__tests__/*.test.{ts,tsx}` 或 `*.test.{ts,tsx}`
|
||||||
|
- 使用 `vi.mock()` 进行模块级 mock;避免重复 mock `vitest.setup.ts` 中已有的内容
|
||||||
|
- 测试工具:`@testing-library/react` + `@testing-library/user-event`
|
||||||
|
|
||||||
|
### 国际化(i18n)
|
||||||
|
|
||||||
|
- 命名空间:`common`(默认)、`features`
|
||||||
|
- 翻译键格式:`namespace:key`(如 `features:timestamp.title`)
|
||||||
|
- 语言:`zh`(默认)、`en`
|
||||||
|
- 翻译文件:`i18n/locales/{zh,en}/{common,features}.json` + 各功能独立 JSON 文件
|
||||||
|
- 使用 `useLazyTranslation` Hook 加载功能专属翻译
|
||||||
|
- 回退策略:缺失的翻译键回退到 `zh`,若仍缺失则返回占位格式 `namespace:key`
|
||||||
|
|
||||||
|
### WXT 生成文件
|
||||||
|
|
||||||
|
- `.wxt/` 目录由 `postinstall`(`wxt prepare`)自动生成,包含 TypeScript 类型声明和扩展 tsconfig
|
||||||
|
- 生产构建输出到 `.output/` 目录
|
||||||
|
- `tsconfig.json` 继承自 `./.wxt/tsconfig.json`
|
||||||
@@ -47,6 +47,7 @@ config/features.tsx # 功能定义(路由 + 元数据的单一事实来源
|
|||||||
entrypoints/ # 扩展入口点 (popup/, options/, sidepanel/, background.ts, content.ts)
|
entrypoints/ # 扩展入口点 (popup/, options/, sidepanel/, background.ts, content.ts)
|
||||||
pages/ # 功能页面组件 (懒加载)
|
pages/ # 功能页面组件 (懒加载)
|
||||||
components/ # 可复用 UI 组件
|
components/ # 可复用 UI 组件
|
||||||
|
components/ui/ # shadcn/ui 基础组件 (button, dialog, select 等)
|
||||||
providers/ # React Context (Router, Theme 等)
|
providers/ # React Context (Router, Theme 等)
|
||||||
hooks/ # 自定义 React Hooks
|
hooks/ # 自定义 React Hooks
|
||||||
utils/ # 工具函数与服务抽象
|
utils/ # 工具函数与服务抽象
|
||||||
@@ -54,11 +55,26 @@ types/ # TypeScript 类型声明
|
|||||||
i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功能独立 JSON)
|
i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功能独立 JSON)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 页面组件模式
|
||||||
|
|
||||||
|
典型功能页面遵循 **UI + Hook 分离** 模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/FeatureName/
|
||||||
|
├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件)
|
||||||
|
├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑)
|
||||||
|
└── constants.ts # 常量定义
|
||||||
|
```
|
||||||
|
|
||||||
|
- 页面组件调用 `useLazyTranslation('featureName')` 获取翻译函数
|
||||||
|
- Hook 负责所有状态管理和业务逻辑,通过返回值暴露给页面
|
||||||
|
- 子组件可进一步拆分(如 `LiveClock.tsx`、`ResultView.tsx`)
|
||||||
|
|
||||||
## 关键架构决策
|
## 关键架构决策
|
||||||
|
|
||||||
**路由**: 不使用 React Router。通过 `config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType`
|
**路由**: 不使用 React Router。通过 `config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType`
|
||||||
渲染对应组件。支持三种渲染模式:popup(弹窗)、sidepanel(侧边栏)和 browser-tab(浏览器新标签页,通过 `open_in_tab` 打开)。
|
渲染对应组件。支持三种渲染模式:popup(弹窗)、sidepanel(侧边栏)和 browser-tab(浏览器新标签页,通过 `open_in_tab` 打开)。
|
||||||
每种模式有独立的路由和可见页面配置。
|
每种模式有独立的路由和可见页面配置(`app/popupRoute`、`app/sidepanelRoute`、`app/tabRoute` 等)。
|
||||||
|
|
||||||
**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。
|
**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。
|
||||||
使用 `utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
|
使用 `utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
|
||||||
@@ -70,6 +86,8 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功
|
|||||||
|
|
||||||
**浏览器兼容**: 优先使用 `wxt/browser` 导出的 `browser` 对象,而非原生 `chrome` API。
|
**浏览器兼容**: 优先使用 `wxt/browser` 导出的 `browser` 对象,而非原生 `chrome` API。
|
||||||
|
|
||||||
|
**代码分割**: `wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组依赖(vendor-react、vendor-i18n、vendor-qr 等),无需手动配置。
|
||||||
|
|
||||||
## 测试环境
|
## 测试环境
|
||||||
|
|
||||||
- 环境: jsdom
|
- 环境: jsdom
|
||||||
@@ -99,8 +117,11 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功
|
|||||||
## 新功能开发清单
|
## 新功能开发清单
|
||||||
|
|
||||||
1. 在 `types/storage.d.ts` 添加 `PageType` 联合类型
|
1. 在 `types/storage.d.ts` 添加 `PageType` 联合类型
|
||||||
2. 在 `config/features.tsx` 的 `FEATURES` 数组添加配置
|
2. 在 `config/features.tsx` 的 `FEATURES` 数组添加配置(指定 key、翻译键、图标、三种渲染模式的组件)
|
||||||
3. 在 `pages/` 创建页面组件 (懒加载)
|
3. 在 `pages/` 创建页面组件 (懒加载):
|
||||||
|
- `index.tsx` — UI 组件,使用 `useLazyTranslation` 获取翻译
|
||||||
|
- `useFeatureName.ts` — 业务逻辑 Hook
|
||||||
|
- `constants.ts` — 常量(可选)
|
||||||
4. 在 `i18n/locales/{zh,en}/features.json` 添加翻译(复杂功能可新建独立 JSON)
|
4. 在 `i18n/locales/{zh,en}/features.json` 添加翻译(复杂功能可新建独立 JSON)
|
||||||
5. 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`
|
5. 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`
|
||||||
6. 添加对应的单元测试
|
6. 添加对应的单元测试
|
||||||
@@ -115,15 +136,11 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功
|
|||||||
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
|
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
|
||||||
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
|
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
|
||||||
|
|
||||||
## 技术栈版本
|
## 关键外部库(非显而易见的)
|
||||||
|
|
||||||
- WXT: ^0.20.26
|
- `@webext-core/messaging` — 扩展消息通信
|
||||||
- React: ^19.2.6
|
- `@dnd-kit` — 拖拽排序(用于页面顺序管理)
|
||||||
- Tailwind CSS: ^3.4.19
|
- `marked` — Markdown 解析
|
||||||
- shadcn/ui (基于 Radix UI + class-variance-authority)
|
- `qrious` + `qr-scanner` — 二维码生成与解析
|
||||||
- TypeScript: ^5.9.3
|
- `dayjs` — 日期处理(时间戳转换)
|
||||||
- Vitest: ^4.1.7
|
- `sonner` — Toast 通知(替代传统 snackbar)
|
||||||
- i18next: ^26.2.0
|
|
||||||
- @dnd-kit (拖拽排序)
|
|
||||||
- marked (Markdown 解析)
|
|
||||||
- qrious + qr-scanner (二维码生成与解析)
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# components/
|
||||||
|
|
||||||
|
通用业务组件目录,存放跨页面复用的 UI 组件,与具体工具页面解耦。
|
||||||
|
|
||||||
|
## 组件列表
|
||||||
|
|
||||||
|
| 组件 | 用途 |
|
||||||
|
| ----------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| `TopBar.tsx` | 顶部导航栏,集成搜索(含历史记录)、主题切换、语言切换、返回导航 |
|
||||||
|
| `RouterContainer.tsx` | 路由容器,根据当前路由动态渲染对应页面组件,集成错误边界和骨架屏 |
|
||||||
|
| `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 |
|
||||||
|
| `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 |
|
||||||
|
| `CopyButton.tsx` | 一键复制按钮,支持复制成功状态动画,封装 `copyTextToClipboard` 和 `toast` 反馈 |
|
||||||
|
| `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 |
|
||||||
|
| `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 |
|
||||||
|
| `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 |
|
||||||
|
| `GlobalSnackbar.tsx` | 全局消息提示组件 + Context Provider,支持受控/Hook/全局单例三种使用方式 |
|
||||||
|
| `ErrorBoundary.tsx` | 全局错误边界(类组件),捕获子组件树 JS 错误并展示友好错误页面 |
|
||||||
|
| `PageErrorBoundary.tsx` | 页面级错误边界,适配 shadcn 暗黑模式,支持 `resetKey` 自动恢复 |
|
||||||
|
| `PageSkeleton.tsx` | 页面骨架屏,提供 `dashboard` 和 `tool` 两种变体,用于 Suspense fallback |
|
||||||
|
|
||||||
|
## 使用约定
|
||||||
|
|
||||||
|
- 优先使用 `components/ui/` 下的 shadcn/ui 基础组件
|
||||||
|
- 组件使用 `cn()` 合并 Tailwind 类名,支持 `className` 透传
|
||||||
|
- 需要 memo 优化的组件使用 `React.memo` + `displayName`
|
||||||
|
- 需要 ref 转发的组件使用 `React.forwardRef` + `displayName`
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# components/ui/
|
||||||
|
|
||||||
|
shadcn/ui 基础原子组件目录,基于 Radix UI 原语 + Tailwind CSS 实现。
|
||||||
|
|
||||||
|
## 组件列表
|
||||||
|
|
||||||
|
| 组件 | 用途 |
|
||||||
|
| -------------- | -------------------------------------------------------------------------------------------------- |
|
||||||
|
| `button.tsx` | 按钮组件,支持 `default/destructive/outline/secondary/ghost/link` 变体和 `default/sm/lg/icon` 尺寸 |
|
||||||
|
| `input.tsx` | 标准输入框,统一的 ring/focus 样式 |
|
||||||
|
| `select.tsx` | 下拉选择组件,包含 Trigger、Content、Item 等子组件 |
|
||||||
|
| `dialog.tsx` | 对话框组件,包含 Overlay、Content、Header、Footer、Title、Description |
|
||||||
|
| `checkbox.tsx` | 复选框组件 |
|
||||||
|
| `label.tsx` | 标签组件 |
|
||||||
|
| `switch.tsx` | 开关组件 |
|
||||||
|
| `badge.tsx` | 徽章组件,支持 `default/secondary/destructive/outline` 变体 |
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新组件
|
||||||
|
|
||||||
|
使用 shadcn/ui CLI 添加新组件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn-ui@latest add <component-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
组件配置在项目根目录的 `components.json` 中定义。
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# config/
|
||||||
|
|
||||||
|
应用级配置目录,存放功能特性的注册中心。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| -------------- | ---------------------------------------- |
|
||||||
|
| `features.tsx` | 核心配置文件,定义所有工具功能的注册信息 |
|
||||||
|
|
||||||
|
## features.tsx
|
||||||
|
|
||||||
|
`FEATURES` 数组是路由和功能元数据的**单一事实来源**,每个功能定义包含:
|
||||||
|
|
||||||
|
- `key`:页面类型标识(`PageType`)
|
||||||
|
- `labelKey` / `descriptionKey`:i18n 翻译键
|
||||||
|
- `themeColorKey`:主题色(`primary/success/warning/error/secondary/info`)
|
||||||
|
- `icon`:lucide-react 图标组件
|
||||||
|
- `defaultVisible`:默认是否可见
|
||||||
|
- `components`:三种渲染模式的懒加载组件(`popup`、`sidepanel`、`tab`)
|
||||||
|
|
||||||
|
## 已注册功能(11 个)
|
||||||
|
|
||||||
|
| key | 图标 | 说明 |
|
||||||
|
| -------------------- | ----------------- | ----------------- |
|
||||||
|
| `dashboard` | — | 仪表盘首页 |
|
||||||
|
| `timestamp` | Clock | 时间戳转换工具 |
|
||||||
|
| `storageCleaner` | Database | 存储清理工具 |
|
||||||
|
| `qrCode` | QrCode | 二维码工具 |
|
||||||
|
| `textStatistics` | FileText | 文本统计工具 |
|
||||||
|
| `jwt` | Key | JWT 解析工具 |
|
||||||
|
| `jsonDiff` | GitCompareArrows | JSON 差异比较工具 |
|
||||||
|
| `base64Converter` | ArrowLeftRight | Base64 转换器 |
|
||||||
|
| `markdownToHtml` | Code | Markdown 转 HTML |
|
||||||
|
| `htmlToMarkdown` | File | HTML 转 Markdown |
|
||||||
|
| `rightClickRestorer` | MousePointerClick | 右键菜单恢复工具 |
|
||||||
|
|
||||||
|
## 导出函数
|
||||||
|
|
||||||
|
- `getFeatureByKey(key)` — 根据 key 获取功能配置
|
||||||
|
- `getDefaultVisibleFeatureKeys()` — 获取默认可见的功能 key 列表
|
||||||
|
- `getAllFeatureKeys()` — 获取所有功能 key 列表
|
||||||
|
- `getDefaultPageOrder()` — 获取默认页面排序(不含 dashboard)
|
||||||
|
- `getEntryPointType()` — 判断当前入口类型(popup/sidepanel/tab)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# entrypoints/
|
||||||
|
|
||||||
|
WXT 框架要求的扩展生命周期入口点,对应 Chrome Extension 的各个上下文。
|
||||||
|
|
||||||
|
## 入口文件
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||||
|
| `background.ts` | Service Worker 入口:注册右键菜单、监听菜单点击、处理消息通信、管理侧边栏状态、注入主环境脚本 |
|
||||||
|
| `content.ts` | Content Script 入口:注入所有页面(`<all_urls>`),在 `document_end` 时初始化消息处理器 |
|
||||||
|
| `rightClickRestorer.content.ts` | 专用 Content Script:处理右键菜单恢复功能,注入浮动状态徽章 |
|
||||||
|
|
||||||
|
## 子目录
|
||||||
|
|
||||||
|
### popup/
|
||||||
|
|
||||||
|
Popup 弹窗页面(点击扩展图标弹出)。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------ |
|
||||||
|
| `index.html` | HTML 入口 |
|
||||||
|
| `main.tsx` | React 挂载点 |
|
||||||
|
| `App.tsx` | 根组件,组装 `RouterProvider` + `TopBar` + `ErrorBoundary` + `RouterContainer` |
|
||||||
|
|
||||||
|
### sidepanel/
|
||||||
|
|
||||||
|
侧边栏页面,结构与 popup 类似,额外通知 background 侧边栏开启/关闭状态。
|
||||||
|
|
||||||
|
### options/
|
||||||
|
|
||||||
|
设置页面,支持:
|
||||||
|
|
||||||
|
- 拖拽排序功能顺序(`@dnd-kit`)
|
||||||
|
- 功能可见性管理(显示/隐藏)
|
||||||
|
- Popup/Sidepanel/Tab 三种模式独立配置
|
||||||
|
|
||||||
|
### content/
|
||||||
|
|
||||||
|
Content Script 内部分模块:
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ----------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `messageHandler.ts` | 消息处理器初始化入口 |
|
||||||
|
| `contextMenuHandler.ts` | 右键菜单点击事件处理,执行时间戳转换/文本统计并通过 UI Popover 展示 |
|
||||||
|
| `uiPopover.ts` | 在页面中注入浮层 Popover UI,展示右键菜单操作结果 |
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
- `background.ts` 是扩展的核心协调者,处理跨上下文通信
|
||||||
|
- `content.ts` 注入到所有页面,负责接收和处理来自 background 的消息
|
||||||
|
- `popup/`、`sidepanel/`、`options/` 共享同一套页面组件(来自 `pages/`),通过 `RouterProvider` 的不同配置实现独立路由
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# i18n/
|
||||||
|
|
||||||
|
国际化资源目录,管理多语言翻译和 i18next 初始化配置。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
i18n/
|
||||||
|
├── index.ts # i18next 初始化配置
|
||||||
|
└── locales/
|
||||||
|
├── zh/ # 中文翻译(默认语言)
|
||||||
|
│ ├── common.json # 通用文案
|
||||||
|
│ ├── features.json # 功能模块标题和描述
|
||||||
|
│ ├── timestamp.json # 时间戳工具翻译
|
||||||
|
│ ├── storageCleaner.json # 存储清理工具翻译
|
||||||
|
│ ├── qrCode.json # 二维码工具翻译
|
||||||
|
│ ├── textStatistics.json # 文本统计工具翻译
|
||||||
|
│ ├── jwt.json # JWT 工具翻译
|
||||||
|
│ ├── jsonDiff.json # JSON 差异工具翻译
|
||||||
|
│ ├── jsonFormat.json # JSON 格式化工具翻译
|
||||||
|
│ ├── base64Converter.json
|
||||||
|
│ ├── markdownToHtml.json
|
||||||
|
│ ├── htmlToMarkdown.json
|
||||||
|
│ └── rightClickRestorer.json
|
||||||
|
└── en/ # 英文翻译(结构同上)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## index.ts
|
||||||
|
|
||||||
|
i18next 初始化配置:
|
||||||
|
|
||||||
|
- 同步加载 `common` 和 `features` 核心命名空间
|
||||||
|
- 自定义 `chromeStorage` 语言检测器,从 Chrome Storage 读取语言偏好
|
||||||
|
- `normalizeLanguage()` 将任意语言标识归一化为 `zh` 或 `en`
|
||||||
|
- 语言变更时同步更新 Day.js 本地化和 localStorage 快照
|
||||||
|
|
||||||
|
## 翻译键格式
|
||||||
|
|
||||||
|
- 命名空间:`common`(默认)、`features`、各功能独立命名空间
|
||||||
|
- 键格式:`namespace:key`(如 `features:timestamp.title`、`timestamp:unitMs`)
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 页面组件 — 懒加载翻译
|
||||||
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
|
const { t } = useLazyTranslation('timestamp');
|
||||||
|
t('timestamp:title');
|
||||||
|
|
||||||
|
// 全局组件 — 直接使用
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
const { t } = useTranslation(['common', 'features']);
|
||||||
|
t('common:settings');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新翻译
|
||||||
|
|
||||||
|
1. 在 `locales/{zh,en}/features.json` 添加功能标题和描述
|
||||||
|
2. 创建 `locales/{zh,en}/{功能名}.json` 添加功能专属翻译
|
||||||
|
3. 在 `utils/useLazyTranslation.ts` 的 `localeModules` 中注册新命名空间
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# lib/
|
||||||
|
|
||||||
|
通用库工具目录,存放与业务无关的底层工具函数。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ---------- | ------------------------------------ |
|
||||||
|
| `utils.ts` | `cn()` 函数 — shadcn/ui 标准工具函数 |
|
||||||
|
|
||||||
|
## cn()
|
||||||
|
|
||||||
|
组合 `clsx` + `tailwind-merge`,用于合并和去重 Tailwind CSS 类名:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// 条件类名 + 合并外部 className
|
||||||
|
<div className={cn(
|
||||||
|
'flex items-center gap-3 px-3',
|
||||||
|
isActive && 'bg-primary text-primary-foreground',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
```
|
||||||
|
|
||||||
|
所有需要动态合并 Tailwind 类名的场景都应使用 `cn()`,而非手动拼接字符串。
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
# pages/
|
||||||
|
|
||||||
|
功能页面组件目录,每个子目录对应一个工具页面。
|
||||||
|
|
||||||
|
## 目录结构约定
|
||||||
|
|
||||||
|
典型页面遵循 **UI + Hook 分离** 模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/FeatureName/
|
||||||
|
├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件)
|
||||||
|
├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑)
|
||||||
|
├── constants.ts # 常量定义(可选)
|
||||||
|
├── SubComponent.tsx # 子组件(可选)
|
||||||
|
└── __tests__/ # 测试文件
|
||||||
|
└── index.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面列表
|
||||||
|
|
||||||
|
### Dashboard/
|
||||||
|
|
||||||
|
仪表盘首页,以卡片网格展示所有可见工具,支持点击导航。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| -------------- | -------------------------------------------- |
|
||||||
|
| `index.tsx` | 页面组件,渲染工具卡片网格 |
|
||||||
|
| `ToolCard.tsx` | 工具卡片组件,展示图标、标题、描述和实时数据 |
|
||||||
|
|
||||||
|
### Timestamp/
|
||||||
|
|
||||||
|
时间戳转换工具,支持秒/毫秒级互转、多时区选择、实时时钟。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| -------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| `index.tsx` | 页面 UI |
|
||||||
|
| `useTimestampConverter.ts` | 业务逻辑 Hook,包含转换模式、输入、单位、时区状态和响应式计算管线 |
|
||||||
|
| `LiveClock.tsx` | 实时时钟子组件,`React.memo` 优化 |
|
||||||
|
| `ResultView.tsx` | 转换结果展示子组件 |
|
||||||
|
| `constants.ts` | 时区列表等常量 |
|
||||||
|
|
||||||
|
### StorageCleaner/
|
||||||
|
|
||||||
|
浏览器存储清理工具,支持 Cookie/LocalStorage/SessionStorage/IndexedDB/Cache/SW 清理。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| --------------------------- | -------------- |
|
||||||
|
| `index.tsx` | 页面 UI |
|
||||||
|
| `useStorageCleaner.ts` | 业务逻辑 Hook |
|
||||||
|
| `StorageOptionsGrid.tsx` | 清理选项网格 |
|
||||||
|
| `StorageCleanerConfirm.tsx` | 清理确认对话框 |
|
||||||
|
| `AutoRefreshToggle.tsx` | 自动刷新开关 |
|
||||||
|
| `ErrorDisplay.tsx` | 错误展示组件 |
|
||||||
|
| `CleaningResult.tsx` | 清理结果展示 |
|
||||||
|
| `OptionItem.tsx` | 单个选项组件 |
|
||||||
|
|
||||||
|
### QrCode/
|
||||||
|
|
||||||
|
二维码工具,支持生成(URL→QR)和解析(QR→URL)。
|
||||||
|
|
||||||
|
| 文件/目录 | 用途 |
|
||||||
|
| ------------- | ---------------- |
|
||||||
|
| `index.tsx` | 页面 UI |
|
||||||
|
| `types.ts` | 类型定义 |
|
||||||
|
| `contexts/` | Context Provider |
|
||||||
|
| `hooks/` | 业务逻辑 Hooks |
|
||||||
|
| `components/` | 子组件 |
|
||||||
|
|
||||||
|
### TextStatistics/
|
||||||
|
|
||||||
|
文本统计工具,实时计算字符数、单词数、行数、字节大小。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ----------- | --------------------------------------------- |
|
||||||
|
| `index.tsx` | 页面组件,集成 `TextInputArea` 和统计结果展示 |
|
||||||
|
|
||||||
|
### Jwt/
|
||||||
|
|
||||||
|
JWT 解析工具,解码 Header/Payload/Signature。
|
||||||
|
|
||||||
|
| 文件/目录 | 用途 |
|
||||||
|
| ------------- | ---------------- |
|
||||||
|
| `index.tsx` | 页面 UI |
|
||||||
|
| `types.ts` | 类型定义 |
|
||||||
|
| `contexts/` | Context Provider |
|
||||||
|
| `hooks/` | 业务逻辑 Hooks |
|
||||||
|
| `components/` | 子组件 |
|
||||||
|
|
||||||
|
### JsonTools/
|
||||||
|
|
||||||
|
JSON 工具集:差异比较、格式化、YAML/TOML/Minify 转换。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ------------------------ | ---------------------------- |
|
||||||
|
| `index.tsx` | 页面入口,子模式切换 |
|
||||||
|
| `types.ts` | 类型定义 |
|
||||||
|
| `diffEngine.ts` | 差异比较引擎 |
|
||||||
|
| `JsonDiffInput.tsx` | JSON 输入组件 |
|
||||||
|
| `DiffResult.tsx` | 差异结果展示 |
|
||||||
|
| `DiffNavigator.tsx` | 差异导航器 |
|
||||||
|
| `JsonFormatSection.tsx` | 格式化区域 |
|
||||||
|
| `JsonConvertSection.tsx` | 转换区域(YAML/TOML/Minify) |
|
||||||
|
| `JsonTree.tsx` | JSON 树形展示 |
|
||||||
|
|
||||||
|
### Base64Converter/
|
||||||
|
|
||||||
|
Base64 编解码工具,支持文本/文件/图片三种模式。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ---------------------------- | -------------------- |
|
||||||
|
| `index.tsx` | 页面入口,子模式切换 |
|
||||||
|
| `useBase64Converter.ts` | 业务逻辑 Hook |
|
||||||
|
| `TextMode.tsx` | 文本模式 |
|
||||||
|
| `FileMode.tsx` | 文件模式 |
|
||||||
|
| `ImageMode.tsx` | 图片模式 |
|
||||||
|
| `Base64ConverterSection.tsx` | 通用转换区域 |
|
||||||
|
|
||||||
|
### MarkdownToHtml/
|
||||||
|
|
||||||
|
Markdown 转 HTML 工具,支持分栏/预览/源码三种视图模式。
|
||||||
|
|
||||||
|
### HtmlToMarkdown/
|
||||||
|
|
||||||
|
HTML 转 Markdown 工具,支持分栏/预览/Markdown 三种视图模式。
|
||||||
|
|
||||||
|
### RightClickRestorer/
|
||||||
|
|
||||||
|
右键菜单恢复工具,解除网站对右键的限制。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| -------------------------- | ------------- |
|
||||||
|
| `index.tsx` | 页面 UI |
|
||||||
|
| `useRightClickRestorer.ts` | 业务逻辑 Hook |
|
||||||
|
|
||||||
|
## 新增页面
|
||||||
|
|
||||||
|
1. 在 `types/storage.d.ts` 添加 `PageType` 联合类型成员
|
||||||
|
2. 在 `config/features.tsx` 的 `FEATURES` 数组添加配置
|
||||||
|
3. 在 `pages/` 创建页面目录(遵循上述结构)
|
||||||
|
4. 在 `i18n/locales/{zh,en}/features.json` 添加翻译
|
||||||
|
5. 如需新权限,更新 `wxt.config.ts`
|
||||||
|
6. 添加对应的单元测试
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# providers/
|
||||||
|
|
||||||
|
React Context Provider 目录,为整个应用提供全局共享状态。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ----------------------- | ---------------------- |
|
||||||
|
| `AppRoot.tsx` | 应用根 Provider 组合器 |
|
||||||
|
| `RouterProvider.tsx` | 路由 Context Provider |
|
||||||
|
| `ThemeModeProvider.tsx` | 主题模式 Provider |
|
||||||
|
|
||||||
|
## AppRoot.tsx
|
||||||
|
|
||||||
|
应用根 Provider 组合器,按顺序包裹:
|
||||||
|
|
||||||
|
```
|
||||||
|
React.StrictMode
|
||||||
|
└── ThemeModeProvider
|
||||||
|
└── RouterProvider
|
||||||
|
└── children
|
||||||
|
```
|
||||||
|
|
||||||
|
## RouterProvider.tsx
|
||||||
|
|
||||||
|
路由 Context Provider,管理:
|
||||||
|
|
||||||
|
- **当前页面**:`currentPage`(`PageType`)
|
||||||
|
- **可见页面列表**:`visiblePages`
|
||||||
|
- **页面排序**:`pageOrder`
|
||||||
|
- **加载状态**:`isLoaded`
|
||||||
|
|
||||||
|
核心特性:
|
||||||
|
|
||||||
|
- 通过 `chrome.storage` 持久化路由状态
|
||||||
|
- 使用 `localStorage` 快照实现首屏 0 闪烁
|
||||||
|
- 支持 popup/sidepanel/tab 三种入口的独立路由同步(通过 `syncKey`、`visiblePagesKey`、`pageOrderKey` 配置)
|
||||||
|
- 处理右键菜单待处理数据的路由跳转
|
||||||
|
- 监听 `chrome.storage.onChanged` 实现跨端同步
|
||||||
|
|
||||||
|
导出:
|
||||||
|
|
||||||
|
- `RouterProvider` 组件
|
||||||
|
- `useRouter()` Hook — 获取 `currentPage`、`visiblePages`、`pageOrder`、`navigateTo`、`goBack` 等
|
||||||
|
|
||||||
|
## ThemeModeProvider.tsx
|
||||||
|
|
||||||
|
主题模式 Provider,管理:
|
||||||
|
|
||||||
|
- **主题模式**:`light` / `dark` / `system`
|
||||||
|
- **解析后的主题**:`resolvedTheme`(`light` / `dark`)
|
||||||
|
|
||||||
|
核心特性:
|
||||||
|
|
||||||
|
- 使用 `localStorage` 快照实现首屏 0 闪烁
|
||||||
|
- 监听系统级暗色模式变化(`matchMedia`)
|
||||||
|
- 通过 `chrome.storage` 跨端同步主题偏好
|
||||||
|
- 自动在 `document.documentElement` 上切换 `dark` class
|
||||||
|
|
||||||
|
导出:
|
||||||
|
|
||||||
|
- `ThemeModeProvider` 组件
|
||||||
|
- `useThemeMode()` Hook — 获取 `themeMode`、`resolvedTheme`、`setThemeMode`
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# public/
|
||||||
|
|
||||||
|
静态资源目录,存放无需构建处理的文件,会被直接复制到输出目录。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件/目录 | 用途 |
|
||||||
|
| -------------- | -------------------------------- |
|
||||||
|
| `icon/` | 扩展图标,提供多种尺寸 |
|
||||||
|
| `icon/16.png` | 16×16 图标(工具栏) |
|
||||||
|
| `icon/32.png` | 32×32 图标 |
|
||||||
|
| `icon/48.png` | 48×48 图标(扩展管理页) |
|
||||||
|
| `icon/96.png` | 96×96 图标 |
|
||||||
|
| `icon/128.png` | 128×128 图标(Chrome Web Store) |
|
||||||
|
| `wxt.svg` | WXT 框架标志 SVG 图标 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 修改图标后需同步更新 `wxt.config.ts` 中的 manifest 配置
|
||||||
|
- 图标格式推荐使用 PNG,确保透明背景
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# src/
|
||||||
|
|
||||||
|
源码目录,存放全局样式定义。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ----------- | ----------------- |
|
||||||
|
| `index.css` | 全局 CSS 入口文件 |
|
||||||
|
|
||||||
|
## index.css
|
||||||
|
|
||||||
|
全局样式入口,包含:
|
||||||
|
|
||||||
|
- **Tailwind 指令**:`@tailwind base/components/utilities`
|
||||||
|
- **shadcn/ui CSS 变量**:定义 `--background`、`--primary`、`--destructive`、`--card`、`--muted`、`--accent`、`--border`、`--ring` 等语义化颜色变量
|
||||||
|
- **主题色值**:`:root`(亮色)和 `.dark`(暗色)两套完整的颜色定义
|
||||||
|
- **圆角变量**:`--radius` 定义全局圆角大小
|
||||||
|
|
||||||
|
## 修改注意事项
|
||||||
|
|
||||||
|
- 修改 CSS 变量会影响所有使用 shadcn/ui 语义化 token 的组件
|
||||||
|
- 新增颜色变量需同时在 `:root` 和 `.dark` 中定义
|
||||||
|
- 避免在组件中硬编码颜色值,应使用 CSS 变量或 Tailwind 的语义化类名
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# types/
|
||||||
|
|
||||||
|
全局共享的 TypeScript 类型声明文件目录。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
### storage.d.ts
|
||||||
|
|
||||||
|
核心类型定义文件,包含:
|
||||||
|
|
||||||
|
**页面类型:**
|
||||||
|
|
||||||
|
- `PageType` — 所有页面类型的联合类型(`dashboard` | `timestamp` | `storageCleaner` | ...)
|
||||||
|
- `JsonToolsPageMode` — JSON 工具子模式(`diff` | `format` | `yaml` | `toml` | `minify`)
|
||||||
|
- `Base64ConverterPageMode` — Base64 子模式(`text` | `file` | `image`)
|
||||||
|
- `Base64ConvertDirection` — 编解码方向(`encode` | `decode`)
|
||||||
|
- `MarkdownToHtmlPreviewMode` — Markdown 预览模式(`split` | `preview` | `html`)
|
||||||
|
- `HtmlToMarkdownPreviewMode` — HTML 预览模式(`split` | `preview` | `markdown`)
|
||||||
|
|
||||||
|
**存储 Schema:**
|
||||||
|
|
||||||
|
- `StorageSchema` — Chrome Storage 完整数据结构定义,所有存储键必须在此声明
|
||||||
|
- 键名使用 kebab-case 格式(如 `app/currentRoute`)
|
||||||
|
- 包含路由、主题、工具偏好、搜索历史等所有持久化数据
|
||||||
|
|
||||||
|
**其他类型:**
|
||||||
|
|
||||||
|
- `FormMapEntry` — 表单映射条目定义
|
||||||
|
- `ContextMenuPendingData` — 右键菜单待处理数据
|
||||||
|
- `StorageCleanerPreferences` / `StorageCleanerOptions` — 存储清理偏好
|
||||||
|
- `CleaningResult` / `StorageCleanResult` — 清理结果类型
|
||||||
|
|
||||||
|
### qrious.d.ts
|
||||||
|
|
||||||
|
`qrious` 库的类型声明,定义 QR 码生成选项和 `QRious` 类。
|
||||||
|
|
||||||
|
## 修改 StorageSchema 的注意事项
|
||||||
|
|
||||||
|
修改 `StorageSchema` 时,必须:
|
||||||
|
|
||||||
|
1. 在 `utils/chromeStorage.ts` 添加版本迁移函数
|
||||||
|
2. 在测试中覆盖迁移场景
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# utils/
|
||||||
|
|
||||||
|
通用工具函数和 React 自定义 Hooks 目录,与具体页面解耦。
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| -------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
|
| `chromeStorage.ts` | Chrome Storage API 封装:类型安全的 `StorageUtils` 类,提供 `get/set/remove` 方法 |
|
||||||
|
| `chromeTabs.ts` | Chrome Tabs API 封装:获取活动标签页、获取域名、在新标签页打开扩展页面 |
|
||||||
|
| `clipboard.ts` | 剪贴板操作:`copyTextToClipboard`(文本)、`copyImageToClipboard`(图片) |
|
||||||
|
| `messages.ts` | 扩展消息通信:基于 `@webext-core/messaging`,定义 `MessageAction` 枚举和 `ProtocolMap` 类型安全映射 |
|
||||||
|
| `contextMenu.ts` | 右键菜单配置与操作:定义菜单项、创建菜单、解析点击事件、ID→PageType 映射 |
|
||||||
|
| `base64Converter.ts` | Base64 编解码:文本↔Base64、文件↔Base64、图片预览,定义文件大小限制和图像 MIME 类型 |
|
||||||
|
| `jwt.ts` | JWT 解析:Base64URL 解码、解析 Header/Payload/Signature、JSON 格式化输出 |
|
||||||
|
| `jsonFormatter.ts` | JSON 格式化/压缩:支持缩进、按键排序、minify |
|
||||||
|
| `jsonToYaml.ts` | JSON→YAML 转换 |
|
||||||
|
| `jsonToToml.ts` | JSON→TOML 转换 |
|
||||||
|
| `markdownToHtml.ts` | Markdown→HTML 转换:基于 `marked` 库,支持 GFM 和换行转换 |
|
||||||
|
| `htmlToMarkdown.ts` | HTML→Markdown 转换:基于 DOMParser 解析 |
|
||||||
|
| `qrCodeParser.ts` | 二维码解析:基于 `qr-scanner` 库从文件中解析二维码 |
|
||||||
|
| `storageCleaner.ts` | 存储清理:获取当前标签页、检测受限 URL、计算 Cookie/Storage 大小、清理操作 |
|
||||||
|
| `textStatistics.ts` | 文本统计:使用 `Intl.Segmenter` 计算字符数/单词数/行数/字节大小 |
|
||||||
|
| `format.ts` | 通用格式化:`formatBytes` 将字节转为可读字符串(B/KB/MB/GB/TB) |
|
||||||
|
| `dayjs.ts` | Day.js 初始化:扩展 UTC、Timezone、RelativeTime 插件,加载中文本地化 |
|
||||||
|
|
||||||
|
## 自定义 Hooks
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| ----------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `useStorageState.ts` | Chrome Storage 状态 Hook:类似 `useState`,值自动同步到 `chrome.storage`,使用 `localStorage` 快照消除首屏闪烁 |
|
||||||
|
| `useLazyTranslation.ts` | 懒加载翻译 Hook:按需动态导入 i18n 命名空间,支持预加载和缓存 |
|
||||||
|
| `useContextMenuData.ts` | 右键菜单数据 Hook:从 storage 读取待处理数据,匹配 featureKey 后消费并触发回调 |
|
||||||
|
| `useDebounce.ts` | 防抖 Hook:对值进行延迟更新,避免频繁触发 |
|
||||||
|
|
||||||
|
## 使用约定
|
||||||
|
|
||||||
|
- 工具函数使用**命名导出**(`export function xxx()`)
|
||||||
|
- 工具函数**不抛异常**,返回包含 `hasError` 和 `error` 字段的结果对象
|
||||||
|
- Hook 使用 `use` 前缀命名,定义返回值接口类型
|
||||||
|
- 存储操作使用 `chromeStorage.ts` 的 `storageUtil` 封装,不要直接调用 `chrome.storage`
|
||||||
|
- 消息通信使用 `messages.ts` 的 `sendMessage`/`onMessage`,不要使用原生 `chrome.runtime.sendMessage`
|
||||||
Reference in New Issue
Block a user