# 单位数字编辑器组件

一个专为数字输入设计的组件，具备单位转换功能。允许用户输入带有不同单位（如 kg、lb、cm、inch）的数字，并自动在不同单位间进行转换。包含下拉单位选择器和基于比率的自动转换功能。

## 安装

```bash
npm install @ticatec/uniface-element
```

## 导入

```typescript
import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
```

## 基本用法

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let weightValue = 75; // 标准单位值（kg）
  let weightUnit = 'kg';
  
  const weightUnits: UnitOption[] = [
    { code: 'kg', text: '千克', ratio: 1, precision: 2 },
    { code: 'lb', text: '磅', ratio: 2.205, precision: 2 },
    { code: 'g', text: '克', ratio: 1000, precision: 0 }
  ];
</script>

<UnitNumberEditor 
  bind:value={weightValue}
  bind:unitCode={weightUnit}
  units={weightUnits}
  placeholder="输入重量"
/>
```

## 属性

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `value` | `number \| null` | 必需 | 标准单位的数字值 |
| `unitCode` | `string` | 必需 | 当前选中的单位代码 |
| `units` | `Array<UnitOption>` | 必需 | 可用的单位选项 |
| `disabled` | `boolean` | `false` | 输入框是否禁用 |
| `readonly` | `boolean` | `false` | 输入框是否只读 |
| `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | 输入框的视觉变体 |
| `compact` | `boolean` | `false` | 是否使用紧凑间距 |
| `style` | `string` | `""` | 附加 CSS 样式 |
| `prefix` | `string` | `""` | 显示的文本前缀 |
| `allowNegative` | `boolean` | `false` | 是否允许负数 |
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 显示模式（编辑/查看） |
| `removable` | `boolean` | `true` | 是否显示清除按钮 |
| `menu$height` | `number` | `0` | 单位下拉菜单的固定高度 |
| `placeholder` | `string` | `""` | 占位符文本 |
| `class` | `string` | `""` | CSS 类名 |
| `onchange` | `OnChangeHandler<number \| null>` | `null` | 值更改时的回调函数 |
| `onfocus` | `((event: FocusEvent) => void) \| null` | `null` | 输入框获得焦点时的回调函数 |
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | 输入框失去焦点时的回调函数 |

## UnitOption 接口

```typescript
interface UnitOption {
  code: string;        // 单位标识符（例如 'kg', 'lb'）
  text?: string;       // 显示名称（例如 '千克'）
  ratio?: number;      // 转换为标准单位的比率
  precision?: number;  // 该单位的小数位数
  max?: number;        // 该单位的最大值
  min?: number;        // 该单位的最小值
}
```

## 方法

| 方法 | 描述 |
|--------|-------------|
| `setFocus()` | 以编程方式使输入框获得焦点 |

## 示例

### 重量转换器

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let weight = 70; // 70 千克作为标准单位
  let weightUnit = 'kg';
  
  const weightUnits: UnitOption[] = [
    { code: 'kg', text: '千克', ratio: 1, precision: 2 },
    { code: 'lb', text: '磅', ratio: 2.20462, precision: 2 },
    { code: 'g', text: '克', ratio: 1000, precision: 0 },
    { code: 'oz', text: '盎司', ratio: 35.274, precision: 2 }
  ];
  
  function handleWeightChange(newWeight) {
    console.log(`重量更改：${newWeight} 千克`);
  }
</script>

<div class="converter">
  <label>重量：</label>
  <UnitNumberEditor 
    bind:value={weight}
    bind:unitCode={weightUnit}
    units={weightUnits}
    onchange={handleWeightChange}
    placeholder="输入重量"
  />
</div>

<style>
  .converter {
    margin-bottom: 16px;
  }
  
  label {
    display: block;
    margin-bottom: 4px;
    font-weight: 500;
  }
</style>
```

### 距离测量

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let distance = null;
  let distanceUnit = 'm';
  
  const distanceUnits: UnitOption[] = [
    { code: 'm', text: '米', ratio: 1, precision: 2 },
    { code: 'ft', text: '英尺', ratio: 3.28084, precision: 2 },
    { code: 'in', text: '英寸', ratio: 39.3701, precision: 1 },
    { code: 'cm', text: '厘米', ratio: 100, precision: 0 },
    { code: 'mm', text: '毫米', ratio: 1000, precision: 0 }
  ];
</script>

<div class="form-group">
  <label>距离：</label>
  <UnitNumberEditor 
    bind:value={distance}
    bind:unitCode={distanceUnit}
    units={distanceUnits}
    placeholder="输入距离"
  />
</div>

{#if distance !== null}
  <div class="conversion-display">
    <h4>转换结果：</h4>
    {#each distanceUnits as unit}
      <p>
        {unit.text}: {distance !== null ? (distance * (unit.ratio ?? 1)).toFixed(unit.precision ?? 2) : 'N/A'} {unit.code}
      </p>
    {/each}
  </div>
{/if}

<style>
  .form-group {
    margin-bottom: 16px;
  }
  
  .conversion-display {
    background: #f8f9fa;
    padding: 16px;
    border-radius: 4px;
    margin-top: 16px;
  }
  
  .conversion-display h4 {
    margin: 0 0 8px 0;
  }
  
  .conversion-display p {
    margin: 4px 0;
    font-family: monospace;
  }
</style>
```

### 温度转换器

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let temperature = 20; // 20°C 作为标准单位
  let tempUnit = 'C';
  
  // 注意：温度转换需要特殊处理，因为存在偏移量
  // 此示例展示了简化的方法 - 实际实现需要自定义转换逻辑
  const temperatureUnits: UnitOption[] = [
    { code: 'C', text: '°摄氏度', ratio: 1, precision: 1 },
    { code: 'F', text: '°华氏度', ratio: 1.8, precision: 1 }, // 简化 - 未考虑 +32 偏移
    { code: 'K', text: '开氏度', ratio: 1, precision: 2 } // 简化 - 未考虑 +273.15 偏移
  ];
  
  // 为了精确的温度转换，你需要实现自定义转换逻辑
  function convertTemperature(value, fromUnit, toUnit) {
    if (value === null) return null;
    
    // 先转换为摄氏度
    let celsius = value;
    if (fromUnit === 'F') celsius = (value - 32) / 1.8;
    if (fromUnit === 'K') celsius = value - 273.15;
    
    // 从摄氏度转换为目标单位
    if (toUnit === 'F') return celsius * 1.8 + 32;
    if (toUnit === 'K') return celsius + 273.15;
    return celsius;
  }
</script>

<div class="form-group">
  <label>温度：</label>
  <UnitNumberEditor 
    bind:value={temperature}
    bind:unitCode={tempUnit}
    units={temperatureUnits}
    placeholder="输入温度"
    allowNegative={true}
  />
</div>
```

### 带事件处理程序

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let value = null;
  let unit = 'kg';
  let isFocused = false;
  
  const units: UnitOption[] = [
    { code: 'kg', text: '千克', ratio: 1, precision: 2 },
    { code: 'lb', text: '磅', ratio: 2.20462, precision: 2 }
  ];
  
  function handleChange(newValue) {
    console.log('值更改：', newValue);
  }
  
  function handleFocus(event) {
    console.log('输入框已聚焦');
    isFocused = true;
  }
  
  function handleBlur(event) {
    console.log('输入框已失焦');
    isFocused = false;
  }
</script>

<UnitNumberEditor 
  bind:value
  bind:unitCode={unit}
  {units}
  onchange={handleChange}
  onfocus={handleFocus}
  onblur={handleBlur}
  placeholder="输入值"
/>

{#if isFocused}
  <p>输入框当前已聚焦</p>
{/if}
```

### 不同变体

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let value1 = 100, value2 = 200, value3 = 300;
  let unit1 = 'm', unit2 = 'm', unit3 = 'm';
  
  const units: UnitOption[] = [
    { code: 'm', text: '米', ratio: 1, precision: 2 },
    { code: 'ft', text: '英尺', ratio: 3.28084, precision: 2 }
  ];
</script>

<!-- 默认变体 -->
<UnitNumberEditor bind:value={value1} bind:unitCode={unit1} {units} placeholder="默认" />

<!-- 轮廓变体 -->
<UnitNumberEditor bind:value={value2} bind:unitCode={unit2} {units} variant="outlined" placeholder="轮廓" />

<!-- 填充变体 -->
<UnitNumberEditor bind:value={value3} bind:unitCode={unit3} {units} variant="filled" placeholder="填充" />
```

### 带最小/最大约束

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let age = null;
  let ageUnit = 'years';
  
  const ageUnits: UnitOption[] = [
    { code: 'years', text: '年', ratio: 1, precision: 0, min: 0, max: 150 },
    { code: 'months', text: '月', ratio: 12, precision: 0, min: 0, max: 1800 },
    { code: 'days', text: '天', ratio: 365.25, precision: 0, min: 0, max: 54750 }
  ];
</script>

<div class="form-group">
  <label>年龄：</label>
  <UnitNumberEditor 
    bind:value={age}
    bind:unitCode={ageUnit}
    units={ageUnits}
    placeholder="输入年龄"
  />
</div>

{#if age !== null}
  <p>年龄：{age} {ageUnits.find(u => u.code === ageUnit)?.text?.toLowerCase()}</p>
{/if}
```

### 食谱原料转换器

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  import Button from "@ticatec/uniface-element/Button";
  
  let ingredients = [
    { name: '面粉', amount: 500, unit: 'g' },
    { name: '糖', amount: 200, unit: 'g' },
    { name: '牛奶', amount: 250, unit: 'ml' }
  ];
  
  const weightUnits: UnitOption[] = [
    { code: 'g', text: '克', ratio: 1, precision: 0 },
    { code: 'kg', text: '千克', ratio: 0.001, precision: 3 },
    { code: 'oz', text: '盎司', ratio: 0.035274, precision: 2 },
    { code: 'lb', text: '磅', ratio: 0.00220462, precision: 4 }
  ];
  
  const volumeUnits: UnitOption[] = [
    { code: 'ml', text: '毫升', ratio: 1, precision: 0 },
    { code: 'l', text: '升', ratio: 0.001, precision: 3 },
    { code: 'cup', text: '杯', ratio: 0.00422675, precision: 3 },
    { code: 'tbsp', text: '汤匙', ratio: 0.067628, precision: 2 }
  ];
  
  function getUnitsForIngredient(name) {
    return name.toLowerCase().includes('milk') ? volumeUnits : weightUnits;
  }
  
  function addIngredient() {
    ingredients = [...ingredients, { name: '', amount: null, unit: 'g' }];
  }
  
  function removeIngredient(index) {
    ingredients = ingredients.filter((_, i) => i !== index);
  }
</script>

<div class="recipe-converter">
  <h3>食谱原料转换器</h3>
  
  {#each ingredients as ingredient, index}
    <div class="ingredient-row">
      <input 
        bind:value={ingredient.name} 
        placeholder="原料名称"
        class="ingredient-name"
      />
      <UnitNumberEditor 
        bind:value={ingredient.amount}
        bind:unitCode={ingredient.unit}
        units={getUnitsForIngredient(ingredient.name)}
        placeholder="数量"
        compact
      />
      <Button 
        label="×" 
        onClick={() => removeIngredient(index)}
        style="width: 32px; height: 32px;"
      />
    </div>
  {/each}
  
  <Button label="添加原料" onClick={addIngredient} />
</div>

<style>
  .recipe-converter {
    max-width: 600px;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  
  .ingredient-row {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 12px;
    align-items: center;
    margin-bottom: 12px;
  }
  
  .ingredient-name {
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 14px;
  }
  
  h3 {
    margin-bottom: 20px;
  }
</style>
```

### 编程式焦点

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  import Button from "@ticatec/uniface-element/Button";
  
  let unitEditor;
  let value = null;
  let unit = 'kg';
  
  const units: UnitOption[] = [
    { code: 'kg', text: '千克', ratio: 1, precision: 2 },
    { code: 'lb', text: '磅', ratio: 2.20462, precision: 2 }
  ];
  
  function focusInput() {
    unitEditor.setFocus();
  }
</script>

<Button label="聚焦输入框" onClick={focusInput} />

<UnitNumberEditor 
  bind:this={unitEditor}
  bind:value
  bind:unitCode={unit}
  {units}
  placeholder="点击按钮聚焦我"
/>
```

## 功能

- **自动转换**：基于比率在不同单位间无缝转换
- **灵活精度**：每个单位可设置自己的小数位数
- **验证**：每个单位的最小/最大约束及自动验证
- **清除功能**：内置清除按钮以重置值
- **焦点管理**：为可访问性提供适当的焦点处理
- **自定义样式**：支持多种视觉变体和自定义 CSS
- **单位下拉菜单**：交互式下拉菜单用于单位选择
- **负数支持**：可选支持负数值
- **前缀支持**：添加文本前缀以增强上下文

## 单位转换逻辑

组件使用基于比率的转换系统：

1. **标准单位**：一个单位应具有 `ratio: 1` 作为基本单位
2. **转换**：其他单位通过其比率乘以标准单位进行转换
3. **显示**：值会自动转换为所选单位的显示值
4. **存储**：`value` 属性始终包含标准单位的值

示例：
```typescript
// 如果 kg 是标准单位（ratio: 1），lb 的 ratio 为 2.20462
// 用户输入 100 lb → 存储为约 45.36 kg
// 用户切换到 kg → 显示约 45.36 kg
```

## 可访问性

- 在输入框和下拉菜单之间提供适当的键盘导航
- 语义化 HTML 结构，兼容屏幕阅读器
- 交互元素的焦点指示器
- 下拉菜单的 ARIA 属性
- 交互状态的清晰视觉反馈

## 最佳实践

1. **标准单位**：始终定义一个 `ratio: 1` 的基本单位
2. **精度**：为每个单位设置适当的精度（整数或小数）
3. **约束**：使用最小/最大值防止无效输入
4. **单位名称**：在 `text` 属性中提供清晰、描述性的单位名称
5. **转换逻辑**：对于复杂转换（如温度），实现自定义逻辑
6. **验证**：如有需要，在更改处理程序中添加额外验证

## 浏览器支持

- 支持完整输入事件的现代浏览器
- 兼容 Svelte 5+
- 支持键盘导航
- 触控友好下拉界面
- 完整的 TypeScript 支持

## 相关组件

- `NumberEditor` - 不带单位的数字输入
- `TextEditor` - 文本输入组件
- `OptionsSelect` - 下拉选择组件
- `TimeEditor` - 时间输入组件