---
title: 开发文档
order: 3
category: pro
---

# PisellPhone 组件开发文档

## 1 组件信息

### 1.1 基本信息

- **组件名称**：PisellPhone
- **组件版本**：V1.0.0
- **组件分类**：表单输入 / 文本展示
- **开发状态**：开发中
- **最后更新**：2026-01-25

### 1.2 组件描述

PisellPhone 是一个专注于电话号码处理的输入/展示组件，支持只读、编辑、禁用三种状态模式。组件基于 Ant Design Input 和 Select
组件开发，提供电话号码自动格式化、国家码选择、格式校验和拨号功能，并复用 PisellSingleLineText 的公共逻辑。

## 2 需求点罗列

### 2.1 功能需求

#### 核心功能

- ✅ 支持三种状态：只读态、编辑态、禁用态
- ✅ 支持受控和非受控两种使用模式
- ✅ 基于 Ant Design Input + Select 组件封装
- ✅ 完整的校验框架（复用 @pisell/utils）
- ✅ 复用公共逻辑（@pisell/utils）

#### 电话号码特有功能

- ✅ 电话号码自动格式化（138 0000 0000）
- ✅ 国家码选择（预设24个常见国家）
- ✅ 国家码显示控制（过滤/置顶/自定义）
- ✅ 格式校验（根据国家码自动校验）
- ✅ 拨号功能（移动端 tel: 协议）
- ✅ 号码脱敏（138****0000）

#### 展示功能（只读态）

- ✅ 格式化或脱敏显示
- ✅ 显示国旗图标
- ✅ 可点击拨号（移动端）
- ✅ 拨号图标（可选）

#### 编辑功能（编辑态）

- ✅ 国家码下拉选择器
- ✅ 输入时只保留数字
- ✅ 实时格式化
- ✅ 根据国家码限制最大长度
- ✅ 校验和错误提示

#### 校验功能

- ✅ 必填校验
- ✅ 格式校验（长度、正则）
- ✅ 自定义校验
- ✅ 错误提示展示

### 2.2 非功能需求

- ✅ TypeScript 类型定义完整
- ✅ 代码注释使用 JSDoc 规范
- ✅ 性能优化（React.memo、useCallback、useMemo）
- ✅ 代码复用（hooks 和 utils 来自 @pisell/utils）
- ✅ 支持24个常见国家

## 3 组件 API

### 3.1 Props 定义

#### 核心属性（继承自 BaseTextInputProps）

| 属性名          | 类型               | 默认值    | 必填 | 说明          |
|--------------|------------------|--------|----|-------------|
| value        | string           | -      | 否  | 电话号码（不含国家码） |
| defaultValue | string           | -      | 否  | 默认值（非受控）    |
| mode         | 'read' \| 'edit' | 'edit' | 否  | 状态模式        |
| disabled     | boolean          | false  | 否  | 是否禁用        |

#### 电话号码特有配置

| 属性名                   | 类型                               | 默认值      | 必填 | 说明               |
|-----------------------|----------------------------------|----------|----|------------------|
| countryCode           | string                           | '+86'    | 否  | 当前选中的国家码         |
| defaultCountryCode    | string                           | '+86'    | 否  | 默认国家码            |
| showCountrySelector   | boolean                          | true     | 否  | 是否显示国家码选择器       |
| availableCountryCodes | string[]                         | 全部24个    | 否  | 可用的国家码列表（从预设中过滤） |
| commonCountryCodes    | string[]                         | -        | 否  | 常用国家码（置顶显示）      |
| countryCodes          | CountryCode[]                    | 预设24个    | 否  | 完全自定义国家码列表（覆盖预设） |
| onCountryCodeChange   | (code: string) => void           | -        | 否  | 国家码变更回调          |
| autoFormat            | boolean                          | true     | 否  | 是否自动格式化          |
| formatRule            | 'mobile' \| 'landline' \| 'auto' | 'auto'   | 否  | 格式化规则            |
| separator             | string                           | ' '      | 否  | 格式化分隔符           |
| enableDial            | boolean                          | true     | 否  | 是否启用拨号（移动端）      |
| showDialIcon          | boolean                          | false    | 否  | 是否显示拨号图标         |
| onDial                | (phone: string) => void          | -        | 否  | 拨号回调             |
| enableMasking         | boolean                          | false    | 否  | 是否脱敏显示（只读态）      |
| maskPattern           | 'middle' \| 'last' \| 'custom'   | 'middle' | 否  | 脱敏模式             |
| customMask            | (phone: string) => string        | -        | 否  | 自定义脱敏函数          |
| showFlag              | boolean                          | true     | 否  | 是否显示国旗图标         |

#### 展示配置（只读态）

| 属性名        | 类型               | 默认值     | 必填 | 说明   |
|------------|------------------|---------|----|------|
| fontSize   | string \| number | 16      | 否  | 字号   |
| fontWeight | string \| number | 500     | 否  | 字重   |
| color      | string           | #101828 | 否  | 文本颜色 |

#### 编辑配置（编辑态）

| 属性名         | 类型      | 默认值     | 必填 | 说明     |
|-------------|---------|---------|----|--------|
| placeholder | string  | 请输入电话号码 | 否  | 占位文本   |
| maxLength   | number  | 根据国家码自动 | 否  | 最大长度   |
| allowClear  | boolean | false   | 否  | 显示清空按钮 |
| autoFocus   | boolean | false   | 否  | 自动聚焦   |

#### 校验配置

| 属性名             | 类型                                                        | 默认值      | 必填 | 说明      |
|-----------------|-----------------------------------------------------------|----------|----|---------|
| required        | boolean                                                   | false    | 否  | 是否必填    |
| validateFormat  | boolean                                                   | true     | 否  | 是否校验格式  |
| customValidator | (phone: string, countryCode: string) => boolean \| string | -        | 否  | 自定义校验函数 |
| validateTrigger | 'onChange' \| 'onBlur' \| 'both'                          | 'onBlur' | 否  | 校验触发时机  |
| errorMessage    | string                                                    | -        | 否  | 自定义错误提示 |

#### 事件回调

| 属性名                 | 类型                                           | 说明      |
|---------------------|----------------------------------------------|---------|
| onChange            | (value: string) => void                      | 值变化回调   |
| onBlur              | (value: string) => void                      | 失焦回调    |
| onFocus             | () => void                                   | 聚焦回调    |
| onCountryCodeChange | (code: string) => void                       | 国家码变更回调 |
| onDial              | (phone: string) => void                      | 拨号回调    |
| onValidate          | (isValid: boolean, errors: string[]) => void | 校验回调    |

### 3.2 CountryCode 类型定义

```
interface CountryCode {
  code: string; // 国家码，如 '+86'
  name: string; // 国家名称，如 '中国'
  flag: string; // 国旗 emoji，如 '🇨🇳'
  format?: string; // 号码格式，如 'XXX XXXX XXXX'（可选）
  maxLength: number; // 最大长度
  pattern: RegExp; // 校验正则
}
```

## 4 组件模块拆解

### 4.1 目录结构

```
pisellPhone/
├── index.tsx
├── PisellPhone.tsx
├── PisellPhone.less
├── PisellPhone.test.tsx
├── PisellPhone.stories.tsx
├── types.ts
├── utils/
│   ├── index.ts
│   ├── phoneFormatter.ts
│   ├── phoneValidator.ts
│   └── countryCodeHelper.ts
├── components/
│   ├── ReadOnlyView.tsx
│   ├── EditView.tsx
│   └── DisabledView.tsx
├── constants/
│   └── countryCodes.ts
└── docs/
    ├── pisellPhone.md
    ├── pisellPhone.$tab-design.md
    ├── pisellPhone.$tab-dev.md
    └── pisellPhone.$tab-test.md
```

### 4.2 模块职责

#### 主组件（PisellPhone.tsx）

- 统筹组件渲染逻辑
- 根据 mode 和 disabled 决定渲染哪个子组件
- 管理国家码状态

#### 子组件

**EditView.tsx**（核心差异）

- 职责：渲染编辑态输入
- 功能：
  - 国家码 Select + 电话号码 Input（Space.Compact 组合）
  - 输入时只保留数字（replace(/\D/g, '')）
  - 自动格式化（根据国家码）
  - 根据国家码动态设置 maxLength
  - 校验和错误提示
- 依赖：Ant Design Input、Select、Space

**ReadOnlyView.tsx**

- 职责：渲染只读态展示和拨号
- 功能：
  - 显示国旗图标
  - 格式化或脱敏显示
  - 移动端可点击拨号（tel: 协议）
  - 拨号图标（可选）
- 依赖：Ant Design Typography.Text、Button

**DisabledView.tsx**

- 职责：渲染禁用态视图
- 功能：基于原始 mode 渲染，透明度 40%
- 依赖：ReadOnlyView 或 EditView

#### 工具函数（utils/）

**phoneFormatter.ts**

- formatPhone - 根据国家码格式化电话号码
- formatChinaMobile - 中国手机号格式化（138 0000 0000）
- formatUSPhone - 美国号码格式化 ((555) 123-4567)
- detectPhoneType - 检测号码类型（手机/固话）

**phoneValidator.ts**

- validatePhone - 电话号码校验（长度、格式）
- 根据国家码配置自动校验

**countryCodeHelper.ts**

- getCountryConfig - 根据国家码获取配置
- getCountryFlag - 获取国旗 emoji
- getMaxLength - 获取最大长度
- filterCountryCodes - 根据 availableCountryCodes 过滤
- sortCountryCodes - 根据 commonCountryCodes 排序

#### 常量（constants/）

**countryCodes.ts**

- PRESET_COUNTRY_CODES - 预设的24个国家码列表
- 包含：中国、香港、澳门、台湾、美国、加拿大、日本、韩国等

## 5 组件实现方案

### 5.1 技术栈

- React 18+
- TypeScript
- Ant Design 5.x（Input、Select、Space、Typography、Button）
- Less（样式）
- 公共逻辑来自 @pisell/utils

### 5.2 核心实现

#### 5.2.1 国家码配置处理逻辑

```
/**
 * 获取最终的国家码列表
 * 优先级：1. countryCodes（完全自定义） 2. availableCountryCodes（过滤预设） 3. PRESET_COUNTRY_CODES
 * 然后根据 commonCountryCodes 排序（置顶）
 */
function getCountryCodes(props: PisellPhoneProps): CountryCode[] {
  let codes: CountryCode[];
  if (props.countryCodes) {
    codes = props.countryCodes;
  } else if (props.availableCountryCodes) {
    codes = PRESET_COUNTRY_CODES.filter(c => props.availableCountryCodes!.includes(c.code));
  } else {
    codes = PRESET_COUNTRY_CODES;
  }
  if (props.commonCountryCodes) {
    const common = codes.filter(c => props.commonCountryCodes!.includes(c.code));
    const others = codes.filter(c => !props.commonCountryCodes!.includes(c.code));
    codes = [...common, ...others];
  }
  return codes;
}
```

#### 5.2.2 电话号码格式化

```
// utils/phoneFormatter.ts

/**
 * 中国大陆手机号格式化：138 0000 0000
 */
export function formatChinaMobile(phone: string): string {
  const cleaned = phone.replace(/\D/g, '');
  if (cleaned.length <= 3) return cleaned;
  if (cleaned.length <= 7) return `${cleaned.slice(0, 3)} ${cleaned.slice(3)}`;
  return `${cleaned.slice(0, 3)} ${cleaned.slice(3, 7)} ${cleaned.slice(7, 11)}`;
}

/**
 * 根据国家码格式化电话号码
 */
export function formatPhone(phone: string, countryCode: string): string {
  const cleaned = phone.replace(/\D/g, '');
  switch (countryCode) {
    case '+86': return formatChinaMobile(cleaned);
    case '+1': return formatUSPhone(cleaned);
    case '+44': return formatUKPhone(cleaned);
    default: return cleaned;
  }
}
```

#### 5.2.3 号码脱敏

```
// utils/phoneFormatter.ts

/**
 * 电话号码脱敏
 * @example maskPhone('13800000000', 'middle') // '138****0000'
 */
export function maskPhone(phone: string, pattern: 'middle' | 'last' = 'middle'): string {
  const cleaned = phone.replace(/\D/g, '');
  if (cleaned.length < 7) return cleaned;
  switch (pattern) {
    case 'middle': return cleaned.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
    case 'last': return cleaned.replace(/(\d{7})\d{4}/, '$1****');
    default: return cleaned;
  }
}
```

#### 5.2.4 EditView 实现（国家码选择 + 格式化输入）

```
import React, { useState, useMemo } from 'react';
import { Input, Select, Space } from 'antd';
import { useTextInputState } from '@pisell/utils';
import { formatPhone, getCountryCodes, getMaxLength } from '../utils';
import type { PisellPhoneProps } from '../types';

const EditView: React.FC<PisellPhoneProps> = (props) => {
  const [countryCode, setCountryCode] = useState(props.countryCode || props.defaultCountryCode || '+86');
  const countryCodes = useMemo(() => getCountryCodes(props), [props]);
  const { value, isValid, errors, handleChange, handleBlur, handleFocus } = useTextInputState(props);

  const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const cleaned = e.target.value.replace(/\D/g, '');
    if (props.autoFormat !== false) handleChange(formatPhone(cleaned, countryCode));
    else handleChange(cleaned);
  };

  return (
    <div className="pisell-phone-edit">
      <Space.Compact style={{ width: '100%' }}>
        {props.showCountrySelector !== false && (
          <Select value={countryCode} onChange={...} ...>
            {countryCodes.map((country) => <Select.Option key={...} value={country.code} ... />)}
          </Select>
        )}
        <Input value={value} onChange={handlePhoneChange} onBlur={handleBlur} ... />
      </Space.Compact>
      {!isValid && errors.length > 0 && <div className="pisell-phone-error">{errors[0]}</div>}
    </div>
  );
};
export default EditView;
```

#### 5.2.5 ReadOnlyView 实现（拨号）

```
import React from 'react';
import { Typography, Button, Space } from 'antd';
import { PhoneOutlined } from '@ant-design/icons';
import { formatPhone, maskPhone, getCountryFlag } from '../utils';
import type { PisellPhoneProps } from '../types';

const ReadOnlyView: React.FC<PisellPhoneProps> = (props) => {
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  const countryCode = props.countryCode || '+86';
  const displayValue = props.enableMasking
    ? maskPhone(props.value || '', props.maskPattern)
    : formatPhone(props.value || '', countryCode);
  const handleDial = () => {
    if (props.enableDial !== false && isMobile) {
      const cleaned = (props.value || '').replace(/\D/g, '');
      const fullNumber = countryCode + cleaned;
      props.onDial?.(fullNumber);
      window.location.href = `tel:${fullNumber}`;
    }
  };
  const canDial = props.enableDial !== false && isMobile;
  return (
    <div className="pisell-phone-read">
      <Space size="small">
        {props.showFlag !== false && <span className="pisell-phone-flag">{getCountryFlag(countryCode)}</span>}
        <Typography.Text onClick={canDial ? handleDial : undefined} ...>{displayValue}</Typography.Text>
        {props.showDialIcon && canDial && <Button type="link" icon={<PhoneOutlined />} onClick={handleDial} />}
      </Space>
    </div>
  );
};
export default ReadOnlyView;
```

## 6 组件组成与引用

### 6.1 依赖的外部组件和工具

| 组件/工具             | 来源                | 用途        |
|-------------------|-------------------|-----------|
| Input             | antd              | 电话号码输入框   |
| Select            | antd              | 国家码选择器    |
| Space.Compact     | antd              | 组合国家码和输入框 |
| Typography.Text   | antd              | 只读态文本展示   |
| Button            | antd              | 拨号按钮      |
| PhoneOutlined     | @ant-design/icons | 拨号图标      |
| useTextInputState | @pisell/utils     | 状态管理（复用）  |
| getDisplayState   | @pisell/utils     | 状态计算（复用）  |

### 6.2 组件结构图

```
PisellPhone
├── EditView
│   ├── Space.Compact
│   │   ├── Select（国家码）
│   │   └── Input（电话号码）
│   └── 错误提示
├── ReadOnlyView
│   ├── 国旗图标
│   ├── Typography.Text（号码，可点击拨号）
│   └── Button（拨号图标，可选）
└── DisabledView
    ├── EditView（复用）
    └── ReadOnlyView（复用）
```

### 6.3 复用关系

```
PisellSingleLineText（基础组件）
↓ 公共逻辑（@pisell/utils）
PisellPhone（电话号码组件）
↓ 复用
├── useTextInputState（状态管理）
├── validateValue（基础校验）
└── getDisplayState（状态计算）
↓ 扩展
├── formatPhone（格式化）
├── validatePhone（电话校验）
└── 国家码配置系统
```

## 7 扩展性设计

### 7.1 通用性

- 预设24个常见国家，开箱即用
- 支持过滤、置顶、完全自定义
- 不绑定业务逻辑
- 支持自定义格式化和校验

### 7.2 可扩展点

1. **自定义国家码列表**：通过 countryCodes 完全覆盖
2. **过滤显示国家**：通过 availableCountryCodes 过滤
3. **常用国家置顶**：通过 commonCountryCodes 排序
4. **自定义格式化**：通过修改 formatPhone 函数
5. **自定义脱敏**：通过 customMask 函数

### 7.3 国际化扩展

支持添加更多国家：

```
const newCountry: CountryCode = {
  code: '+xxx',
  name: '国家名',
  flag: '🏳️',
  maxLength: 10,
  pattern: /^\d{10}$/
};
```

## 8 性能优化

### 8.1 优化策略

1. **React.memo**：避免不必要的重渲染
2. **useMemo**：缓存国家码列表的过滤和排序
3. **useCallback**：缓存事件处理函数
4. **复用公共逻辑**：减少代码量

### 8.2 性能指标

- 首次渲染：< 20ms
- 重渲染：< 10ms
- 国家码切换：< 5ms

## 9 开发注意事项

### 9.1 代码规范

1. 使用 TypeScript 严格模式
2. 所有函数添加 JSDoc 注释
3. 遵循 ESLint 和 Prettier 规则
4. 复用 @pisell/utils 中的公共逻辑

### 9.2 关键实现点

1. **只保留数字**：

- 使用 `replace(/\D/g, '')` 移除非数字字符
- 在 handleChange 之前处理

2. **国家码配置优先级**：

- countryCodes > availableCountryCodes > PRESET_COUNTRY_CODES
- 然后根据 commonCountryCodes 排序

3. **移动端检测**：

- 使用 navigator.userAgent 检测
- 只在移动端启用拨号功能

4. **格式化时机**：

- 输入时实时格式化（如果 autoFormat=true）
- 只读态显示时格式化

5. **校验逻辑**：

- 基础校验复用 validateValue
- 电话格式校验使用 validatePhone
- 两者结合使用

## 10 总结

PisellPhone 是一个功能丰富的电话号码输入组件，通过复用 @pisell/utils
的公共逻辑，大幅减少了开发工作量。组件提供了灵活的国家码配置系统（预设24国 + 过滤 + 置顶 + 自定义），支持自动格式化、格式校验和移动端拨号等特性。

**核心优势**：

- ✅ 预设24个常见国家，开箱即用
- ✅ 灵活的国家码配置（4种方式）
- ✅ 自动格式化和智能校验
- ✅ 移动端拨号和号码脱敏
- ✅ 完整复用公共逻辑（70% 代码复用）
- ✅ 与 SingleLineText 一致的 API 风格
