---
title: 分段涂抹功能
summary: 介绍幻彩串灯的分段涂抹功能，支持灯珠可视化选择、全段/分段涂抹控制及颜色调节，通过 paint_colour_1 DP 下发。
---

## 分段涂抹功能

> **项目简介**：本模块适用于 Wi-Fi + BLE 协议的幻彩串灯（灯串）设备，支持对灯珠进行分段选择和独立控制，实现精确的灯光效果定制。

### 🖼️ 预览：
<Image src='/images/extended/lampSting/string-paint.png' style={{ width: '120px', height: '224px' }} />

分段涂抹功能允许用户通过可视化界面选择特定的灯珠段，并对选中的灯珠段进行独立的颜色控制。该功能支持彩光和白光两种模式，可以实现全段控制、单段控制和分段涂抹等多种操作方式，是用户个性化灯光效果的重要工具。

### 功能列表

1. **灯珠可视化显示**：以网格形式展示所有灯珠，支持点击选择
2. **全选/取消全选**：一键选择或取消所有灯珠
3. **分段选择**：支持点击单个或多个灯珠进行选择
4. **分段涂抹控制**：对选中的灯珠段进行独立的颜色控制
5. **全段涂抹控制**：对所有灯珠进行统一的颜色控制
6. **实时预览**：滑动调节时实时更新灯带颜色显示
7. **数据同步**：灯珠颜色数据同步到云存储和设备

### 功能实现详解

#### 实现概述

分段涂抹功能采用 Redux 管理选中状态，通过 `paint_colour_1` DP 点进行数据下发。核心实现思路：

- **状态管理**：使用 Redux 的 `commonSlice` 管理选中灯珠列表（`checkedIdList`）和全选状态（`stripCheckAll`）
- **数据流转**：用户选择 → 更新 Redux 状态 → 颜色调节 → 构建 DP 数据 → 下发设备
- **涂抹模式**：支持油漆桶模式（全段）和单点涂抹模式（分段）
- **数据持久化**：灯珠颜色数据保存到云存储，支持跨会话恢复

#### 核心组件

- `src/components/light-strip/index.tsx`: 灯带可视化组件，负责灯珠的显示和选择交互
- `src/components/light/index.tsx`: 灯光控制主组件，整合分段涂抹和颜色控制逻辑
- `src/redux/modules/commonSlice.ts`: Redux 状态管理，存储选中灯珠列表和全选状态
- `src/hooks/useDrawToolDataList.ts`: 涂抹工具数据管理 Hook，处理灯珠数据的更新
- `src/devices/protocols/paintColour1.ts`: `paint_colour_1` DP 点的编解码器
- `src/hooks/useStorageData.ts`: 云存储数据管理 Hook

**组件职责说明**：

- **LightStrip 组件**：负责灯珠的可视化渲染和点击交互，维护选中状态的 UI 反馈
- **Light 组件**：整合颜色控制和分段涂抹逻辑，处理颜色变化和 DP 下发
- **commonSlice**：集中管理选中状态，提供状态更新方法
- **useDrawToolDataList Hook**：封装灯珠数据更新逻辑，处理全段和分段两种模式

#### 数据流

```mermaid
sequenceDiagram
    participant User as 用户
    participant LightStrip as 灯带组件
    participant Redux as Redux Store
    participant Light as 灯光组件
    participant Hook as 数据Hook
    participant Protocol as 协议解析器
    participant Device as 设备
    participant Cloud as 云存储

    User->>LightStrip: 点击选择灯珠
    LightStrip->>Redux: 更新选中列表
    Redux-->>LightStrip: 更新UI状态
    User->>Light: 调节颜色
    Light->>Hook: 调用更新函数
    Hook->>Hook: 构建灯珠数据数组
    Hook->>Protocol: 编码DP数据
    Protocol->>Device: 下发paint_colour_1
    Device-->>User: 显示效果
    Hook->>Cloud: 保存灯珠颜色数据
```

#### Mermaid 流程图

**功能主流程图**：

```mermaid
flowchart TD
    A[进入灯光控制页面] --> B[加载灯珠数据]
    B --> C[显示灯带可视化]
    C --> D{用户操作}
    D -->|点击灯珠| E[更新选中状态]
    D -->|点击全选| F[切换全选状态]
    D -->|调节颜色| G[获取选中灯珠]
    E --> H[更新Redux状态]
    F --> H
    G --> I{是否全选}
    I -->|是| J[全段涂抹模式]
    I -->|否| K[分段涂抹模式]
    J --> L[构建全段DP数据]
    K --> M[构建分段DP数据]
    L --> N[编码DP数据]
    M --> N
    N --> O[下发到设备]
    O --> P[更新云存储]
    P --> Q[实时预览效果]
```

**数据流转图**：

```mermaid
sequenceDiagram
    participant User as 用户操作
    participant UI as UI组件
    participant State as Redux状态
    participant Logic as 业务逻辑
    participant DP as DP协议
    participant Device as 设备
    participant Storage as 云存储

    User->>UI: 选择灯珠
    UI->>State: dispatch(updateCheckedIdList)
    State-->>UI: 更新选中状态
    User->>UI: 调节颜色
    UI->>Logic: 获取选中灯珠列表
    Logic->>Logic: 构建颜色数据数组
    Logic->>DP: 编码paint_colour_1数据
    DP->>Device: 下发DP指令
    Device-->>User: 显示灯光效果
    Logic->>Storage: 保存灯珠颜色数据
```

**组件交互图**：

```mermaid
graph TB
    A[Light组件] -->|使用| B[LightStrip组件]
    A -->|使用| C[ColorLight组件]
    A -->|使用| D[WhiteLight组件]
    B -->|dispatch| E[Redux Store]
    C -->|调用| F[useDrawToolDataList]
    D -->|调用| F
    F -->|使用| G[useStorageData]
    F -->|编码| H[paintColour1协议]
    H -->|下发| I[设备]
    G -->|保存| J[云存储]
```

**状态变化图**：

```mermaid
stateDiagram-v2
    [*] --> 初始状态: 页面加载
    初始状态 --> 选择灯珠: 用户点击
    选择灯珠 --> 调节颜色: 选中完成
    调节颜色 --> 滑动调节: 开始滑动
    滑动调节 --> 实时预览: 颜色变化
    实时预览 --> 松手确认: 停止滑动
    松手确认 --> 下发DP: 构建数据
    下发DP --> 更新存储: 成功回调
    更新存储 --> 选择灯珠: 继续操作
    更新存储 --> [*]: 完成
```

#### 关键代码片段

**灯珠选择处理**：

```tsx
// 文件: src/components/light-strip/index.tsx
const onClick = useCallback(
  (checked: boolean, index: number) => {
    if (!checked) {
      checkedSet.delete(index);
    } else {
      checkedSet.add(index);
    }
    const newCheckedSet = new Set(checkedSet);
    dispatch(actions.common.updateCheckedIdList(Array.from(newCheckedSet)));
  },
  [checkedSet]
);
```

**全选/取消全选**：

```tsx
// 文件: src/components/light-strip/index.tsx
const handleCheckAll = () => {
  // 全选
  if (!checkAllBool) {
    const newSet = new Set<number>();
    new Array(lightNum).fill(0).map((_, i) => newSet.add(i));
    dispatch(actions.common.updateCheckedIdList(Array.from(newSet)));
  } else {
    const newArr = [];
    dispatch(actions.common.updateCheckedIdList(newArr));
  }
};
```

**分段涂抹数据更新**：

```tsx
// 文件: src/hooks/useDrawToolDataList.ts
const updateStripDataListAll = ({ isMoving, color, isSectionAll }: UpdateStripDataListAllOps) => {
  if (!color) {
    return;
  }
  if (!isSectionAll && checkedSet.size === 0) {
    ty.showToast({
      title: Strings.getLang('checkLightTip'),
      icon: 'none',
    });
  }
  if (isMoving) {
    // 滑动时实时，更新灯带颜色
    updateStripDataList(color);
    return;
  }
  // 全段控制时 下发dp 更新颜色
  if (isSectionAll) {
    const res: DiySceneData = {
      ...(diy_scene || {}),
      ...(color || {}),
      effect: 1,
      daubType: 'all',
      // 所有段都更新为这个颜色
      segments: new Array(ledNumber).fill(0).map(i => ({
        index: i,
        ...color,
      })),
    };

    console.log('[diy_scene] set:', res);
    cutDrawDiySceneSet(res, {
      success() {
        const newLightStripDataList = updateStripDataList(color, isSectionAll);
        preUpdateCloudDataRef.current = newLightStripDataList;
      },
      fail(e) {
        console.error('fail', e);
      },
    });
    return;
  }
  // 松手时，下发 dp 数据
  // 如果外部有颜色传入并且存在选中的灯珠那么更新选中的灯珠颜色
  if (checkedSet.size > 0) {
    const indexs = Array.from(checkedSet);
    const curSegments = getArray(diy_scene?.segments);
    const res: DiySceneData = {
      ...(diy_scene || {}),
      daubType: 'single',
      segments: new Array(ledNumber)
        .fill(0)
        .map((_, index) => {
          const item = curSegments[index] || {
            isWhite: false,
            hue: 0,
            saturation: 1000,
            value: 1000,
            brightness: 0,
            temperature: 0,
          };
          // 编辑选中的颜色
          if (indexs.includes(index)) {
            item.brightness = color.brightness;
            item.hue = color.hue;
            item.isWhite = color.isWhite;
            item.saturation = color.saturation;
            item.value = color.value;
            item.temperature = color.temperature;
          }
          return item;
        })
        .slice(0, ledNumber),
    };
    console.log('[diy_scene] set:', res);
    cutDrawDiySceneSet(res, {
      success() {
        const newLightStripDataList = updateStripDataList(color, false);
        preUpdateCloudDataRef.current = newLightStripDataList;
      },
      fail(params) {
        console.error('__checkedSet fail', params);
      },
    });
  }
};
```

**彩光分段控制**：

```tsx
// 文件: src/components/light/index.tsx
const updateColourStripDataListAll = (_hsv = currentHsv, isSectionAll = false, extParams?) => {
  const { checkedSet: _checkedSet } = extParams || {};
  const __checkedSet = _checkedSet || checkedSet || new Set();
  const _currentHsv = _hsv || currentHsv;
  preHsvRef.current = _currentHsv;
  if (!_currentHsv) {
    return;
  }
  if (isMovingRef.current) {
    // 滑动时实时，更新灯带颜色
    updateStripDataList(currentLightStripDataList, __checkedSet, {
      ..._currentHsv,
      mode: DimmerMode.colour,
    });
    return;
  }
  // 全段控制时 下发dp 更新颜色
  if (isSectionAll) {
    const hsvFull = {
      hue: _currentHsv.h,
      saturation: _currentHsv.s,
      value: _currentHsv.v,
    };
    const indexs = new Set() as any;
    const dimmerMode = DimmerMode.colour;
    const smearMode = SmearMode.all;
    const res = {
      ...paintColorData,
      ...hsvFull,
      indexs,
      ledNumber,
      dimmerMode,
      smearMode,
    };
    paintColorDataRef.current = res;
    timeoutPutDpStatus();
    sActions.paint_colour_1.set(res, {
      success() {
        const newLightStripDataList = updateStripDataList(
          currentLightStripDataList,
          __checkedSet,
          {
            ..._currentHsv,
            mode: DimmerMode.colour,
          },
          isSectionAll
        );
        const _isEqualColor = isEqual(preUpdateCloudDataRef.current, newLightStripDataList);
        console.warn(newLightStripDataList, 'handleUpdate2Cloud1');
        !_isEqualColor && handleUpdate2Cloud(newLightStripDataList);
        preUpdateCloudDataRef.current = newLightStripDataList;
      },
      fail(e) {
        console.error('fail', e);
      },
    });
    return;
  }
  // 松手时，下发 dp 数据
  // 如果外部有颜色传入并且存在选中的灯珠那么更新选中的灯珠颜色
  if (__checkedSet.size > 0) {
    const hsvFull = {
      hue: _currentHsv.h,
      saturation: _currentHsv.s,
      value: _currentHsv.v,
    };
    const indexs = __checkedSet;
    const dimmerMode = DimmerMode.colour;
    const smearMode = SmearMode.single;
    const res = {
      ...paintColorData,
      ...hsvFull,
      indexs,
      ledNumber,
      dimmerMode,
      smearMode,
    };
    paintColorDataRef.current = res;
    timeoutPutDpStatus();
    sActions.paint_colour_1.set(res, {
      success() {
        const newLightStripDataList = updateStripDataList(currentLightStripDataList, __checkedSet, {
          ..._currentHsv,
          mode: DimmerMode.colour,
        });
        const _isEqualColor = isEqual(preUpdateCloudDataRef.current, newLightStripDataList);
        !_isEqualColor && newLightStripDataList && handleUpdate2Cloud(newLightStripDataList);
        preUpdateCloudDataRef.current = newLightStripDataList;
      },
      fail(params) {
        console.error('__checkedSet fail', params);
      },
    });
  }
};
```

#### 数据结构

**选中状态数据结构**（Redux）：

```typescript
type CommonState = {
  stripCheckAll: boolean; // 灯珠是否全选中了
  checkedIdList: number[]; // 选中的灯珠id列表
};
```

**paint_colour_1 DP 数据结构**：

```typescript
interface SmearDataType {
  version: number; // 版本号
  dimmerMode: DimmerMode; // 模式 (0: 白光, 1: 彩光, 2: 色卡, 3: 组合)
  effect?: number; // 涂抹效果 (0: 无, 1: 渐变)
  ledNumber?: number; // 灯带UI段数
  smearMode?: SmearMode; // 涂抹动作 (0: 油漆桶, 1: 涂抹, 2: 橡皮擦)
  hue?: number; // 彩光色相
  saturation?: number; // 彩光饱和度
  value?: number; // 彩光亮度
  brightness?: number; // 白光亮度
  temperature?: number; // 白光色温
  indexs?: Set<number>; // 选中的灯珠索引集合
}
```

**灯珠颜色数据数组**：

```typescript
// 灯珠颜色数据：RGB颜色字符串数组
// 例如：['rgb(255,0,0)', 'rgb(0,255,0)', ...]
type LightStripDataList = string[];
```

**涂抹模式枚举**：

```typescript
enum SmearMode {
  all = 0, // 油漆桶模式（全段）
  single = 1, // 单点涂抹模式（分段）
  clear = 2, // 橡皮擦模式
}
```

#### API 调用

**DP 下发接口**：

- **DP 点**：`paint_colour_1`
- **调用方式**：`structuredActions.paint_colour_1.set(data, options)`
- **数据格式**：经过 `SmearFormater` 编码的十六进制字符串
- **调用时机**：
  - 全段控制：用户调节颜色且全选状态时
  - 分段控制：用户调节颜色且存在选中灯珠时
  - 滑动调节：仅在松手时下发，滑动过程中只更新 UI

**云存储接口**：

- **存储键**：`lightStripDataKey`（常量定义）
- **调用方式**：`useSetStorageData(lightStripDataKey)`
- **数据格式**：RGB 颜色字符串数组
- **调用时机**：DP 下发成功后，更新云存储数据

### 注意事项

#### 实现难点

1. **选中状态同步**：

   - **难点**：需要在多个组件间同步选中状态，确保 UI 和数据一致
   - **解决方案**：使用 Redux 集中管理状态，通过 `useSelector` 和 `dispatch` 实现状态共享和更新

2. **实时预览性能**：

   - **难点**：滑动调节时需要实时更新大量灯珠颜色，可能造成性能问题
   - **解决方案**：使用节流（throttle）限制更新频率，滑动时只更新 UI，松手时才下发 DP

3. **数据对齐**：

   - **难点**：灯珠数量可能变化，需要确保数据数组长度与设备灯珠数量一致
   - **解决方案**：在 `useCutDrawDiySceneSet` 中检查数据长度，不足时补全默认颜色，超出时截断

4. **全选状态判断**：
   - **难点**：需要准确判断是否所有灯珠都被选中
   - **解决方案**：通过比较选中集合大小和灯珠总数，使用 `useEffect` 监听变化并更新全选状态

#### 常见问题

1. **问题**：选择灯珠后调节颜色无效果

   - **原因**：未选中任何灯珠且未开启全选模式
   - **解决方法**：检查 `checkedSet.size` 和 `stripCheckAll` 状态，提示用户先选择灯珠

2. **问题**：滑动调节时颜色更新延迟

   - **原因**：节流时间设置过长或 DP 下发过于频繁
   - **解决方法**：调整节流时间（建议 300ms），滑动时只更新 UI，松手时再下发 DP

3. **问题**：设备灯珠数量变化后数据不匹配

   - **原因**：云存储数据长度与设备灯珠数量不一致
   - **解决方法**：在数据加载时自动对齐，补全或截断数据数组

4. **问题**：全选状态显示不正确
   - **原因**：选中状态更新后未及时同步全选状态
   - **解决方法**：使用 `useEffect` 监听选中列表变化，自动更新全选状态

#### 性能优化

1. **节流处理**：

   - 颜色调节使用节流，避免频繁更新
   - 滑动时使用节流更新 UI，减少渲染次数

2. **数据缓存**：

   - 使用 `useRef` 缓存上一次的灯珠数据，避免重复更新
   - 使用 `useMemo` 缓存计算结果，减少重复计算

3. **条件渲染**：

   - 根据灯珠数量动态调整渲染层级，减少不必要的 DOM 节点
   - 使用 `useMemo` 缓存灯珠元素列表

4. **状态更新优化**：
   - 批量更新状态，减少 Redux dispatch 次数
   - 使用 `useCallback` 缓存事件处理函数

#### 边界情况

1. **灯珠数量为 0**：

   - 处理：组件返回空视图，不进行渲染
   - 代码：`if (!lightNum) return <View />;`

2. **未选中任何灯珠**：

   - 处理：提示用户先选择灯珠，不执行颜色更新
   - 代码：检查 `checkedSet.size === 0 && !isSectionAll`

3. **设备灯珠数量变化**：

   - 处理：自动对齐数据长度，补全或截断
   - 代码：在 `useCutDrawDiySceneSet` 中处理

4. **DP 下发失败**：

   - 处理：显示错误提示，不更新云存储数据
   - 代码：在 `fail` 回调中处理错误

5. **同时进行多个操作**：
   - 处理：使用 `isPutDpIngReg` 标志位防止重复下发
   - 代码：设置超时恢复机制（1 秒）

