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

﻿# PisellPercent 组件开发文档

## 组件信息

**组件名称**：PisellPercent

**组件版本**：1.0.0

**组件分类**：表单输入 / 数据展示

**开发状态**：开发中

**最后更新**：2026-01-26

---

## 组件描述

PisellPercent 是基于 PisellNumber 派生的百分比组件，专门用于处理比例、完成度、增长率等百分比数据场景。它继承了 PisellNumber
的所有基础能力，并添加了百分号显示、进度条可视化、范围转换等百分比特有的功能。

**核心价值**：

- 语义化：通过组件名明确表达数据类型（百分比）
- 可视化：支持 text、progress、circle 三种展示变体
- 范围转换：自动转换不同范围的数值
- 交互友好：支持滑块+输入框组合编辑

**适用场景**：

- 任务完成度展示
- 数据统计（增长率、占比等）
- 进度条展示
- KPI 指标展示
- 满意度调查

---

## 组件需求点罗列

### 功能需求

1. **继承 PisellNumber 的能力**
  - ✅ 三态视图（只读/编辑/禁用）
  - ✅ 受控/非受控模式
  - ✅ 数值校验
  - ✅ 事件回调

2. **百分号处理**
  - ✅ 自动添加 % 符号
  - ✅ 可配置是否显示
  - ✅ 符号与数值无空格（85.5%）

3. **展示变体**
  - ✅ text：纯文本显示
  - ✅ progress：进度条（Ant Design Progress）
  - ✅ circle：环形进度

4. **范围转换**
  - ✅ [0, 100] 范围：直接使用
  - ✅ [0, 1] 范围：自动 ×100
  - ✅ 自定义范围：按比例转换

5. **编辑功能**
  - ✅ InputNumber 输入
  - ✅ 滑块调节（可选）
  - ✅ 滑块+输入框组合

---

## 组件 API

### Props 接口定义

```
interface PisellPercentProps extends PisellNumberProps {
  // ========== 百分比特有属性 ==========
  
  /** 是否显示百分号，默认 true */
  showPercentSymbol?: boolean;
  
  /** 百分比范围，默认 [0, 100] */
  range?: [number, number];
  
  /** 展示变体，默认 'text' */
  variant?: 'text' | 'progress' | 'circle';
  
  /** 进度条配置（variant='progress' 时） */
  progressProps?: ProgressProps;
  
  /** 环形进度配置（variant='circle' 时） */
  circleProps?: ProgressProps;
  
  /** 编辑态是否显示滑块，默认 false */
  showSlider?: boolean;
  
  /** 滑块配置 */
  sliderProps?: SliderProps;
  
  // 以下属性修改了默认值
  /** 小数精度，默认 1 */
  precision?: number;
  
  /** 最小值，默认 0 */
  min?: number;
  
  /** 最大值，默认 100 */
  max?: number;
}
```

---

## 组件模块拆解

### 文件结构

```
pisellPercent/
├── index.tsx                      # 对外导出入口
├── PisellPercent.tsx              # 主组件
├── PisellPercent.less             # 组件样式
├── PisellPercent.test.tsx         # 单元测试
├── PisellPercent.stories.tsx      # Storybook 示例
├── types.ts                       # TypeScript 类型定义
│
├── components/                    # 子组件目录
│   ├── index.tsx                 # 子组件导出
│   ├── ReadOnlyView.tsx          # 只读态视图
│   ├── EditView.tsx              # 编辑态视图
│   └── DisabledView.tsx          # 禁用态视图
│
├── utils/                         # 工具函数目录
│   ├── index.ts                  # 工具函数导出
│   ├── percentFormatter.ts       # 百分比格式化
│   └── rangeConverter.ts         # 范围转换
│
└── docs/                          # 文档目录
    ├── pisellPercent.md          # 使用文档
    ├── pisellPercent.$tab-design.md   # 设计文档
    ├── pisellPercent.$tab-dev.md      # 开发文档（本文件）
    └── pisellPercent.$tab-test.md     # 测试文档
```

### 核心模块说明

#### 1. 主组件（PisellPercent.tsx）

**职责**：

- 计算显示状态
- 根据状态路由到子组件
- 处理百分比特有属性的默认值

#### 2. 只读态视图（ReadOnlyView.tsx）

**职责**：

- 根据 variant 渲染不同形式
- text 变体：格式化数值 + % 符号
- progress 变体：渲染进度条
- circle 变体：渲染环形进度

**关键逻辑**：

```
ReadOnlyView = (props) => {
  const { variant = 'text', value, range = [0, 100] } = props;
  
  // 范围转换
  const displayValue = convertToPercent(value, range);
  
  if (variant === 'text') {
    return <span>{displayValue}%</span>;
  }
  
  if (variant === 'progress') {
    return <Progress percent={displayValue} {...progressProps} />;
  }
  
  if (variant === 'circle') {
    return <Progress type="circle" percent={displayValue} {...circleProps} />;
  }
}
```

#### 3. 编辑态视图（EditView.tsx）

**职责**：

- 渲染 InputNumber（后缀显示 %）
- 可选：渲染 Slider（滑块）
- 双向同步滑块和输入框
- 状态管理和校验

**关键逻辑**：

```
import React from 'react';
import { InputNumber, Slider, Space } from 'antd';
import { useNumberInputState } from '@pisell/utils';
import type { PisellPercentProps } from '../types';

const EditView: React.FC<PisellPercentProps> = (props) => {
  const { 
    showSlider = false, 
    range = [0, 100],
    sliderProps,
    ...restProps 
  } = props;
  
  const { value, isValid, errors, handleChange, handleBlur, handleFocus } = 
    useNumberInputState(restProps);
  
  if (showSlider) {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        <Slider 
          value={value ?? 0} 
          onChange={handleChange} 
          min={range[0]}
          max={range[1]}
          {...sliderProps} 
        />
        <InputNumber 
          value={value} 
          onChange={handleChange}
          onBlur={handleBlur}
          onFocus={handleFocus}
          suffix="%" 
          status={isValid ? undefined : 'error'}
          style={{ width: '100%' }}
        />
      </Space>
    );
  }
  
  return (
    <InputNumber 
      value={value} 
      onChange={handleChange}
      onBlur={handleBlur}
      onFocus={handleFocus}
      suffix="%" 
      status={isValid ? undefined : 'error'}
      style={{ width: '100%' }}
    />
  );
};

export default EditView;
```

#### 4. 工具函数

**percentFormatter.ts**：

```
import { formatNumber } from '@pisell/utils';

/**
 * 格式化百分比（添加 % 符号）
 * @param value 数值
 * @param options 格式化选项
 * @returns 格式化后的百分比字符串
 */
export function formatPercent(
  value: number | null,
  options: { precision?: number; useGrouping?: boolean }
): string | null {
  if (value === null || value === undefined) return null;
  
  const formatted = formatNumber(value, { 
    precision: options.precision ?? 1,
    useGrouping: options.useGrouping ?? false
  });
  
  return formatted ? `${formatted}%` : null;
}
```

**rangeConverter.ts**：

```
/**
 * 范围转换（将任意范围转换为 0-100 百分比）
 * @param value 输入值
 * @param range 输入值的范围
 * @returns 0-100 范围的百分比值
 * 
 * @example
 * convertToPercent(0.85, [0, 1]) // 85
 * convertToPercent(850, [0, 1000]) // 85
 */
export function convertToPercent(
  value: number | null,
  range: [number, number] = [0, 100]
): number | null {
  if (value === null || value === undefined) return null;
  
  const [min, max] = range;
  
  // [0, 100] → 直接返回
  if (min === 0 && max === 100) {
    return value;
  }
  
  // [0, 1] → 乘以 100
  if (min === 0 && max === 1) {
    return value * 100;
  }
  
  // 自定义范围 → 按比例计算
  return ((value - min) / (max - min)) * 100;
}

/**
 * 反向转换（将 0-100 百分比转换为指定范围）
 * @param percent 0-100 范围的百分比值
 * @param range 目标范围
 * @returns 目标范围内的值
 */
export function convertFromPercent(
  percent: number | null,
  range: [number, number] = [0, 100]
): number | null {
  if (percent === null || percent === undefined) return null;
  
  const [min, max] = range;
  
  // [0, 100] → 直接返回
  if (min === 0 && max === 100) {
    return percent;
  }
  
  // [0, 1] → 除以 100
  if (min === 0 && max === 1) {
    return percent / 100;
  }
  
  // 自定义范围 → 按比例计算
  return min + (percent / 100) * (max - min);
}
```

---

## 组件实现方案

### 关键点

1. **范围转换**：
  - 编辑态：用户输入的值在 range 范围内
  - 只读态：转换为 0-100 百分比显示
  - 需要双向转换

2. **% 符号位置**：
  - text 变体：格式化字符串中添加
  - 编辑态：使用 `suffix="%"`（输入框内部，无边框）

3. **Progress 集成**：
  - 只读态的 progress 和 circle 变体
  - percent 属性需要 0-100 的值
  - 支持自定义配置（颜色、大小等）

4. **Slider 集成**：
  - 编辑态可选显示
  - 与 InputNumber 双向同步
  - 支持自定义配置（marks、step等）

---

## 范围转换详解

### 输入值 → 显示值

```
// [0, 100] 范围
输入: 85.5 → 显示: 85.5%

// [0, 1] 范围
输入: 0.855 → 转换: 0.855 * 100 = 85.5 → 显示: 85.5%

// [0, 1000] 范围
输入: 855 → 转换: (855 / 1000) * 100 = 85.5 → 显示: 85.5%
```

### Progress 使用

```
// Progress 的 percent 属性需要 0-100 的值
<Progress percent={convertToPercent(value, range)} />
```

---

## 开发时间评估

| 任务        | 预计时间      | 备注                           |
|-----------|-----------|------------------------------|
| 类型定义      | 0.5h      | 继承 + 扩展                      |
| 工具函数      | 1.5h      | 格式化 + 范围转换                   |
| 子组件开发     | 3h        | ReadOnly（3种变体）/Edit/Disabled |
| 主组件开发     | 0.5h      | 状态路由                         |
| 样式编写      | 0.5h      | Less 样式                      |
| 单元测试      | 2h        | 覆盖核心逻辑                       |
| Storybook | 1.5h      | 多种变体示例                       |
| 文档完善      | 1h        | 使用文档                         |
| **总计**    | **10.5h** | **约 1.5 个工作日**               |
