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

﻿# PisellUrl 组件设计文档

> **组件定位**：PisellUrl 是 Pisell 组件库中的 URL 链接输入/展示组件，提供 URL 格式校验、协议自动补全、链接预览和跳转功能。组件基于
> Ant Design Input 组件开发，确保链接地址的正确性和使用便捷性。

## 1 组件概述

### 1.1 组件定位

PisellUrl 是一个专注于 URL 链接处理的输入组件，具有以下特点：

- **格式校验**：严格的 URL 格式校验
- **协议处理**：自动补全 http/https 协议
- **链接跳转**：只读态支持点击跳转（新窗口打开）
- **链接图标**：显示链接图标
- **协议隐藏**：只读态可选择隐藏协议部分
- **状态灵活切换**：支持只读、编辑、禁用三种状态
- **代码复用**：复用 PisellSingleLineText 的公共逻辑

### 1.2 使用场景

- 网站地址输入
- 社交媒体链接
- 文档/文件链接
- 产品详情页链接
- 外部资源链接

### 1.3 设计稿

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

### 1.4 与 SingleLineText 的关系

- **独立组件**：PisellUrl 是独立组件，不是继承关系
- **复用逻辑**：复用 `useTextInputState`、`validateValue` 等公共 hooks 和 utils
- **扩展功能**：在单行文本基础上增加 URL 校验、协议处理、链接跳转等特性

## 2 组件状态设计

### 2.1 三种状态模式

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

| 状态      | 视觉表现                      | 交互行为                       | 透明度  | 使用场景          |
|---------|---------------------------|----------------------------|------|---------------|
| **只读态** | 显示链接地址，可点击跳转，显示链接图标，可隐藏协议 | 不可编辑，点击新窗口打开链接             | 100% | 信息展示、链接查看     |
| **编辑态** | 显示输入框，自动补全协议，实时校验格式       | 可输入，实时校验，自动补全协议            | 100% | 表单输入、URL 地址编辑 |
| **禁用态** | 输入框灰化显示                   | 不可编辑，无 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 URL 格式校验

**校验方法**：

```
// 方法1：使用 URL 构造函数（推荐）
function isValidUrl(url: string): boolean {
  try {
    new URL(url);
    return true;
  } catch (error) {
    return false;
  }
}

// 方法2：使用正则表达式
const URL_REGEX = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
function validateUrlByRegex(url: string): boolean {
  return URL_REGEX.test(url);
}

// 更严格的正则（支持端口、查询参数等）
const STRICT_URL_REGEX = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
```

**分部校验**：

```
interface UrlParts {
  protocol?: string; // http, https, ftp
  host: string; // www.example.com
  port?: string; // 8080
  pathname?: string; // /path/to/page
  search?: string; // ?key=value
  hash?: string; // #section
}

function parseUrl(url: string): UrlParts | null {
  try {
    const urlObj = new URL(url);
    return {
      protocol: urlObj.protocol.replace(':', ''),
      host: urlObj.hostname,
      port: urlObj.port,
      pathname: urlObj.pathname,
      search: urlObj.search,
      hash: urlObj.hash
    };
  } catch {
    return null;
  }
}

function validateUrlParts(url: string): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];
  const parts = parseUrl(url);
  if (!parts) {
    errors.push('URL 格式不正确');
    return { isValid: false, errors };
  }
  const validProtocols = ['http', 'https', 'ftp'];
  if (parts.protocol && !validProtocols.includes(parts.protocol)) errors.push(`不支持的协议：${parts.protocol}`);
  if (!parts.host || parts.host.length === 0) errors.push('缺少域名');
  return { isValid: errors.length === 0, errors };
}
```

**配置项**：

- `strictValidation`：是否使用严格校验（默认 true）
- `allowedProtocols`：允许的协议列表（默认 ['http', 'https']）
- `requireProtocol`：是否要求必须有协议（默认 false）

### 3.2 协议自动补全

**协议处理**：

```
// 自动补全协议
function normalizeUrl(url: string, defaultProtocol: string = 'https://'): string {
  if (!url) return '';
  const trimmedUrl = url.trim();
  if (/^[a-z]+:\/\//i.test(trimmedUrl)) return trimmedUrl;
  return defaultProtocol + trimmedUrl;
}

// 智能补全（识别常见域名）
function smartNormalizeUrl(url: string): string {
  if (!url) return '';
  const trimmedUrl = url.trim();
  if (/^[a-z]+:\/\//i.test(trimmedUrl)) return trimmedUrl;
  if (trimmedUrl.startsWith('localhost') || trimmedUrl.startsWith('127.0.0.1')) return 'http://' + trimmedUrl;
  return 'https://' + trimmedUrl;
}
```

**配置项**：

- `autoCompleteProtocol`：是否自动补全协议（默认 true）
- `defaultProtocol`：默认协议（'http://' | 'https://'，默认 'https://'）
- `onProtocolComplete`：协议补全回调

### 3.3 链接跳转功能

**跳转实现**：

```
// 链接跳转函数
function handleUrlClick(url: string, options?: UrlClickOptions) {
  if (!url) return;
  const normalizedUrl = normalizeUrl(url, options?.defaultProtocol);
  if (!isValidUrl(normalizedUrl)) {
    console.warn('Invalid URL:', normalizedUrl);
    return;
  }
  const target = options?.openInNewWindow !== false ? '_blank' : '_self';
  const features = options?.openInNewWindow !== false ? 'noopener,noreferrer' : undefined;
  if (target === '_blank') window.open(normalizedUrl, target, features);
  else window.location.href = normalizedUrl;
  options?.onClick?.(normalizedUrl);
}

interface UrlClickOptions {
  defaultProtocol?: string;
  openInNewWindow?: boolean;
  onClick?: (url: string) => void;
}
```

**配置项**：

- `enableLinkClick`：是否启用链接跳转（默认 true）
- `openInNewWindow`：是否新窗口打开（默认 true）
- `showLinkIcon`：是否显示链接图标
- `onLinkClick`：链接点击回调

### 3.4 协议隐藏

**隐藏协议显示**：

```
// 隐藏协议部分，显示 www.example.com
function hideUrlProtocol(url: string): string {
  return url.replace(/^https?:\/\//, '');
}

// 隐藏 www. 前缀
function hideWwwPrefix(url: string): string {
  return url.replace(/^(https?:\/\/)?(www\.)?/, '');
}

// 仅显示域名
function extractDomain(url: string): string {
  try {
    const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
    return urlObj.hostname;
  } catch {
    return url;
  }
}
```

**配置项**：

- `showProtocol`：是否显示协议（只读态，默认 false）
- `showWww`：是否显示 www 前缀（默认 false）
- `showPathname`：是否显示路径（默认 true）
- `displayFormat`：显示格式（'full' | 'domain' | 'custom'）

## 4 组件 Props 设计

### 4.1 完整 Props 定义

```
interface PisellUrlProps {
  value?: string;
  defaultValue?: string;
  mode?: 'read' | 'edit';
  disabled?: boolean;
  strictValidation?: boolean;
  allowedProtocols?: string[];
  requireProtocol?: boolean;
  autoCompleteProtocol?: boolean;
  defaultProtocol?: 'http://' | 'https://';
  onProtocolComplete?: (url: string) => void;
  enableLinkClick?: boolean;
  openInNewWindow?: boolean;
  showLinkIcon?: boolean;
  onLinkClick?: (url: string) => void;
  showProtocol?: boolean;
  showWww?: boolean;
  showPathname?: boolean;
  displayFormat?: 'full' | 'domain' | 'custom';
  customFormat?: (url: string) => string;
  fontSize?: string | number;
  fontWeight?: string | number;
  color?: string;
  linkColor?: string;
  placeholder?: string;
  maxLength?: number;
  allowClear?: boolean;
  autoFocus?: boolean;
  required?: boolean;
  customValidator?: (url: string) => boolean | string;
  validateTrigger?: 'onChange' | 'onBlur' | 'both';
  errorMessage?: string;
  onChange?: (value: string) => void;
  onBlur?: (value: string) => void;
  onFocus?: () => void;
  onValidate?: (isValid: boolean, errors: string[]) => void;
  className?: string;
  style?: React.CSSProperties;
  'aria-label'?: string;
  'aria-describedby'?: string;
}
```

### 4.2 与 SingleLineText 的差异

| 配置项                                     | SingleLineText | Url | 说明         |
|-----------------------------------------|----------------|-----|------------|
| strictValidation/allowedProtocols       | ❌              | ✅   | URL 格式校验   |
| autoCompleteProtocol/defaultProtocol    | ❌              | ✅   | 协议自动补全     |
| enableLinkClick/openInNewWindow         | ❌              | ✅   | 链接跳转功能     |
| showProtocol/showPathname/displayFormat | ❌              | ✅   | 协议/路径显示控制  |
| prefix/suffix/addon                     | ✅              | ❌   | URL 不需要前后缀 |

## 5 技术实现要点

### 5.1 基于 Ant Design Input

组件使用 Input 组件：

```
import { Input } from 'antd';
import { LinkOutlined } from '@ant-design/icons';

function EditView(props: PisellUrlProps) {
  const { value, handleChange, isValid, errors } = useTextInputState(props);
  const handleBlur = () => {
    if (props.autoCompleteProtocol !== false && value) {
      const normalized = normalizeUrl(value, props.defaultProtocol);
      if (normalized !== value) {
        handleChange(normalized);
        props.onProtocolComplete?.(normalized);
      }
    }
    textState.handleBlur();
  };
  return (
    <div className="pisell-url-edit">
      <Input value={value} onChange={(e) => handleChange(e.target.value)} onBlur={handleBlur} prefix={<LinkOutlined />} ... />
      {!isValid && errors.length > 0 && <div className="pisell-url-error">{errors[0]}</div>}
    </div>
  );
}
```

### 5.2 只读态链接跳转实现

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

function ReadOnlyView(props: PisellUrlProps) {
  const url = props.value || '';
  const displayUrl = formatUrlDisplay(url, { showProtocol: props.showProtocol, showWww: props.showWww, showPathname: props.showPathname, displayFormat: props.displayFormat, customFormat: props.customFormat });
  const handleClick = () => {
    if (props.enableLinkClick !== false) {
      props.onLinkClick?.(url);
      handleUrlClick(url, { defaultProtocol: props.defaultProtocol, openInNewWindow: props.openInNewWindow });
    }
  };
  const canClick = props.enableLinkClick !== false;
  return (
    <div className="pisell-url-read">
      <Space size="small">
        {props.showLinkIcon !== false && <LinkOutlined style={{ color: props.linkColor || '#7F56DA' }} />}
        {canClick ? <Typography.Link onClick={handleClick} ...>{displayUrl}</Typography.Link> : <Typography.Text ...>{displayUrl}</Typography.Text>}
      </Space>
    </div>
  );
}
```

### 5.3 URL 校验实现

```
// URL 校验函数
function validateUrl(url: string, props: PisellUrlProps): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];
  if (props.required && !url?.trim()) {
    errors.push('请输入 URL 地址');
    return { isValid: false, errors };
  }
  if (!url) return { isValid: true, errors };
  const normalizedUrl = props.autoCompleteProtocol !== false ? normalizeUrl(url, props.defaultProtocol) : url;
  if (props.strictValidation !== false && !isValidUrl(normalizedUrl)) {
    errors.push(props.errorMessage || 'URL 地址格式不正确');
    return { isValid: false, errors };
  }
  if (props.requireProtocol && !url.match(/^[a-z]+:\/\//i)) errors.push('URL 必须包含协议（如 http:// 或 https://）');
  if (props.allowedProtocols?.length) {
    const parts = parseUrl(normalizedUrl);
    if (parts?.protocol && !props.allowedProtocols.includes(parts.protocol)) errors.push(`只允许以下协议：${props.allowedProtocols.join(', ')}`);
  }
  if (props.customValidator) {
    const result = props.customValidator(normalizedUrl);
    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 PisellUrl(props: PisellUrlProps) {
  const displayState = getDisplayState(props.mode, props.disabled);
  const textState = useTextInputState({
    ...props,
    validator: (value) => {
      const result = validateUrl(value, props);
      return result.isValid ? true : result.errors[0];
    }
  });
  // ... 组件实现
}
```

## 6 工具函数

### 6.1 URL 相关

```
// utils/urlHelper.ts

/**
 * 格式化 URL 显示
 */
export function formatUrlDisplay(url: string, options: { showProtocol?: boolean; showWww?: boolean; showPathname?: boolean; displayFormat?: 'full' | 'domain' | 'custom'; customFormat?: (url: string) => string }): string {
  if (!url) return '';
  if (options.displayFormat === 'custom' && options.customFormat) return options.customFormat(url);
  if (options.displayFormat === 'domain') return extractDomain(url);
  let display = url;
  if (options.showProtocol === false) display = hideUrlProtocol(display);
  if (options.showWww === false) display = display.replace(/^(https?:\/\/)?(www\.)/, '$1');
  if (options.showPathname === false) {
    try {
      const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
      display = urlObj.protocol + '//' + urlObj.hostname;
    } catch { }
  }
  return display;
}

/**
 * 检测是否为安全链接（https）
 */
export function isSecureUrl(url: string): boolean {
  return url.startsWith('https://');
}

/**
 * 提取URL参数
 */
export function extractUrlParams(url: string): Record<string, string> {
  try {
    const urlObj = new URL(url);
    const params: Record<string, string> = {};
    urlObj.searchParams.forEach((value, key) => { params[key] = value; });
    return params;
  } catch {
    return {};
  }
}
```

## 7 开发规范

### 7.1 文件结构

```
pisellUrl/
├── index.tsx
├── PisellUrl.tsx
├── PisellUrl.less
├── PisellUrl.test.tsx
├── types.ts
├── utils/
│   ├── index.ts
│   ├── urlValidator.ts
│   ├── urlHelper.ts
│   └── urlFormatter.ts
├── components/
│   ├── ReadOnlyView.tsx
│   ├── EditView.tsx
│   └── DisabledView.tsx
└── docs/
    ├── pisellUrl.md
    ├── pisellUrl.$tab-design.md
    ├── pisellUrl.$tab-dev.md
    └── pisellUrl.$tab-test.md
```

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

### 7.2 代码质量要求

1. **TypeScript 类型**：所有 Props 和 URL 配置必须有明确的类型定义
2. **注释规范**：校验、格式化函数需详细注释
3. **测试覆盖**：单元测试覆盖率不低于 85%，重点测试格式校验和协议处理
4. **安全性**：链接跳转需添加 noopener noreferrer

### 7.3 设计原则

1. **基于 Ant Design**：复用 Input、QRCode 组件
2. **逻辑复用**：复用 SingleLineText 的状态管理
3. **用户体验**：自动补全协议、智能校验
4. **安全性**：防止钓鱼链接、XSS 攻击

## 8 使用示例

### 8.1 基础用法

```
// 编辑态
<PisellUrl mode="edit" placeholder="请输入网址" required />

// 只读态（可跳转）
<PisellUrl mode="read" value="https://www.example.com" enableLinkClick openInNewWindow />
```

### 8.2 协议自动补全

```
<PisellUrl
  mode="edit"
  autoCompleteProtocol
  defaultProtocol="https://"
  onProtocolComplete={(url) => console.log('补全后:', url)}
/>
```

### 8.3 协议限制

```
// 仅允许 https
<PisellUrl mode="edit" allowedProtocols={['https']} errorMessage="仅支持 HTTPS 协议" />

// 必须包含协议
<PisellUrl mode="edit" requireProtocol errorMessage="请输入完整的 URL（包含 http:// 或 https://）" />
```

### 8.4 自定义显示格式

```
// 隐藏协议和路径，仅显示域名
<PisellUrl mode="read" value="https://www.example.com/path/to/page?key=value" showProtocol={false} showPathname={false} />
// 显示：example.com

// 完全自定义格式
<PisellUrl
  mode="read"
  value="https://www.example.com/long/path"
  displayFormat="custom"
  customFormat={(url) => `访问 ${extractDomain(url)}`}
/>
// 显示：访问 example.com
```

## 9 总结

PisellUrl 是一个专注于 URL 链接处理的输入组件，具备以下核心能力：

- ✅ 三种状态模式（只读、编辑、禁用）
- ✅ 严格的 URL 格式校验
- ✅ 协议自动补全（http/https）
- ✅ 协议白名单/黑名单
- ✅ 链接跳转（新窗口打开，安全性保障）
- ✅ 协议/路径显示控制
- ✅ 代码复用，复用 SingleLineText 公共逻辑

本组件与 PisellSingleLineText 共享公共逻辑（部分 hooks/utils），同时提供 URL 特有的校验、协议处理和链接跳转功能。
