---
title: DIY 场景编辑功能
summary: 介绍 DIY 场景编辑功能，支持动态场景和静态场景两种编辑模式，实现场景创建、编辑、预览、保存和管理。
---

## DIY 场景编辑功能

> 适用范围：Wi-Fi + BLE 面板

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

DIY 场景编辑功能模块允许用户创建和编辑自定义灯光场景，支持两种编辑模式：动态场景编辑（基于颜色和动效参数）和静态场景编辑（基于灯珠涂抹）。该模块提供了完整的场景创建、编辑、预览、保存和管理功能，是用户个性化灯光效果的核心工具。

### 功能列表

1. **动态 DIY 场景编辑**：通过颜色列表和动效参数创建动态场景
2. **静态 DIY 场景编辑**：通过涂抹方式精确控制每个灯珠的颜色
3. **场景预览**：实时预览场景效果
4. **场景保存**：将场景保存到云存储
5. **场景加载**：从云存储加载已有场景进行编辑
6. **场景删除**：删除不需要的场景
7. **场景列表管理**：查看和管理所有 DIY 场景


### 功能实现详解 

#### 动态 DIY 场景编辑
动态 DIY 场景编辑使用`SceneData`数据结构，包含颜色列表和动效参数。编辑页面提供颜色管理和参数调整功能，场景数据通过`rgbic_linerlight_scene` DP 点下发。

##### 核心组件

- `src/pages/diyEdit/index.tsx`: 动态 DIY 场景编辑页面
- `src/components/colors/index.tsx`: 颜色列表组件
- `src/components/motion-config/index.tsx`: 动效参数配置组件
- `src/hooks/useSceneSet.ts`: 场景设置 Hook

##### 数据流

```mermaid
flowchart TD
    A[进入编辑页面] --> B[加载场景数据]
    B --> C{是否编辑模式}
    C -->|是| D[从云存储加载]
    C -->|否| E[使用默认数据]
    D --> F[显示颜色列表]
    E --> F
    F --> G[用户添加/删除颜色]
    G --> H[选择动效类型]
    H --> I[调整动效参数]
    I --> J[预览场景]
    J --> K[保存场景]
    K --> L[编码场景数据]
    L --> M[保存到云存储]
    M --> N[下发DP到设备]
```

##### 关键代码片段

**场景保存实现**：

```tsx
  // 参考：src/pages/diyEdit/index.tsx
  const handleSave = async (sceneName: string) => {
    const params = {
      ...DEFAULTOPTIONS,
      ...values,
      key: 100,
      name: sceneName || '',
      changeType: current,
      colors: getArray(diyEditColors).map(color => ({
        hue: color.h,
        saturation: Math.floor(color.s / 10),
        value: Math.floor(color.v / 10),
        brightness: 0,
        temperature: 0,
      })),
      dataType: 0,
    } as SceneData;

    if (isDataId(dataId)) {
      await storage.updateItem(dataId, encodeCloudSceneData(params));
      sceneApi.set(params);
      Dispatcher.instance.dispatch('diyCreated', dataId);
    } else {
      showLoading({
        title: '',
        mask: true,
      });
      const newDataId = await storage.addItem(encodeCloudSceneData(params), {
        dedup: (a, b) => {
          try {
            return JSON.parse(a).name === JSON.parse(b).name;
          } catch (error) {
            return false;
          }
        },
      });
      hideLoading();
      if (typeof newDataId === 'number') {
        sceneApi.set(params);
        Dispatcher.instance.dispatch('diyCreated', newDataId);
      } else {
        TipApi.show({
          show: true,
          content: (
            <View className={styles.tipWrap}>
              <Image src={res.icon_warn} className={styles.tipIcon} />
              <View className={styles.tipText}>{Strings.getLang('name_dedup')}</View>
            </View>
          ),
        });
        return -1;
      }
    }
  };
```

##### 数据结构

**场景数据格式**：

```typescript
type SceneData = {
  dataType: number; // 0: 动态场景
  name: string; // 场景名称
  key: number; // 场景键值
  changeType: number; // 动效类型
  colors: Array<{
    // 颜色列表
    hue: number;
    saturation: number;
    value: number;
    brightness: number;
    temperature: number;
  }>;
  // 其他动效参数...
};
```

#### 静态 DIY 场景编辑

静态 DIY 场景编辑使用`DiySceneData`数据结构，包含每个灯珠的颜色信息。通过涂抹方式控制每个灯珠，场景数据通过`diy_scene` DP 点下发。

##### 核心组件

- `src/pages/staticDiyEdit/index.tsx`: 静态 DIY 场景编辑页面
- `src/components/light-strip/index.tsx`: 灯带可视化组件
- `src/hooks/useDrawToolDataList.ts`: 涂抹工具数据管理 Hook
- `src/hooks/useCutDrawDiyScene.ts`: 涂抹场景设置 Hook

##### 数据流

```mermaid
sequenceDiagram
    participant User as 用户
    participant Page as 编辑页面
    participant Strip as 灯带组件
    participant Hook as 数据Hook
    participant Protocol as 协议解析器
    participant Device as 设备

    User->>Page: 选择颜色和涂抹模式
    User->>Strip: 选择灯珠
    Strip->>Page: 更新选中状态
    Page->>Hook: 调用更新函数
    Hook->>Hook: 更新灯带数据
    Hook->>Protocol: 编码场景数据
    Protocol->>Device: 下发DP
    Device-->>User: 显示效果
    User->>Page: 保存场景
    Page->>Hook: 保存到云存储
```

##### 关键代码片段

**场景保存实现**：

```tsx
// 参考：src/pages/staticDiyEdit/index.tsx
  const handleSave = async (sceneName: string) => {
    const params: DiySceneData = {
      name: sceneName,
      ...(diy_scene || ({} as DiySceneData)),
      segments: hasChange ? diy_scene?.segments : getArray(getDefault().segments),
    };
    console.log('[handleSave]:', params);
    if (isDataId(dataId)) {
      await storage.updateItem(dataId, encodeCloudSceneData(params));
      cutDrawDiySceneSet(params);
      Dispatcher.instance.dispatch('diyCreated', dataId);
    } else {
      showLoading({ title: '', mask: true });

      const newDataId = await storage.addItem(encodeCloudSceneData(params));
      if (typeof newDataId === 'number') {
        cutDrawDiySceneSet(params);
        Dispatcher.instance.dispatch('diyCreated', newDataId);
        hideLoading();
      } else {
        hideLoading();
        TipApi.show({
          show: true,
          content: (
            <View className={styles.tipWrap}>
              <Image src={res.icon_warn} className={styles.tipIcon} />
              <View className={styles.tipText}>{Strings.getLang('name_dedup')}</View>
            </View>
          ),
        });
        return -1;
      }
    }
  };
```

**涂抹场景设置**：

```tsx
// 参考：src/hooks/useCutDrawDiyScene.ts
export const useCutDrawDiySceneSet = () => {
  const ledNumber = useProps(p => p[dpCodes.led_number_set]);
  const structuredActions = useStructuredActions();

  return (data: DiySceneData, options?: PublishDpsOptions) => {
    const newData = { ...(data || ({} as DiySceneData)) };
    const segments = getArray(newData?.segments);
    if (ledNumber) {
      if (segments.length >= ledNumber) {
        newData.segments = segments.slice(0, ledNumber);
      } else {
        const defaultColor: DiySceneData['segments'][0] = {
          isWhite: false,
          hue: 0,
          saturation: 1000,
          value: 1000,
          brightness: 0,
          temperature: 0,
        };
        newData.segments = segments.concat(
          ...new Array(ledNumber - segments.length).fill(0).map(() => defaultColor)
        );
      }
    }
    structuredActions.diy_scene.set(newData, options);
  };
};
```

##### 数据结构

**DIY 场景数据格式**：

```typescript
type DiySceneData = {
  name?: string;
  version?: number;
  id?: number;
  daubType?: 'all' | 'single' | 'clear'; // 涂抹类型
  effect?: number; // 效果类型
  segments?: Array<{
    // 灯珠数据
    hue?: number;
    saturation?: number;
    value?: number;
    brightness?: number;
    temperature?: number;
    isWhite?: boolean;
  }>;
};
```

#### 场景预览

场景预览通过`useSceneSet` Hook 的`preview`方法实现，将场景数据编码后下发到设备，不保存到云存储。

##### 核心组件

- `src/hooks/useSceneSet.ts`: 场景设置 Hook

##### 数据流

```mermaid
flowchart TD
    A[用户点击预览] --> B[构建场景数据]
    B --> C[useSceneSet.preview]
    C --> D[编码场景数据]
    D --> E[下发DP到设备]
    E --> F[设备执行场景]
    F --> G[用户观察效果]
    G --> H{继续编辑?}
    H -->|是| I[返回编辑]
    H -->|否| J[保存场景]
```

##### 关键代码片段

**预览实现**：

```tsx
// 参考：src/hooks/useSceneSet.ts
  const preview = async (scene: SceneData) => {
    if (getArray(scene?.colors).length > 0) {
      await set(scene);
    }
  };
```

#### 场景保存

场景保存包括数据编码、云存储保存、DP 下发三个步骤。支持新增和更新两种模式，保存成功后触发场景创建事件。

##### 核心组件

- `src/pages/diyEdit/index.tsx`: 动态场景保存
- `src/pages/staticDiyEdit/index.tsx`: 静态场景保存
- `src/hooks/useCloudStorageCombinedList.ts`: 云存储列表管理
- `src/hooks/useCloudDrawToolList.ts`: 场景数据编解码

##### 数据流

已在功能 1 和功能 2 中详细说明。

#### 场景加载

场景加载从云存储读取场景数据，解码后填充到编辑页面。根据场景类型（动态或静态）加载到对应的编辑页面。

##### 核心组件

- `src/pages/diyEdit/index.tsx`: 动态场景加载
- `src/pages/staticDiyEdit/index.tsx`: 静态场景加载
- `src/hooks/useCloudDrawToolList.ts`: 场景数据解码

##### 数据流

```mermaid
flowchart TD
    A[用户选择编辑] --> B[获取场景ID]
    B --> C[从云存储读取]
    C --> D[解码场景数据]
    D --> E{场景类型}
    E -->|动态| F[加载到动态编辑页]
    E -->|静态| G[加载到静态编辑页]
    F --> H[填充颜色列表]
    G --> I[填充灯珠数据]
    H --> J[填充动效参数]
    I --> K[显示灯带状态]
```

#### 场景删除

场景删除从云存储删除场景数据，更新场景列表。删除前需要用户确认。

##### 核心组件

- `src/pages/diyEdit/index.tsx`: 删除功能
- `src/pages/staticDiyEdit/index.tsx`: 删除功能
- `src/hooks/useCloudStorageCombinedList.ts`: 云存储删除

##### 关键代码片段

**删除实现**（以动态场景为例）：

```tsx
// 参考：src/pages/diyEdit/index.tsx
        onRightClick={() => {
          showConfirmDeleteModal({
            content: Strings.getLang('deleteSceneTip'),
            success(res) {
              if (res.confirm) {
                storage.delItem(dataId);
                router.back();
              }
            },
          });
        }}
```

#### 场景列表管理


场景列表管理通过`useDiySceneInit` Hook 实现，从云存储读取所有场景，按类型分类显示，支持场景选择和切换。

##### 核心组件

- `src/components/tab-diy/index.tsx`: DIY 场景列表组件
- `src/hooks/useDiySceneInit.ts`: DIY 场景初始化 Hook

##### 数据流

```mermaid
flowchart TD
    A[进入DIY标签] --> B[useDiySceneInit初始化]
    B --> C[从云存储读取场景列表]
    C --> D[解码场景数据]
    D --> E[按类型分类]
    E --> F[显示场景卡片]
    F --> G[用户选择场景]
    G --> H{场景类型}
    H -->|动态| I[应用动态场景]
    H -->|静态| J[应用静态场景]
    I --> K[下发场景DP]
    J --> K
    K --> L[更新当前场景ID]
```

##### 关键代码片段

**场景初始化**：

```tsx
// 参考：src/hooks/useDiySceneInit.ts
  const initDiyScene = useCallback(() => {
    if (!inited) return;
    if (!switch_led) return;
    console.log('init diy', current);
    if (current) {
      if (+current !== -1) {
        const item = getArray(storage.list).find(item => +item.id === +current);
        if (item?.value) {
          const props = decodeCloudSceneData(item?.value);
          if (props.type === 'scene') {
            sceneApi.set(props.ret);
            actions.work_mode.set('scene');
            setCurrent(String(item?.id));
          }
          if (props.type === 'draw') {
            cutDrawDiySceneSet(props?.ret);
            setCurrent(String(item?.id));
          }
        }
      } else {
        const item = getArray(storage.list)[0];
        if (item?.value) {
          const props = decodeCloudSceneData(item?.value);
          if (props.type === 'scene') {
            sceneApi.set(props.ret);
            actions.work_mode.set('scene');
            setCurrent(String(item?.id));
          }
          if (props.type === 'draw') {
            cutDrawDiySceneSet(props?.ret);
            setCurrent(String(item?.id));
          }
        }
      }
    }
  }, [current, storage.list, switch_led, inited]);
```

**场景列表生成**：

```tsx
// 参考：src/hooks/useDiySceneInit.ts
  const diySceneList = useMemo(
    () =>
      splitArray(
        getArray(storage.list)
          .sort((a, b) => b.id - a.id)
          .map(item => {
            const props = decodeCloudSceneData(item.value);
            return {
              dataId: item.id,
              data: props?.ret || {},
              cloudType: props.type,
            };
          }),
        2
      ),
    [storage.list]
  );
```

### 注意事项

####  实现难点

1. **场景数据编解码**：

   - **难点**：动态场景和静态场景使用不同的数据格式和 DP 点
   - **解决方案**：使用`encodeCloudSceneData`和`decodeCloudSceneData`统一编解码，通过`type`字段区分场景类型

2. **灯珠数据对齐**：

   - **难点**：静态场景的灯珠数据需要与设备灯珠数量对齐
   - **解决方案**：在`useCutDrawDiySceneSet`中检查数据长度，不足时补全默认颜色，超出时截断

3. **场景类型区分**：

   - **难点**：需要区分动态场景（scene）和静态场景（draw）
   - **解决方案**：在云存储数据中使用`type`字段标识，加载时根据类型选择对应的处理逻辑

4. **场景名称去重**：
   - **难点**：防止场景名称重复
   - **解决方案**：使用`dedup`函数检查名称，保存时提示用户修改

#### 常见问题

1. **问题**：场景加载后数据不正确

   - **原因**：数据解码失败或格式不匹配
   - **解决方法**：检查数据格式，增加错误处理，使用默认值兜底

2. **问题**：预览无效果

   - **原因**：设备未连接或 DP 下发失败
   - **解决方法**：检查设备连接状态，查看 DP 下发日志

3. **问题**：静态场景灯珠数量不匹配
   - **原因**：设备灯珠数量变化或数据损坏
   - **解决方法**：自动对齐灯珠数量，补全或截断数据

#### 性能优化

1. **场景列表缓存**：

   - 使用`useMemo`缓存场景列表，避免重复计算
   - 场景列表按 ID 倒序排列，最新场景在前

2. **数据编码优化**：

   - 场景数据编码使用高效的字符串拼接
   - 避免不必要的深拷贝

3. **懒加载**：

   - 场景列表分页显示，避免一次性加载所有场景
   - 场景数据按需解码

4. **防抖处理**：
   - 场景保存使用防抖，避免重复保存
   - 场景预览使用节流，避免频繁下发 DP

#### 边界情况

1. **场景列表为空**：

   - 处理：显示空状态提示，引导用户创建场景
   - 提供默认场景选项

2. **场景数据损坏**：

   - 处理：解码失败时使用默认数据
   - 提示用户场景数据异常

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

   - 处理：自动对齐灯珠数量
   - 超出部分截断，不足部分补全默认颜色

4. **场景名称过长**：

   - 处理：限制场景名称长度
   - 超出部分截断或提示用户

5. **同时编辑多个场景**：
   - 处理：同一时间只能编辑一个场景
   - 切换编辑时保存当前场景

