---
title: 设计文档
order: 2
category: pro
---

# PisellEmail 组件设计文档

> **组件定位**：PisellEmail 是 Pisell 组件库中的电子邮箱输入/展示组件，提供邮箱格式校验、域名自动补全、快捷发送邮件功能。组件基于
> Ant Design Input 组件开发，确保邮箱地址的正确性和使用便捷性。

## 1 组件概述

### 1.1 组件定位

PisellEmail 是一个专注于邮箱地址处理的输入组件，具有以下特点：

- **格式校验**：严格的邮箱格式正则校验
- **域名补全**：输入时智能提示常用邮箱域名
- **邮件发送**：只读态支持点击唤起邮件客户端（mailto）
- **域名隐藏**：只读态可选择隐藏域名部分
- **状态灵活切换**：支持只读、编辑、禁用三种状态
- **代码复用**：复用 PisellSingleLineText 的公共逻辑

### 1.2 使用场景

- 用户注册/登录邮箱输入
- 联系方式信息录入
- 客户管理系统邮箱字段
- 工单/反馈系统邮箱
- 邮件订阅功能

### 1.3 设计稿

Figma 设计稿：https://www.figma.com/design/pM8Ho6d7kCMv9vIBvFHUlj/Pisell-2.0-Component-Library?node-id=3350-5733&m=dev

### 1.4 与 SingleLineText 的关系

- **独立组件**：PisellEmail 是独立组件，不是继承关系
- **复用逻辑**：复用 `useTextInputState`、`validateValue` 等公共 hooks 和 utils
- **扩展功能**：在单行文本基础上增加邮箱校验、域名补全、mailto 等特性

## 2 组件状态设计

### 2.1 三种状态模式

PisellEmail 支持三种状态，与 SingleLineText 保持一致：

| 状态      | 视觉表现                    | 交互行为                        | 透明度  | 使用场景        |
|---------|-------------------------|-----------------------------|------|-------------|
| **只读态** | 显示邮箱地址，可点击唤起邮件客户端，可隐藏域名 | 不可编辑，点击唤起邮件客户端（mailto:协议）   | 100% | 信息展示、联系方式查看 |
| **编辑态** | 显示输入框，输入时域名智能补全         | 可输入，实时校验格式，支持域名补全提示         | 100% | 表单输入、邮箱地址编辑 |
| **禁用态** | 输入框灰化显示                 | 不可编辑，无 hover/focus 状态，不可发邮件 | 40%  | 表单禁用、权限限制   |

### 2.2 状态切换逻辑

```
// 状态逻辑与 SingleLineText 一致
interface StateControl {
mode: 'read' | 'edit';
disabled?: boolean;
}

function getDisplayState(mode: string, disabled: boolean) {
if (disabled) return 'disabled';
return mode;
}
```

## 3 核心功能设计

### 3.1 邮箱格式校验

**校验正则**：

```
// 标准邮箱格式校验
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

// 更严格的校验（RFC 5322）
const STRICT_EMAIL_REGEX = /^(?:[a-z0-9!#$%&'*+/=?^_
`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$
/i;

**校验函数**：

function validateEmail(email: string, strict: boolean = false): boolean {
const regex = strict ? STRICT_EMAIL_REGEX : EMAIL_REGEX;
return regex.test(email);
}

// 分部校验
function validateEmailParts(email: string): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];

    // 1. 基本格式检查
    if (!email.includes('@')) {
        errors.push('邮箱地址必须包含 @ 符号');
        return { isValid: false, errors };
    }
    
    const [localPart, domainPart] = email.split('@');
    
    // 2. 本地部分校验
    if (!localPart || localPart.length === 0) {
        errors.push('@ 符号前不能为空');
    }
    if (localPart.length > 64) {
        errors.push('@ 符号前最多 64 个字符');
    }
    
    // 3. 域名部分校验
    if (!domainPart || domainPart.length === 0) {
        errors.push('@ 符号后不能为空');
    }
    if (!domainPart.includes('.')) {
        errors.push('域名必须包含 . 符号');
    }
    if (domainPart.endsWith('.')) {
        errors.push('域名不能以 . 结尾');
    }
    
    // 4. 顶级域名校验
    const parts = domainPart.split('.');
    const tld = parts[parts.length - 1];
    if (tld.length < 2) {
        errors.push('顶级域名至少 2 个字符');
    }
    
    return {
        isValid: errors.length === 0,
        errors
    };

}
```

**配置项**：

- `strictValidation`：是否使用严格校验（默认 false）
- `allowedDomains`：白名单域名列表（仅允许指定域名）
- `blockedDomains`：黑名单域名列表（禁止某些域名）

### 3.2 域名自动补全

**常用域名列表**：

```
const COMMON_EMAIL_DOMAINS = [
'gmail.com',
'qq.com',
'163.com',
'126.com',
'sina.com',
'outlook.com',
'hotmail.com',
'yahoo.com',
'icloud.com',
// 企业邮箱
'company.com',
];
```

**补全逻辑**：

```
// 域名补全函数
function getEmailSuggestions(
input: string,
domains: string[] = COMMON_EMAIL_DOMAINS
): string[] {
if (!input.includes('@')) {
return [];
}

    const [localPart, partialDomain] = input.split('@');
    
    if (!localPart) {
        return [];
    }
    
    // 过滤匹配的域名
    const matchedDomains = domains.filter(domain => 
        domain.toLowerCase().startsWith(partialDomain.toLowerCase())
    );
    
    // 返回完整邮箱建议
    return matchedDomains.map(domain => `${localPart}@${domain}`);

}

// 使用示例
getEmailSuggestions('user@g')  // ['user@gmail.com']
getEmailSuggestions('user@16') // ['user@163.com', 'user@126.com']
```

**补全组件**：

- 使用 Ant Design AutoComplete 组件
- 输入 @ 后自动显示建议
- 键盘上下键选择
- 回车或点击确认

### 3.3 邮件发送功能

**mailto 协议**：

```
// 只读态点击发送邮件
function handleEmailClick(email: string, options?: MailtoOptions) {
if (!email) return;

    // 构建 mailto URL
    let mailtoUrl = `mailto:${email}`;
    
    // 添加可选参数
    const params: string[] = [];
    if (options?.subject) {
        params.push(`subject=${encodeURIComponent(options.subject)}`);
    }
    if (options?.body) {
        params.push(`body=${encodeURIComponent(options.body)}`);
    }
    if (options?.cc) {
        params.push(`cc=${encodeURIComponent(options.cc)}`);
    }
    if (options?.bcc) {
        params.push(`bcc=${encodeURIComponent(options.bcc)}`);
    }
    
    if (params.length > 0) {
        mailtoUrl += '?' + params.join('&');
    }
    
    // 唤起邮件客户端
    window.location.href = mailtoUrl;

}

interface MailtoOptions {
subject?: string; // 邮件主题
body?: string; // 邮件正文
cc?: string; // 抄送
bcc?: string; // 密送
}
```

**配置项**：

- `enableMailto`：是否启用 mailto 功能（默认 true）
- `showEmailIcon`：是否显示邮件图标
- `mailtoOptions`：邮件默认参数
- `onMailto`：发送邮件回调

### 3.4 域名隐藏

**隐藏域名显示**：

// 隐藏域名部分，显示 u***@gmail.com
function hideEmailDomain(email: string): string {
const [localPart, domainPart] = email.split('@');

    if (!localPart || !domainPart) {
        return email;
    }
    
    // 本地部分脱敏
    const visibleChars = Math.min(2, localPart.length);
    const maskedLocal = localPart.substring(0, visibleChars) + '***';
    
    return `${maskedLocal}@${domainPart}`;

}

// 完全隐藏域名，显示 user@***
function maskEmailDomain(email: string): string {
const [localPart] = email.split('@');
return `${localPart}@***`;
}

**配置项**：

- `hideDomain`：是否隐藏域名（默认 false）
- `maskPattern`：脱敏模式（'partial' | 'full'）

## 4 组件 Props 设计

### 4.1 完整 Props 定义

```
interface PisellEmailProps {
// ========== 核心属性（继承自 SingleLineText）==========
value?: string;
defaultValue?: string;
mode?: 'read' | 'edit';
disabled?: boolean;

    // ========== 邮箱特有配置 ==========
    // 校验配置
    strictValidation?: boolean;        // 是否严格校验，默认 false
    allowedDomains?: string[];         // 白名单域名
    blockedDomains?: string[];         // 黑名单域名
    
    // 域名补全
    enableAutoComplete?: boolean;      // 是否启用自动补全，默认 true
    autoCompleteDomains?: string[];    // 自定义补全域名列表
    showAllSuggestions?: boolean;      // 是否显示所有建议（不过滤）
    
    // 邮件发送
    enableMailto?: boolean;            // 是否启用 mailto，默认 true
    showEmailIcon?: boolean;           // 是否显示邮件图标，默认 false
    mailtoOptions?: MailtoOptions;     // mailto 默认参数
    onMailto?: (email: string) => void; // 发送邮件回调
    
    // 显示配置
    hideDomain?: boolean;              // 是否隐藏域名（只读态）
    maskPattern?: 'partial' | 'full';  // 脱敏模式
    displayAsLink?: boolean;           // 是否显示为链接（只读态），默认 true
    
    // ========== 展示配置 ==========
    fontSize?: string | number;
    fontWeight?: string | number;
    color?: string;
    linkColor?: string;                // 链接颜色（只读态）
    
    // ========== 编辑配置 ==========
    placeholder?: string;              // 默认 '请输入邮箱地址'
    maxLength?: number;                // 默认 254（RFC 5321）
    allowClear?: boolean;
    autoFocus?: boolean;
    
    // ========== 校验配置 ==========
    required?: boolean;
    customValidator?: (email: string) => boolean | string;
    validateTrigger?: 'onChange' | 'onBlur' | 'both';
    errorMessage?: string;
    
    // ========== 事件回调 ==========
    onChange?: (value: string) => void;
    onBlur?: (value: string) => void;
    onFocus?: () => void;
    onSelect?: (value: string) => void;  // 选择补全建议回调
    onValidate?: (isValid: boolean, errors: string[]) => void;
    
    // ========== 样式类名 ==========
    className?: string;
    style?: React.CSSProperties;
    
    // ========== 无障碍访问 ==========
    'aria-label'?: string;
    'aria-describedby'?: string;
}
```

### 4.2 与 SingleLineText 的差异

| 配置项                                    | SingleLineText | Email | 说明       |
|----------------------------------------|----------------|-------|----------|
| strictValidation/allowedDomains        | ❌              | ✅     | 邮箱格式校验   |
| enableAutoComplete/autoCompleteDomains | ❌              | ✅     | 域名自动补全   |
| enableMailto/mailtoOptions             | ❌              | ✅     | 邮件发送功能   |
| hideDomain/maskPattern                 | ❌              | ✅     | 域名隐藏/脱敏  |
| displayAsLink/linkColor                | ❌              | ✅     | 链接样式     |
| prefix/suffix/addon                    | ✅              | ❌     | 邮箱不需要前后缀 |

## 5 技术实现要点

### 5.1 基于 Ant Design AutoComplete

组件使用 AutoComplete 实现域名补全：

```
import { AutoComplete, Space } from 'antd';
import { MailOutlined } from '@ant-design/icons';

// 编辑态渲染
function EditView(props: PisellEmailProps) {
const { value, handleChange, isValid, errors } = useTextInputState(props);
const [suggestions, setSuggestions] = useState<string[]>([]);

    // 生成补全建议
    const handleSearch = (searchText: string) => {
        if (props.enableAutoComplete !== false) {
            const domains = props.autoCompleteDomains || COMMON_EMAIL_DOMAINS;
            const suggestions = getEmailSuggestions(searchText, domains);
            setSuggestions(suggestions);
        }
    };
    
    // 选择建议
    const handleSelect = (value: string) => {
        handleChange(value);
        props.onSelect?.(value);
    };
    
    return (
        <div className="pisell-email-edit">
            <AutoComplete
                value={value}
                onChange={(val) => {
                    handleChange(val);
                    handleSearch(val);
                }}
                onSelect={handleSelect}
                onBlur={handleBlur}
                onFocus={handleFocus}
                options={suggestions.map(email => ({ value: email }))}
                placeholder={props.placeholder || '请输入邮箱地址'}
                maxLength={props.maxLength || 254}
                allowClear={props.allowClear}
                disabled={props.disabled}
                status={isValid ? undefined : 'error'}
                style={{ width: '100%' }}
            >
                <Input 
                    prefix={<MailOutlined />}
                    type="email"
                />
            </AutoComplete>
            
            {!isValid && errors.length > 0 && (
                <div className="pisell-email-error">
                    {errors[0]}
                </div>
            )}
        </div>
    );

}
```

### 5.2 只读态邮件发送实现

```
import { Typography, Button, Space } from 'antd';
import { MailOutlined } from '@ant-design/icons';

function ReadOnlyView(props: PisellEmailProps) {
const email = props.value || '';

    // 处理域名隐藏
    const displayEmail = props.hideDomain
        ? (props.maskPattern === 'full' 
            ? maskEmailDomain(email)
            : hideEmailDomain(email))
        : email;
    
    // 邮件发送处理
    const handleMailto = () => {
        if (props.enableMailto !== false) {
            props.onMailto?.(email);
            handleEmailClick(email, props.mailtoOptions);
        }
    };
    
    const canMailto = props.enableMailto !== false;
    const showAsLink = props.displayAsLink !== false && canMailto;
    
    return (
        <div className="pisell-email-read">
            <Space size="small">
                {showAsLink ? (
                    <Typography.Link
                        onClick={handleMailto}
                        style={{
                            fontSize: props.fontSize,
                            fontWeight: props.fontWeight,
                            color: props.linkColor || '#1890ff',
                        }}
                    >
                        {displayEmail}
                    </Typography.Link>
                ) : (
                    <Typography.Text
                        style={{
                            fontSize: props.fontSize,
                            fontWeight: props.fontWeight,
                            color: props.color,
                        }}
                    >
                        {displayEmail}
                    </Typography.Text>
                )}
                
                {props.showEmailIcon && canMailto && (
                    <Button
                        type="link"
                        size="small"
                        icon={<MailOutlined />}
                        onClick={handleMailto}
                    />
                )}
            </Space>
        </div>
    );

}
```

### 5.3 邮箱校验实现

```
// 邮箱校验函数
function validateEmail(
email: string,
props: PisellEmailProps
): { isValid: boolean; errors: string[] } {
const errors: string[] = [];

    // 1. 必填校验
    if (props.required && !email?.trim()) {
        errors.push('请输入邮箱地址');
        return { isValid: false, errors };
    }
    
    if (!email) {
        return { isValid: true, errors };
    }
    
    // 2. 格式校验
    const regex = props.strictValidation ? STRICT_EMAIL_REGEX : EMAIL_REGEX;
    if (!regex.test(email)) {
        errors.push(props.errorMessage || '邮箱地址格式不正确');
        return { isValid: false, errors };
    }
    
    // 3. 白名单校验
    if (props.allowedDomains && props.allowedDomains.length > 0) {
        const domain = email.split('@')[1];
        if (!props.allowedDomains.includes(domain)) {
            errors.push(`只允许以下域名：${props.allowedDomains.join(', ')}`);
        }
    }
    
    // 4. 黑名单校验
    if (props.blockedDomains && props.blockedDomains.length > 0) {
        const domain = email.split('@')[1];
        if (props.blockedDomains.includes(domain)) {
            errors.push('该邮箱域名不可用');
        }
    }
    
    // 5. 自定义校验
    if (props.customValidator) {
        const result = props.customValidator(email);
        if (result !== true) {
            errors.push(typeof result === 'string' ? result : '校验失败');
        }
    }
    
    return {
        isValid: errors.length === 0,
        errors
    };

}
```

### 5.4 复用 SingleLineText 逻辑

```
// 复用公共逻辑
import { useTextInputState } from '../pisellSingleLineText/hooks/useTextInputState';
import { getDisplayState } from '../pisellSingleLineText/utils/stateHelper';

// 扩展校验逻辑
function PisellEmail(props: PisellEmailProps) {
const displayState = getDisplayState(props.mode, props.disabled);

    // 使用公共状态管理，传入自定义校验
    const textState = useTextInputState({
        ...props,
        validator: (value) => {
            const result = validateEmail(value, props);
            return result.isValid ? true : result.errors[0];
        }
    });
    
    // ... 组件实现

}
```

## 6 工具函数

### 6.1 邮箱相关

// utils/emailHelper.ts

```
/**

* 提取邮箱域名
  */
  export function extractEmailDomain(email: string): string {
  const parts = email.split('@');
  return parts.length === 2 ? parts[1] : '';
  }

/**

* 提取邮箱本地部分
  */
  export function extractEmailLocal(email: string): string {
  const parts = email.split('@');
  return parts.length >= 1 ? parts[0] : '';
  }

/**
 * 规范化邮箱地址（转小写）
 */
export function normalizeEmail(email: string): string {
    return email.trim().toLowerCase();
}

/**
 * 检测是否为临时邮箱
 */
export function isDisposableEmail(email: string): boolean {
    const disposableDomains = [
        'tempmail.com',
        '10minutemail.com',
        'guerrillamail.com',
        // ... 更多临时邮箱域名
    ];
    
    const domain = extractEmailDomain(email);
    return disposableDomains.includes(domain);
}
```

## 7 开发规范

### 7.1 文件结构

```
pisellEmail/
├── index.tsx # 导出文件
├── PisellEmail.tsx # 组件主文件
├── PisellEmail.less # 样式文件
├── PisellEmail.test.tsx # 单元测试
├── types.ts # 类型定义
├── utils/ # 工具函数（邮箱特有）
│ ├── index.ts
│ ├── emailValidator.ts # 邮箱校验
│ ├── emailHelper.ts # 邮箱辅助函数
│ └── domainSuggestions.ts # 域名补全
├── components/ # 子组件
│ ├── ReadOnlyView.tsx # 只读态视图（mailto）
│ ├── EditView.tsx # 编辑态视图（自动补全）
│ └── DisabledView.tsx # 禁用态视图
├── constants/ # 常量
│ ├── emailDomains.ts # 常用域名列表
│ └── emailRegex.ts # 邮箱正则
└── docs/ # 文档
├── pisellEmail.md # 使用文档
├── pisellEmail.$tab-design.md # 设计文档（本文档）
├── pisellEmail.$tab-dev.md # 开发文档
└── pisellEmail.$tab-test.md # 测试文档
```

**注意**：部分公共逻辑复用 `pisellSingleLineText` 的 hooks。

### 7.2 代码质量要求

1. **TypeScript 类型**：所有 Props 和邮箱配置必须有明确的类型定义
2. **注释规范**：校验、补全函数需详细注释
3. **测试覆盖**：单元测试覆盖率不低于 85%，重点测试格式校验
4. **国际化支持**：错误提示支持多语言

### 7.3 设计原则

1. **基于 Ant Design**：复用 AutoComplete 组件
2. **逻辑复用**：复用 SingleLineText 的状态管理
3. **用户体验**：智能补全、即时校验
4. **隐私保护**：支持域名隐藏

## 8 使用示例

### 8.1 基础用法

// 编辑态

```
<PisellEmail
mode="edit"
placeholder="请输入邮箱地址"
required
/>
```

// 只读态（可发邮件）

``````````
<PisellEmail
mode="read"
value="user@example.com"
enableMailto
displayAsLink
/>

`````````

### 8.2 域名补全

````
<PisellEmail
mode="edit"
enableAutoComplete
autoCompleteDomains={[
'gmail.com',
'company.com',
'custom.com'
]}
/>

```
### 8.3 域名限制

// 白名单
```
<PisellEmail
mode="edit"
allowedDomains={['company.com', 'partner.com']}
errorMessage="请使用公司邮箱"
/>
```

// 黑名单
```
<PisellEmail
mode="edit"
blockedDomains={['tempmail.com', '10minutemail.com']}
errorMessage="不支持临时邮箱"
/>
```

### 8.4 mailto 配置

```
<PisellEmail
mode="read"
value="support@company.com"
mailtoOptions={{
subject: '产品咨询',
body: '您好，我想咨询...'
}}
onMailto={(email) => {
console.log('发送邮件至:', email);
}}
/>
```

### 8.5 域名隐藏

```
<PisellEmail
mode="read"
value="user@example.com"
hideDomain
maskPattern="partial"
/>
```
// 显示：u***@example.com

## 9 总结

PisellEmail 是一个专注于邮箱地址处理的输入组件，具备以下核心能力：

- ✅ 三种状态模式（只读、编辑、禁用）
- ✅ 严格的邮箱格式校验（支持 RFC 5322）
- ✅ 智能域名补全（常用邮箱域名）
- ✅ 域名白名单/黑名单
- ✅ mailto 邮件发送（一键唤起邮件客户端）
- ✅ 域名隐藏/脱敏（隐私保护）
- ✅ 代码复用，复用 SingleLineText 公共逻辑

本组件与 PisellSingleLineText 共享公共逻辑（部分 hooks/utils），同时提供邮箱特有的校验、补全和 mailto 功能。
