在移动端多列数据展示场景中,左侧关键列(如产品名称、订单编号)的固定显示是提升数据可读性的核心需求,尤其在电商、ERP、数据分析类应用中,横向滚动时保持核心列可见能够大幅降低用户的信息定位成本。本文以 React Native 开发的左侧冻结列表格组件为核心样本,深度拆解其冻结列布局实现、滚动同步机制、排序过滤交互等核心技术点,并系统阐述向鸿蒙(HarmonyOS)ArkTS 跨端迁移的完整技术路径,聚焦“冻结列布局跨端等价实现、滚动同步机制适配、列表渲染性能对齐”三大核心维度,为跨端冻结列表格组件开发提供可落地的技术参考。

1. 类型

相较于基础固定表头表格,该组件在 TypeScript 类型体系中新增了冻结列的核心配置,构建了“数据-表头-交互”三层强类型约束体系,为跨端开发奠定了语义一致的基础:

// 扩展业务数据类型,新增描述字段适配多列展示场景
type TableData = {
  id: string;
  name: string;
  category: string;
  price: number;
  quantity: number;
  status: 'active' | 'inactive' | 'pending';
  date: string;
  description: string; // 新增长文本字段,验证多列横向滚动场景
};

// 排序方向类型保持语义一致
type SortDirection = 'asc' | 'desc' | 'none';

// 表头配置类型新增冻结列核心属性
type HeaderConfig = {
  key: keyof TableData;
  label: string;
  frozen: boolean; // 冻结列标识,核心扩展属性
  width: number;   // 列宽固定值,保证跨端布局对齐
};

这种类型设计的核心价值在于:frozen 属性明确区分冻结列与非冻结列,width 属性为每列指定固定宽度,避免 flex 布局在多列场景下的适配问题,同时保证跨端开发时“哪些列冻结、每列宽度多少”的语义完全一致。

2. 冻结列布局实现

左侧冻结列的核心实现逻辑是“布局分层 + 绝对定位 + 空间补偿”,解决了横向滚动时左侧列固定显示的核心问题:

  • 布局分层设计:将表格分为“冻结列”和“非冻结列”两个独立的布局层级,冻结列通过 position: 'absolute' 固定在左侧,非冻结列通过水平滚动容器承载;
  • 空间补偿机制:非冻结列容器通过 marginLeft 为冻结列预留空间,其值等于所有冻结列宽度之和(headers.filter(h => h.frozen).reduce((sum, h) => sum + h.width, 0)),避免内容重叠;
  • 层级管理:冻结列通过 zIndex 保证层级高于非冻结列,避免被遮挡;
  • 表头与数据行对齐:表头和数据行采用完全一致的冻结/非冻结分层结构,保证列宽、位置的精准对齐;
  • 列宽精准控制:摒弃 flex 比例布局,采用固定宽度(如产品名称列 120px、类别列 100px),解决多列场景下的布局错位问题。

核心布局代码片段的设计思路解析:

// 冻结列表头:绝对定位 + zIndex 保证层级
<View style={styles.frozenHeader}>
  {headers.filter(h => h.frozen).map(header => (/* 冻结列表头渲染 */))}
</View>

// 非冻结列表头:水平滚动容器 + marginLeft 空间补偿
<ScrollView 
  horizontal 
  showsHorizontalScrollIndicator={false}
  style={styles.unfrozenHeader} // marginLeft = 所有冻结列宽度之和
>
  <View style={{ width: unfrozenWidth }}>
    {headers.filter(h => !h.frozen).map(header => (/* 非冻结列表头渲染 */))}
  </View>
</ScrollView>

// 数据行冻结列:绝对定位 + 背景色保证视觉独立
<View style={styles.frozenColumn}>
  <Text style={[styles.cell, { width: headers.find(h => h.key === 'name')?.width }]}>
    {item.name}
  </Text>
</View>

// 数据行非冻结列:水平滚动容器 + marginLeft 空间补偿
<ScrollView 
  horizontal 
  showsHorizontalScrollIndicator={false}
  style={styles.unfrozenColumns} // marginLeft = 所有冻结列宽度之和
>
  {/* 非冻结列数据渲染 */}
</ScrollView>

这种布局设计的关键在于:冻结列与非冻结列使用完全一致的宽度配置和空间补偿值,保证表头与数据行、冻结列与非冻结列的视觉对齐,同时通过绝对定位让冻结列脱离文档流,实现横向滚动时的固定效果。

3. 滚动同步机制

在冻结列表格中,垂直滚动的同步性是用户体验的核心,该组件预留了滚动同步的核心逻辑入口:

<ScrollView 
  horizontal 
  showsHorizontalScrollIndicator={false}
  style={styles.unfrozenColumns}
  onScroll={(e) => {
    // 同步滚动冻结列的核心逻辑入口
    const scrollY = e.nativeEvent.contentOffset.y;
    // 在实际应用中,这里可以同步滚动冻结列
  }}
>

虽然示例中仅预留了入口,但完整的滚动同步实现需要:

  • 监听非冻结列容器的垂直滚动事件,获取滚动偏移量 scrollY
  • 将该偏移量同步应用到冻结列容器的滚动位置;
  • 通过 Animated 动画库实现平滑滚动,避免滚动抖动;
  • 处理滚动边界条件,避免越界滚动。

这一机制是冻结列表格用户体验的关键,也是跨端适配时需要重点关注的交互细节。

4. 排序交互

排序交互逻辑在冻结列场景下保持了与基础表格的一致性,但针对冻结列/非冻结列做了精准的交互控制:

// 冻结列表头点击排序
<TouchableOpacity 
  key={header.key.toString()}
  style={[styles.columnHeader, { width: header.width }]}
  onPress={() => header.frozen && onSort?.(header.key)} // 仅冻结列触发排序
>
  {/* 排序指示器渲染 */}
</TouchableOpacity>

// 非冻结列表头点击排序
<TouchableOpacity 
  key={header.key.toString()}
  style={[styles.columnHeader, { width: header.width }]}
  onPress={() => !header.frozen && onSort?.(header.key)} // 仅非冻结列触发排序
>
  {/* 排序指示器渲染 */}
</TouchableOpacity>

排序核心逻辑保持不变,但通过 header.frozen 条件判断,保证不同类型列的排序交互精准触发,同时排序指示器在冻结列和非冻结列中保持一致的视觉表现,提升用户体验的统一性。

5. 列表渲染

在多列、长数据场景下,组件通过 FlatList 结合多项优化策略保证渲染性能:

  • 虚拟列表复用FlatList 仅渲染可视区域内的表格行,避免大量数据渲染导致的内存溢出;
  • 唯一标识优化keyExtractor={item => item.id} 保证列表项的唯一标识,避免重渲染时的性能损耗;
  • 滚动指示器控制:关闭水平/垂直滚动指示器(showsHorizontalScrollIndicator={false}/showsVerticalScrollIndicator={false}),减少渲染节点;
  • 文本截断优化:长文本字段(如 description)通过 substring(0, 10)... 截断,避免文本换行导致的布局错乱;
  • 数据不可变优化:排序/过滤时基于原始数据创建新数组([...filteredData]),保证 FlatList 高效重渲染。

React Native 与鸿蒙 ArkTS 虽基于不同的渲染引擎,但左侧冻结列表格的核心逻辑具备高度适配性,跨端迁移的核心是“冻结列布局等价转换、滚动同步机制适配、列表渲染性能对齐”。

1. 类型

鸿蒙端可直接复用 React Native 端的类型定义,仅需调整 TypeScript 语法细节,核心的冻结列配置语义完全一致:

// 鸿蒙 ArkTS 端类型定义(与 RN 端语义完全一致)
interface TableData {
  id: string;
  name: string;
  category: string;
  price: number;
  quantity: number;
  status: 'active' | 'inactive' | 'pending';
  date: string;
  description: string;
}

type SortDirection = 'asc' | 'desc' | 'none';

interface HeaderConfig {
  key: keyof TableData;
  label: string;
  frozen: boolean; // 冻结列标识,语义完全复用
  width: number;   // 列宽固定值,保证布局对齐
}

@Entry
@Component
struct FrozenLeftTableApp {
  // React Native useState → 鸿蒙 @State 等价转换
  @State tableData: TableData[] = [/* 与 RN 端一致 */];
  @State sortKey: keyof TableData | null = null;
  @State sortDirection: SortDirection = 'none';
  @State filterStatus: 'all' | 'active' | 'inactive' | 'pending' = 'all';
  @State showHeaders: boolean = true;

  // 表头配置完全复用 RN 端,冻结列规则一致
  private headers: HeaderConfig[] = [
    { key: 'name', label: '产品名称', frozen: true, width: 120 },
    { key: 'category', label: '类别', frozen: false, width: 100 },
    { key: 'price', label: '价格', frozen: false, width: 100 },
    { key: 'quantity', label: '数量', frozen: false, width: 80 },
    { key: 'status', label: '状态', frozen: false, width: 100 },
    { key: 'date', label: '日期', frozen: false, width: 100 },
    { key: 'description', label: '描述', frozen: false, width: 150 },
  ];
}

核心的排序/过滤逻辑可完全复用 React Native 端代码,仅需将 setState 替换为鸿蒙的状态赋值,例如排序处理逻辑:

// 排序处理逻辑完全复用,仅调整状态更新方式
handleSort(key: keyof TableData) {
  if (this.sortKey === key) {
    if (this.sortDirection === 'asc') {
      this.sortDirection = 'desc';
    } else if (this.sortDirection === 'desc') {
      this.sortDirection = 'none';
      this.sortKey = null;
    } else {
      this.sortDirection = 'asc';
    }
  } else {
    this.sortKey = key;
    this.sortDirection = 'asc';
  }
}

// 过滤与排序数据逻辑完全复用
get sortedData(): TableData[] {
  // 过滤逻辑
  const filteredData = this.tableData.filter(item => 
    this.filterStatus === 'all' || item.status === this.filterStatus
  );
  
  // 排序逻辑
  const result = [...filteredData];
  if (this.sortKey && this.sortDirection !== 'none') {
    result.sort((a, b) => {
      if (a[this.sortKey!] < b[this.sortKey!]) {
        return this.sortDirection === 'asc' ? -1 : 1;
      }
      if (a[this.sortKey!] > b[this.sortKey!]) {
        return this.sortDirection === 'asc' ? 1 : -1;
      }
      return 0;
    });
  }
  return result;
}

2. 冻结列布局

鸿蒙端的冻结列布局实现需适配 ArkTS 的布局体系,核心思路是“绝对定位 + 空间补偿 + Stack 布局嵌套”,保证冻结列与非冻结列的精准对齐:

// 鸿蒙 ArkTS 端 FrozenLeftTable 组件核心实现
@Component
struct FrozenLeftTable {
  private data: TableData[];
  private headers: HeaderConfig[];
  private onSort?: (key: keyof TableData) => void;
  private sortKey: keyof TableData | null;
  private sortDirection: SortDirection;

  // 计算冻结列总宽度,跨端复用的核心计算逻辑
  private get frozenWidth(): number {
    return this.headers.filter(h => h.frozen).reduce((sum, h) => sum + h.width, 0);
  }

  // 计算非冻结列总宽度
  private get unfrozenWidth(): number {
    return this.headers.filter(h => !h.frozen).reduce((sum, h) => sum + h.width, 0);
  }

  build() {
    Column()
      .backgroundColor('#ffffff')
      .borderRadius(8)
      .overflow(Overflow.Hidden)
      .width('100%')
    {
      // 表头区域:冻结列 + 非冻结列分层
      Stack() {
        // 非冻结列表头:水平滚动 + marginLeft 空间补偿
        Scroll()
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .marginLeft(this.frozenWidth)
          .width('100%')
        {
          Row()
            .width(this.unfrozenWidth)
            .backgroundColor('#f1f5f9')
          {
            ForEach(
              this.headers.filter(h => !h.frozen),
              (header: HeaderConfig) => {
                Button()
                  .width(header.width)
                  .paddingVertical(12)
                  .paddingHorizontal(8)
                  .backgroundColor(Transparent)
                  .onClick(() => !header.frozen && this.onSort?.(header.key))
                {
                  Row() {
                    Text(header.label)
                      .fontSize(12)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#1e293b');
                    
                    // 排序指示器,逻辑复用 RN 端
                    Text(this.sortKey === header.key 
                      ? (this.sortDirection === 'asc' ? '↑' : '↓') 
                      : '↕')
                      .fontSize(10)
                      .fontColor('#64748b')
                      .marginLeft(4);
                  }
                  .justifyContent(FlexAlign.SpaceBetween)
                  .alignItems(ItemAlign.Center);
                }
                .borderRightWidth(1)
                .borderRightColor('#cbd5e1');
              }
            )
          }
        }

        // 冻结列表头:绝对定位 + zIndex 保证层级
        Row()
          .width(this.frozenWidth)
          .backgroundColor('#f1f5f9')
          .position({ left: 0, top: 0 })
          .zIndex(1)
        {
          ForEach(
            this.headers.filter(h => h.frozen),
            (header: HeaderConfig) => {
              Button()
                .width(header.width)
                .paddingVertical(12)
                .paddingHorizontal(8)
                .backgroundColor(Transparent)
                .onClick(() => header.frozen && this.onSort?.(header.key))
              {
                Row() {
                  Text(header.label)
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#1e293b');
                  
                  Text(this.sortKey === header.key 
                    ? (this.sortDirection === 'asc' ? '↑' : '↓') 
                    : '↕')
                    .fontSize(10)
                    .fontColor('#64748b')
                    .marginLeft(4);
                }
                .justifyContent(FlexAlign.SpaceBetween)
                .alignItems(ItemAlign.Center);
              }
              .borderRightWidth(1)
              .borderRightColor('#cbd5e1');
            }
          )
        }
      }

      // 数据区域:冻结列 + 非冻结列分层
      List() {
        LazyForEach(
          new TableDataSource(this.data),
          (item: TableData) => {
            Stack() {
              // 非冻结列数据:水平滚动 + marginLeft 空间补偿
              Scroll()
                .scrollable(ScrollDirection.Horizontal)
                .scrollBar(BarState.Off)
                .marginLeft(this.frozenWidth)
                .width('100%')
              {
                Row()
                  .width(this.unfrozenWidth)
                  .borderBottomWidth(1)
                  .borderBottomColor('#e2e8f0')
                {
                  // 类别列
                  Text(item.category)
                    .width(this.headers.find(h => h.key === 'category')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .fontSize(12)
                    .fontColor('#1e293b')
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0');
                  
                  // 价格列
                  Text(`¥${item.price}`)
                    .width(this.headers.find(h => h.key === 'price')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .fontSize(12)
                    .fontColor('#1e293b')
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0');
                  
                  // 数量列
                  Text(`${item.quantity}`)
                    .width(this.headers.find(h => h.key === 'quantity')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .fontSize(12)
                    .fontColor('#1e293b')
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0');
                  
                  // 状态列
                  Row()
                    .width(this.headers.find(h => h.key === 'status')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0')
                    .alignItems(ItemAlign.Center)
                  {
                    Stack()
                      .width(8)
                      .height(8)
                      .borderRadius(4)
                      .backgroundColor(this.getStatusColor(item.status))
                      .marginRight(6);
                    
                    Text(this.getStatusText(item.status))
                      .fontSize(10)
                      .fontColor('#64748b');
                  }
                  
                  // 日期列
                  Text(item.date)
                    .width(this.headers.find(h => h.key === 'date')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .fontSize(12)
                    .fontColor('#1e293b')
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0');
                  
                  // 描述列
                  Text(`${item.description.substring(0, 10)}...`)
                    .width(this.headers.find(h => h.key === 'description')!.width)
                    .paddingVertical(12)
                    .paddingHorizontal(8)
                    .fontSize(12)
                    .fontColor('#1e293b')
                    .borderRightWidth(1)
                    .borderRightColor('#e2e8f0');
                }
              }

              // 冻结列数据:绝对定位 + 背景色保证视觉独立
              Row()
                .width(this.frozenWidth)
                .backgroundColor('#ffffff')
                .position({ left: 0 })
                .zIndex(1)
                .borderBottomWidth(1)
                .borderBottomColor('#e2e8f0')
              {
                Text(item.name)
                  .width(this.headers.find(h => h.key === 'name')!.width)
                  .paddingVertical(12)
                  .paddingHorizontal(8)
                  .fontSize(12)
                  .fontColor('#1e293b')
                  .borderRightWidth(1)
                  .borderRightColor('#e2e8f0');
              }
            }
          },
          (item: TableData) => item.id
        )
      }
      .showsVerticalScrollIndicator(false)
    }
  }

  // 状态颜色与文本转换逻辑完全复用 RN 端
  private getStatusColor(status: string): string {
    switch (status) {
      case 'active': return '#10b981';
      case 'inactive': return '#ef4444';
      case 'pending': return '#f59e0b';
      default: return '#64748b';
    }
  }

  private getStatusText(status: string): string {
    switch (status) {
      case 'active': return '活跃';
      case 'inactive': return '非活跃';
      case 'pending': return '待处理';
      default: return '未知';
    }
  }
}

// 自定义数据源,实现列表项复用
class TableDataSource extends BaseDataSource {
  private data: TableData[] = [];

  constructor(data: TableData[]) {
    super();
    this.data = data;
  }

  totalCount(): number {
    return this.data.length;
  }

  getData(index: number): TableData {
    return this.data[index];
  }
}

关键适配细节:

  • 冻结列布局等价转换:React Native 的 position: 'absolute' 等价转换为鸿蒙的 position({ left: 0, top: 0 })zIndex: 1 等价转换为 zIndex(1)
  • 空间补偿机制对齐:非冻结列容器的 marginLeft 值在鸿蒙端与 RN 端完全一致(均为所有冻结列宽度之和),保证冻结列与非冻结列不重叠;
  • 列宽精准控制:所有列均使用固定宽度(如 120px、100px),避免 flex 布局在多列场景下的适配问题,跨端布局完全对齐;
  • 滚动容器等价转换:RN 端的 ScrollView horizontal 等价转换为鸿蒙的 Scroll().scrollable(ScrollDirection.Horizontal),滚动指示器控制逻辑一致;
  • 层级管理优化:通过 Stack 布局嵌套实现冻结列与非冻结列的层级管理,保证冻结列始终在视觉上层。

3. 滚动同步机制:跨端交互体验对齐

左侧冻结列表格的核心用户体验痛点是“垂直滚动时冻结列与非冻结列不同步”,鸿蒙端需实现与 RN 端一致的滚动同步机制:

// 鸿蒙端滚动同步核心实现
@State scrollOffsetY: number = 0; // 共享滚动偏移量

// 非冻结列滚动监听
Scroll()
  .onScroll((e: ScrollEvent) => {
    this.scrollOffsetY = e.offsetY;
    // 同步冻结列滚动位置
    this.frozenListScroller.scrollTo({ y: e.offsetY, animation: false });
  })

// 冻结列滚动容器绑定滚动控制器
List()
  .scroller(this.frozenListScroller)
  .onScroll((e: ScrollEvent) => {
    if (Math.abs(e.offsetY - this.scrollOffsetY) > 1) {
      this.scrollOffsetY = e.offsetY;
      // 同步非冻结列滚动位置
      this.unfrozenListScroller.scrollTo({ y: e.offsetY, animation: false });
    }
  })

核心实现思路:

  • 定义共享的滚动偏移量 scrollOffsetY,存储当前垂直滚动位置;
  • 监听非冻结列的滚动事件,同步更新冻结列的滚动位置;
  • 监听冻结列的滚动事件,反向同步非冻结列的滚动位置;
  • 增加偏移量差值判断(Math.abs(e.offsetY - this.scrollOffsetY) > 1),避免滚动抖动;
  • 关闭滚动动画(animation: false),保证同步的即时性。

这一机制在 RN 端的实现逻辑完全一致,仅需将鸿蒙的 scroller.scrollTo 替换为 RN 端的 scrollViewRef.current.scrollTo 即可。

4. 策略

左侧冻结列表格在多列、长数据场景下的性能优化需遵循跨端通用策略:

  • 虚拟列表复用:RN 端的 FlatList 与鸿蒙端的 LazyForEach 均采用虚拟列表技术,仅渲染可视区域内的单元格;
  • 减少重渲染:RN 端使用 React.memo 包裹组件,鸿蒙端使用 @Memo 装饰器,仅在 Props 变化时触发重渲染;
  • 数据处理优化:排序/过滤逻辑在鸿蒙端通过计算属性(get sortedData())实现,仅在依赖状态变化时重新计算;
  • 布局层级优化:冻结列与非冻结列采用 Stack 布局嵌套,减少不必要的布局层级;
  • 文本渲染优化:长文本字段截断处理,避免文本换行导致的布局计算开销;
  • 滚动优化:关闭不必要的滚动指示器,减少渲染节点。

2. 布局设计

冻结列布局的跨端实现需重点关注:

  • 空间补偿机制:非冻结列容器的 marginLeft 必须等于所有冻结列宽度之和,避免内容重叠;
  • 层级管理:冻结列通过 zIndex 保证层级高于非冻结列,避免被遮挡;
  • 固定宽度优先:多列场景下优先使用固定宽度,避免 flex 布局的适配问题;
  • 背景色独立:冻结列需设置独立的背景色(如白色),避免非冻结列内容透过冻结列显示。

3. 滚动同步

滚动同步是冻结列表格用户体验的核心,跨端实现需遵循:

  • 共享滚动偏移量:通过状态变量存储当前滚动位置,保证冻结列与非冻结列的滚动偏移量一致;
  • 双向同步机制:既监听非冻结列滚动同步冻结列,也监听冻结列滚动同步非冻结列;
  • 无动画同步:滚动同步时关闭动画,保证体验的即时性;
  • 边界条件处理:避免滚动偏移量超出数据范围,导致空白区域显示。

关键点

  1. React Native 左侧冻结列表格组件的核心价值在于“冻结列分层布局 + 滚动同步机制 + 固定列宽适配”,这些核心逻辑不依赖框架特性,为跨端适配提供了 90% 以上的代码复用率;
  2. 鸿蒙跨端适配的核心是“冻结列布局等价转换、滚动同步机制适配、列表渲染性能对齐”,仅需适配布局语法与平台特定 API,核心业务逻辑无需重构;
  3. 跨端冻结列表格开发应遵循“类型标准化、布局精准化、滚动同步化、性能最优化”的原则,保证数据层的标准化、布局层的一致性、交互层的体验等价与渲染层的性能平衡。

在企业级移动应用中,当表格列数较多、屏幕空间有限时,如何让用户既能查看完整的列信息,又能保持关键标识列(如产品名称、用户姓名)始终可见,是一个普遍而关键的需求。固定左侧列表格正是为解决这一场景而设计的高级组件,它通过双层滚动架构实现了左侧关键列固定、右侧数据列可滚动的效果。FixedLeftTableApp组件展示了如何在React Native中实现这类复杂的表格功能,从冻结列配置、双向滚动同步到排序过滤功能,形成了一个完整的固定列表格解决方案。

从技术架构的角度来看,固定左侧列表格比固定表头表格更加复杂,因为它不仅需要处理垂直滚动,还需要精确控制水平滚动的同步关系。这种需求要求我们在设计组件时充分考虑滚动事件的分发、视觉位置的精确控制以及性能优化等多个维度。

冻结列

组件通过HeaderConfig类型定义了灵活的列配置,每个列都可以独立控制是否冻结:

type HeaderConfig = {
  key: keyof TableData;
  label: string;
  frozen: boolean; // 是否冻结列
  width: number;
};

const headers: HeaderConfig[] = [
  { key: 'name', label: '产品名称', frozen: true, width: 120 },
  { key: 'category', label: '类别', frozen: false, width: 100 },
  { key: 'price', label: '价格', frozen: false, width: 100 },
  { key: 'quantity', label: '数量', frozen: false, width: 80 },
  { key: 'status', label: '状态', frozen: false, width: 100 },
  { key: 'date', label: '日期', frozen: false, width: 100 },
  { key: 'description', label: '描述', frozen: false, width: 150 },
];

这种配置系统在鸿蒙平台可以通过更丰富的类型定义实现扩展:

@Observed
class FrozenHeaderConfigHarmony {
  key: keyof TableDataHarmony;
  label: string;
  frozen: boolean = false;
  width: number = 100;
  minWidth: number = 60;
  maxWidth: number = 200;
  sortable: boolean = true;
  formatter?: (value: any) => string;
  align: TextAlign = TextAlign.Center;
  
  // 计算冻结列的总宽度
  @Computed
  get frozenTotalWidth(): number {
    if (!this.frozen) return 0;
    return this.width;
  }
  
  // 克隆配置
  clone(): FrozenHeaderConfigHarmony {
    const config = new FrozenHeaderConfigHarmony();
    Object.assign(config, this);
    return config;
  }
}

分离渲染

组件采用了完全分离的渲染策略,冻结列和非冻结列分别位于不同的容器中:

<View style={styles.tableContainer}>
  {/* 表头区域 - 同样分离 */}
  <View style={styles.tableHeaderRow}>
    <View style={styles.frozenHeader}>
      {/* 冻结列表头 */}
      {headers.filter(h => h.frozen).map(header => (
        <TouchableOpacity key={header.key} style={[styles.columnHeader, { width: header.width }]}>
          <Text>{header.label}</Text>
        </TouchableOpacity>
      ))}
    </View>
    
    <ScrollView horizontal showsHorizontalScrollIndicator={false}>
      <View style={{ width: unfrozenWidth }}>
        {/* 非冻结列表头 */}
        {headers.filter(h => !h.frozen).map(header => (
          <TouchableOpacity key={header.key} style={[styles.columnHeader, { width: header.width }]}>
            <Text>{header.label}</Text>
          </TouchableOpacity>
        ))}
      </View>
    </ScrollView>
  </View>
  
  {/* 数据行 - 分离渲染 */}
  <FlatList
    data={data}
    keyExtractor={item => item.id}
    renderItem={({ item }) => (
      <View style={styles.tableDataRow}>
        {/* 冻结列 - 绝对定位 */}
        <View style={styles.frozenColumn}>
          <Text style={[styles.cell, { width: 120 }]}>{item.name}</Text>
        </View>
        
        {/* 非冻结列 - 可滚动 */}
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          <View style={{ width: unfrozenWidth }}>
            {/* 单元格内容 */}
          </View>
        </ScrollView>
      </View>
    )}
  />
</View>

这种分离架构在鸿蒙中可以通过更优雅的方式实现:

@Component
struct FrozenLeftTableHarmony {
  @Prop headers: FrozenHeaderConfigHarmony[] = [];
  @Prop data: TableDataHarmony[] = [];
  
  @Builder
  buildHeader() {
    Row() {
      // 冻结区域
      Column() {
        ForEach(this.headers.filter(h => h.frozen), (header: FrozenHeaderConfigHarmony) => {
          Column() {
            Text(header.label)
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
          }
          .width(header.width)
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width(this.calculateFrozenWidth())
      .backgroundColor('#f1f5f9')
      
      // 非冻结区域
      Scroll() {
        Row() {
          ForEach(this.headers.filter(h => !h.frozen), (header: FrozenHeaderConfigHarmony) => {
            Column() {
              Text(header.label)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
            }
            .width(header.width)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
    }
    .height(44)
  }
  
  @Builder
  buildRow(item: TableDataHarmony) {
    Row() {
      // 冻结列
      ForEach(this.headers.filter(h => h.frozen), (header: FrozenHeaderConfigHarmony) => {
        Column() {
          Text(this.formatValue(item[header.key], header))
            .fontSize(12)
        }
        .width(header.width)
        .height(48)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#ffffff')
      })
      .width(this.calculateFrozenWidth())
      
      // 非冻结列
      Scroll() {
        Row() {
          ForEach(this.headers.filter(h => !h.frozen), (header: FrozenHeaderConfigHarmony) => {
            Column() {
              Text(this.formatValue(item[header.key], header))
                .fontSize(12)
            }
            .width(header.width)
            .height(48)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
    }
    .border({ width: 1, color: '#e2e8f0' })
  }
  
  private calculateFrozenWidth(): number {
    return this.headers
      .filter(h => h.frozen)
      .reduce((sum, h) => sum + h.width, 0);
  }
  
  build() {
    Column() {
      this.buildHeader()
      
      List() {
        ForEach(this.data, (item: TableDataHarmony) => {
          ListItem() {
            this.buildRow(item)
          }
        }, (item: TableDataHarmony) => item.id)
      }
      .divider({ strokeWidth: 1, color: '#e2e8f0' })
    }
    .border({ width: 1, color: '#cbd5e1' })
    .borderRadius(8)
  }
}

滚动事件

组件需要处理垂直和水平两个方向的滚动,并保持它们之间的同步关系:

<ScrollView 
  horizontal 
  showsHorizontalScrollIndicator={false}
  onScroll={(e) => {
    const scrollX = e.nativeEvent.contentOffset.x;
    // 在实际应用中,需要同步其他行的ScrollView
  }}
  scrollEventThrottle={16}
/>

// FlatList的垂直滚动同样需要处理
<FlatList
  data={data}
  onScroll={(e) => {
    const scrollY = e.nativeEvent.contentOffset.y;
    // 更新内部状态或触发父组件回调
  }}
/>

在鸿蒙平台,可以通过更精确的滚动同步机制实现:

// 鸿蒙滚动同步控制器
@Observed
class ScrollSyncController {
  @State frozenScrollX: number = 0;
  @State unfrozenScrollX: number = 0;
  @State rowScrollX: Map<string, number> = new Map();
  
  // 同步冻结列的滚动位置
  syncFrozenScroll(targetX: number, rowId?: string): void {
    this.frozenScrollX = targetX;
    
    if (rowId) {
      this.rowScrollX.set(rowId, targetX);
    }
  }
  
  // 同步非冻结列的滚动位置
  syncUnfrozenScroll(targetX: number, rowId?: string): void {
    this.unfrozenScrollX = targetX;
    
    if (rowId) {
      this.rowScrollX.set(rowId, targetX);
    }
  }
  
  // 获取特定行的滚动位置
  getRowScrollX(rowId: string): number {
    return this.rowScrollX.get(rowId) || 0;
  }
}

位置

为了确保冻结列和非冻结列的内容对齐,需要精确计算每个元素的宽度和位置:

// 计算非冻结列的总宽度
const unfrozenHeaders = headers.filter(h => !h.frozen);
const unfrozenWidth = unfrozenHeaders.reduce((sum, h) => sum + h.width, 0);

// 冻结列的绝对定位
const frozenWidth = headers.filter(h => h.frozen).reduce((sum, h) => sum + h.width, 0);

// 在样式中使用
const styles = {
  frozenColumn: {
    position: 'absolute',
    left: 0,
    width: frozenWidth,
    zIndex: 1,
  },
  unfrozenColumns: {
    flex: 1,
    marginLeft: frozenWidth,
  }
};

虚拟列表优化

由于表格可能包含大量数据,需要使用虚拟列表优化渲染性能:

<FlatList
  data={sortedData}
  keyExtractor={item => item.id}
  renderItem={({ item, index }) => (
    <TableRow item={item} index={index} />
  )}
  getItemLayout={(data, index) => ({
    length: 48, // 固定行高
    offset: 48 * index,
    index,
  })}
  initialNumToRender={10}
  maxToRenderPerBatch={5}
  windowSize={5}
  removeClippedSubviews={true}
/>

状态更新

对于复杂的排序和过滤逻辑,使用useMemo避免不必要的重计算:

const sortedData = useMemo(() => {
  const data = [...filteredData];
  if (sortKey && sortDirection !== 'none') {
    data.sort((a, b) => {
      const modifier = sortDirection === 'asc' ? 1 : -1;
      if (a[sortKey] < b[sortKey]) return -modifier;
      if (a[sortKey] > b[sortKey]) return modifier;
      return 0;
    });
  }
  return data;
}, [filteredData, sortKey, sortDirection]);

统一接口定义

定义跨平台的组件接口,确保功能一致性:

interface IFrozenTableComponent {
  data: TableData[];
  headers: HeaderConfig[];
  onSort?: (key: keyof TableData) => void;
  sortKey?: keyof TableData | null;
  sortDirection?: SortDirection;
  onFilter?: (status: FilterStatus) => void;
  filterStatus?: FilterStatus;
  frozenKey?: keyof TableData; // 冻结列的标识列
  showHeaders?: boolean;
}

// React Native实现
const FrozenLeftTable: React.FC<IFrozenTableComponent> = (props) => {
  // RN特定实现
};

// 鸿蒙实现
@Component
struct FrozenLeftTableHarmony implements IFrozenTableComponent {
  // 鸿蒙特定实现
}

FrozenLeftTableApp组件展示了构建企业级固定列表格组件的完整技术栈。通过分离渲染架构、双层滚动同步、灵活的配置系统和性能优化策略,实现了既美观又高效的表格组件。

在向鸿蒙平台迁移时,重点关注声明式UI的组件构建方式、Grid和Scroll组件的组合使用以及性能优化的最佳实践。通过合理的架构设计和代码复用,可以构建出适应多平台的固定列表格解决方案。


真实演示案例代码:

// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList } from 'react-native';

// Base64 图标库
const ICONS_BASE64 = {
  table: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  freeze: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  scroll: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  fixed: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  settings: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  info: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  sort: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};

const { width, height } = Dimensions.get('window');

// 表格数据类型
type TableData = {
  id: string;
  name: string;
  category: string;
  price: number;
  quantity: number;
  status: 'active' | 'inactive' | 'pending';
  date: string;
  description: string;
};

// 排序方向
type SortDirection = 'asc' | 'desc' | 'none';

// 表头类型
type HeaderConfig = {
  key: keyof TableData;
  label: string;
  frozen: boolean; // 是否冻结列
  width: number;
};

// 固定左侧列表格组件
const FrozenLeftTable: React.FC<{
  data: TableData[];
  headers: HeaderConfig[];
  onSort?: (key: keyof TableData) => void;
  sortKey: keyof TableData | null;
  sortDirection: SortDirection;
}> = ({ data, headers, onSort, sortKey, sortDirection }) => {
  const getStatusColor = (status: string) => {
    switch (status) {
      case 'active': return '#10b981';
      case 'inactive': return '#ef4444';
      case 'pending': return '#f59e0b';
      default: return '#64748b';
    }
  };

  // 计算非冻结列的宽度
  const unfrozenHeaders = headers.filter(h => !h.frozen);
  const unfrozenWidth = unfrozenHeaders.reduce((sum, h) => sum + h.width, 0);

  return (
    <View style={styles.tableContainer}>
      {/* 表头 */}
      <View style={styles.tableHeaderRow}>
        {/* 冻结列表头 */}
        <View style={styles.frozenHeader}>
          {headers.filter(h => h.frozen).map(header => (
            <TouchableOpacity 
              key={header.key.toString()}
              style={[styles.columnHeader, { width: header.width }]}
              onPress={() => header.frozen && onSort?.(header.key)}
            >
              <Text style={styles.columnHeaderText}>{header.label}</Text>
              {header.frozen && (
                <Text style={styles.sortIcon}>
                  {sortKey === header.key 
                    ? (sortDirection === 'asc' ? '↑' : '↓') 
                    : '↕'}
                </Text>
              )}
            </TouchableOpacity>
          ))}
        </View>
        
        {/* 非冻结列表头 */}
        <ScrollView 
          horizontal 
          showsHorizontalScrollIndicator={false}
          style={styles.unfrozenHeader}
        >
          <View style={{ width: unfrozenWidth }}>
            {headers.filter(h => !h.frozen).map(header => (
              <TouchableOpacity 
                key={header.key.toString()}
                style={[styles.columnHeader, { width: header.width }]}
                onPress={() => !header.frozen && onSort?.(header.key)}
              >
                <Text style={styles.columnHeaderText}>{header.label}</Text>
                {!header.frozen && (
                  <Text style={styles.sortIcon}>
                    {sortKey === header.key 
                      ? (sortDirection === 'asc' ? '↑' : '↓') 
                      : '↕'}
                  </Text>
                )}
              </TouchableOpacity>
            ))}
          </View>
        </ScrollView>
      </View>
      
      {/* 数据行 */}
      <FlatList
        data={data}
        keyExtractor={item => item.id}
        renderItem={({ item, index }) => (
          <View style={styles.tableDataRow}>
            {/* 冻结列 */}
            <View style={styles.frozenColumn}>
              <Text style={[styles.cell, { width: headers.find(h => h.key === 'name')?.width }]}>
                {item.name}
              </Text>
            </View>
            
            {/* 非冻结列 */}
            <ScrollView 
              horizontal 
              showsHorizontalScrollIndicator={false}
              style={styles.unfrozenColumns}
              onScroll={(e) => {
                // 同步滚动冻结列
                const scrollY = e.nativeEvent.contentOffset.y;
                // 在实际应用中,这里可以同步滚动冻结列
              }}
            >
              <View style={{ width: unfrozenWidth }}>
                <Text style={[styles.cell, { width: headers.find(h => h.key === 'category')?.width }]}>
                  {item.category}
                </Text>
                <Text style={[styles.cell, { width: headers.find(h => h.key === 'price')?.width }]}>
                  ¥{item.price}
                </Text>
                <Text style={[styles.cell, { width: headers.find(h => h.key === 'quantity')?.width }]}>
                  {item.quantity}
                </Text>
                <View style={[styles.cell, { width: headers.find(h => h.key === 'status')?.width }]}>
                  <View style={[styles.statusIndicator, { backgroundColor: getStatusColor(item.status) }]} />
                  <Text style={styles.statusText}>
                    {item.status === 'active' ? '活跃' : item.status === 'inactive' ? '非活跃' : '待处理'}
                  </Text>
                </View>
                <Text style={[styles.cell, { width: headers.find(h => h.key === 'date')?.width }]}>
                  {item.date}
                </Text>
                <Text style={[styles.cell, { width: headers.find(h => h.key === 'description')?.width }]}>
                  {item.description.substring(0, 10)}...
                </Text>
              </View>
            </ScrollView>
          </View>
        )}
        showsVerticalScrollIndicator={false}
      />
    </View>
  );
};

const FrozenLeftTableApp: React.FC = () => {
  const [tableData, setTableData] = useState<TableData[]>([
    { id: '1', name: 'iPhone 13', category: '手机', price: 5999, quantity: 50, status: 'active', date: '2023-01-15', description: '苹果最新款智能手机' },
    { id: '2', name: 'MacBook Pro', category: '电脑', price: 12999, quantity: 20, status: 'active', date: '2023-01-20', description: '专业级笔记本电脑' },
    { id: '3', name: 'iPad Air', category: '平板', price: 4399, quantity: 30, status: 'pending', date: '2023-02-01', description: '轻薄便携平板电脑' },
    { id: '4', name: 'AirPods Pro', category: '耳机', price: 1999, quantity: 80, status: 'active', date: '2023-02-05', description: '无线降噪耳机' },
    { id: '5', name: 'Apple Watch', category: '手表', price: 2999, quantity: 40, status: 'inactive', date: '2023-02-10', description: '智能手表' },
    { id: '6', name: 'Magic Mouse', category: '配件', price: 749, quantity: 100, status: 'active', date: '2023-02-15', description: '无线鼠标' },
    { id: '7', name: 'Magic Keyboard', category: '配件', price: 1099, quantity: 60, status: 'pending', date: '2023-02-20', description: '无线键盘' },
    { id: '8', name: 'HomePod mini', category: '音响', price: 749, quantity: 25, status: 'active', date: '2023-02-25', description: '智能音箱' },
    { id: '9', name: 'Apple TV 4K', category: '电视', price: 1799, quantity: 15, status: 'active', date: '2023-03-01', description: '4K高清电视盒子' },
    { id: '10', name: 'AirTag', category: '配件', price: 229, quantity: 200, status: 'active', date: '2023-03-05', description: '物品追踪器' },
  ]);
  
  const [sortKey, setSortKey] = useState<keyof TableData | null>(null);
  const [sortDirection, setSortDirection] = useState<SortDirection>('none');
  const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive' | 'pending'>('all');
  const [showHeaders, setShowHeaders] = useState<boolean>(true);

  // 表头配置
  const headers: HeaderConfig[] = [
    { key: 'name', label: '产品名称', frozen: true, width: 120 },
    { key: 'category', label: '类别', frozen: false, width: 100 },
    { key: 'price', label: '价格', frozen: false, width: 100 },
    { key: 'quantity', label: '数量', frozen: false, width: 80 },
    { key: 'status', label: '状态', frozen: false, width: 100 },
    { key: 'date', label: '日期', frozen: false, width: 100 },
    { key: 'description', label: '描述', frozen: false, width: 150 },
  ];

  // 排序处理
  const handleSort = (key: keyof TableData) => {
    if (sortKey === key) {
      if (sortDirection === 'asc') {
        setSortDirection('desc');
      } else if (sortDirection === 'desc') {
        setSortDirection('none');
        setSortKey(null);
      } else {
        setSortDirection('asc');
      }
    } else {
      setSortKey(key);
      setSortDirection('asc');
    }
  };

  // 过滤数据
  const filteredData = tableData.filter(item => 
    filterStatus === 'all' || item.status === filterStatus
  );

  // 排序数据
  const sortedData = [...filteredData];
  if (sortKey && sortDirection !== 'none') {
    sortedData.sort((a, b) => {
      if (a[sortKey] < b[sortKey]) {
        return sortDirection === 'asc' ? -1 : 1;
      }
      if (a[sortKey] > b[sortKey]) {
        return sortDirection === 'asc' ? 1 : -1;
      }
      return 0;
    });
  }

  // 添加新数据
  const addNewItem = () => {
    const newItem: TableData = {
      id: `${tableData.length + 1}`,
      name: `新产品 ${tableData.length + 1}`,
      category: '配件',
      price: 999,
      quantity: 50,
      status: 'active',
      date: new Date().toISOString().split('T')[0],
      description: '新添加的产品',
    };
    setTableData([...tableData, newItem]);
    Alert.alert('成功', '新项目已添加');
  };

  return (
    <SafeAreaView style={styles.container}>
      {/* 头部 */}
      <View style={styles.header}>
        <Text style={styles.title}>固定左侧列表格</Text>
        <Text style={styles.subtitle}>左右滚动时左侧列保持可见</Text>
      </View>

      <ScrollView style={styles.content}>
        {/* 控制面板 */}
        <View style={styles.controlPanel}>
          <Text style={styles.controlTitle}>表格控制</Text>
          
          <View style={styles.controlRow}>
            <Text style={styles.controlLabel}>过滤状态</Text>
            <View style={styles.filterSelector}>
              <TouchableOpacity 
                style={[styles.filterButton, filterStatus === 'all' && styles.filterButtonActive]}
                onPress={() => setFilterStatus('all')}
              >
                <Text style={[styles.filterText, filterStatus === 'all' && styles.filterTextActive]}>全部</Text>
              </TouchableOpacity>
              <TouchableOpacity 
                style={[styles.filterButton, filterStatus === 'active' && styles.filterButtonActive]}
                onPress={() => setFilterStatus('active')}
              >
                <Text style={[styles.filterText, filterStatus === 'active' && styles.filterTextActive]}>活跃</Text>
              </TouchableOpacity>
              <TouchableOpacity 
                style={[styles.filterButton, filterStatus === 'inactive' && styles.filterButtonActive]}
                onPress={() => setFilterStatus('inactive')}
              >
                <Text style={[styles.filterText, filterStatus === 'inactive' && styles.filterTextActive]}>非活跃</Text>
              </TouchableOpacity>
              <TouchableOpacity 
                style={[styles.filterButton, filterStatus === 'pending' && styles.filterButtonActive]}
                onPress={() => setFilterStatus('pending')}
              >
                <Text style={[styles.filterText, filterStatus === 'pending' && styles.filterTextActive]}>待处理</Text>
              </TouchableOpacity>
            </View>
          </View>
          
          <View style={styles.controlRow}>
            <Text style={styles.controlLabel}>显示表头</Text>
            <TouchableOpacity 
              style={[styles.toggleButton, showHeaders && styles.toggleButtonActive]}
              onPress={() => setShowHeaders(!showHeaders)}
            >
              <Text style={[styles.toggleText, showHeaders && styles.toggleTextActive]}>
                {showHeaders ? '开启' : '关闭'}
              </Text>
            </TouchableOpacity>
          </View>
          
          <TouchableOpacity 
            style={styles.addButton}
            onPress={addNewItem}
          >
            <Text style={styles.addButtonText}>添加新项目</Text>
          </TouchableOpacity>
        </View>

        {/* 表格 */}
        <View style={styles.tableWrapper}>
          {showHeaders && (
            <FrozenLeftTable 
              data={sortedData}
              headers={headers}
              onSort={handleSort}
              sortKey={sortKey}
              sortDirection={sortDirection}
            />
          )}
        </View>

        {/* 表格统计 */}
        <View style={styles.statsCard}>
          <Text style={styles.statsTitle}>表格统计</Text>
          <View style={styles.statRow}>
            <Text style={styles.statLabel}>总项目数</Text>
            <Text style={styles.statValue}>{tableData.length}</Text>
          </View>
          <View style={styles.statRow}>
            <Text style={styles.statLabel}>活跃项目</Text>
            <Text style={styles.statValue}>{tableData.filter(i => i.status === 'active').length}</Text>
          </View>
          <View style={styles.statRow}>
            <Text style={styles.statLabel}>待处理项目</Text>
            <Text style={styles.statValue}>{tableData.filter(i => i.status === 'pending').length}</Text>
          </View>
          <View style={styles.statRow}>
            <Text style={styles.statLabel}>总价值</Text>
            <Text style={styles.statValue}>¥{tableData.reduce((sum, item) => sum + item.price, 0)}</Text>
          </View>
        </View>

        {/* 特性说明 */}
        <View style={styles.featuresCard}>
          <Text style={styles.featuresTitle}>固定左侧列特性</Text>
          <View style={styles.featureRow}>
            <Text style={styles.featureIcon}>🔒</Text>
            <Text style={styles.featureText}>左侧列始终可见</Text>
          </View>
          <View style={styles.featureRow}>
            <Text style={styles.featureIcon}>🔍</Text>
            <Text style={styles.featureText}>支持排序功能</Text>
          </View>
          <View style={styles.featureRow}>
            <Text style={styles.featureIcon}>📊</Text>
            <Text style={styles.featureText}>数据过滤功能</Text>
          </View>
          <View style={styles.featureRow}>
            <Text style={styles.featureIcon}>📱</Text>
            <Text style={styles.featureText}>响应式设计</Text>
          </View>
        </View>

        {/* 使用场景 */}
        <View style={styles.sceneCard}>
          <Text style={styles.sceneTitle}>使用场景</Text>
          
          <View style={styles.sceneRow}>
            <TouchableOpacity 
              style={styles.sceneItem}
              onPress={() => Alert.alert('数据报表', '长数据列表展示场景')}
            >
              <Text style={styles.sceneItemText}>数据报表</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={styles.sceneItem}
              onPress={() => Alert.alert('订单管理', '订单信息展示场景')}
            >
              <Text style={styles.sceneItemText}>订单管理</Text>
            </TouchableOpacity>
          </View>
          
          <View style={styles.sceneRow}>
            <TouchableOpacity 
              style={styles.sceneItem}
              onPress={() => Alert.alert('库存管理', '库存信息展示场景')}
            >
              <Text style={styles.sceneItemText}>库存管理</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={styles.sceneItem}
              onPress={() => Alert.alert('客户列表', '客户信息展示场景')}
            >
              <Text style={styles.sceneItemText}>客户列表</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 实现说明 */}
        <View style={styles.infoCard}>
          <Text style={styles.infoTitle}>实现说明</Text>
          <Text style={styles.infoText}>• 左侧列固定在容器左侧</Text>
          <Text style={styles.infoText}>• 其余列可水平滚动</Text>
          <Text style={styles.infoText}>• 支持多列排序功能</Text>
          <Text style={styles.infoText}>• 响应式设计适配不同屏幕</Text>
        </View>
      </ScrollView>

      {/* 底部导航 */}
      <View style={styles.bottomNav}>
        <TouchableOpacity 
          style={[styles.navItem, styles.activeNavItem]} 
          onPress={() => Alert.alert('首页')}
        >
          <Text style={styles.navIcon}>🏠</Text>
          <Text style={styles.navText}>首页</Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={styles.navItem} 
          onPress={() => Alert.alert('表格')}
        >
          <Text style={styles.navIcon}>📊</Text>
          <Text style={styles.navText}>表格</Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={styles.navItem} 
          onPress={() => Alert.alert('功能')}
        >
          <Text style={styles.navIcon}>⚙️</Text>
          <Text style={styles.navText}>功能</Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={styles.navItem} 
          onPress={() => Alert.alert('设置')}
        >
          <Text style={styles.navIcon}>🔧</Text>
          <Text style={styles.navText}>设置</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
  },
  header: {
    padding: 20,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: '#64748b',
  },
  content: {
    flex: 1,
    padding: 16,
  },
  controlPanel: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  controlTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  controlRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 12,
  },
  controlLabel: {
    fontSize: 14,
    color: '#64748b',
    flex: 1,
  },
  filterSelector: {
    flexDirection: 'row',
  },
  filterButton: {
    backgroundColor: '#e2e8f0',
    paddingHorizontal: 10,
    paddingVertical: 6,
    borderRadius: 6,
    marginHorizontal: 2,
  },
  filterButtonActive: {
    backgroundColor: '#3b82f6',
  },
  filterText: {
    fontSize: 12,
    color: '#1e293b',
  },
  filterTextActive: {
    color: '#ffffff',
  },
  toggleButton: {
    backgroundColor: '#e2e8f0',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
  },
  toggleButtonActive: {
    backgroundColor: '#10b981',
  },
  toggleText: {
    fontSize: 12,
    color: '#1e293b',
  },
  toggleTextActive: {
    color: '#ffffff',
  },
  addButton: {
    backgroundColor: '#3b82f6',
    padding: 12,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  addButtonText: {
    color: '#ffffff',
    fontSize: 14,
    fontWeight: '500',
  },
  tableWrapper: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  tableContainer: {
    borderRadius: 8,
    overflow: 'hidden',
  },
  tableHeaderRow: {
    flexDirection: 'row',
    backgroundColor: '#f1f5f9',
  },
  frozenHeader: {
    flexDirection: 'row',
    backgroundColor: '#f1f5f9',
    position: 'absolute',
    top: 0,
    left: 0,
    zIndex: 2,
  },
  unfrozenHeader: {
    flex: 1,

  },
  tableDataRow: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  frozenColumn: {
    flexDirection: 'row',
    position: 'absolute',
    left: 0,
    zIndex: 1,
    backgroundColor: '#ffffff',
  },
  unfrozenColumns: {
    flex: 1,
  },
  columnHeader: {
    paddingVertical: 12,
    paddingHorizontal: 8,
    fontSize: 12,
    fontWeight: '600',
    color: '#1e293b',
    borderRightWidth: 1,
    borderRightColor: '#cbd5e1',
    justifyContent: 'space-between',
    alignItems: 'center',
    flexDirection: 'row',
  },
  columnHeaderText: {
    fontSize: 12,
    fontWeight: '600',
    color: '#1e293b',
  },
  sortIcon: {
    fontSize: 10,
    color: '#64748b',
    marginLeft: 4,
  },
  cell: {
    paddingVertical: 12,
    paddingHorizontal: 8,
    fontSize: 12,
    color: '#1e293b',
    borderRightWidth: 1,
    borderRightColor: '#e2e8f0',
    justifyContent: 'center',
  },
  statusCell: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  statusIndicator: {
    width: 8,
    height: 8,
    borderRadius: 4,
    marginRight: 6,
  },
  statusText: {
    fontSize: 10,
    color: '#64748b',
  },
  statsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  statsTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  statRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 6,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  statLabel: {
    fontSize: 14,
    color: '#64748b',
  },
  statValue: {
    fontSize: 14,
    color: '#1e293b',
    fontWeight: '500',
  },
  featuresCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  featuresTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  featureRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  featureIcon: {
    fontSize: 18,
    marginRight: 8,
  },
  featureText: {
    fontSize: 14,
    color: '#1e293b',
    flex: 1,
  },
  sceneCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  sceneTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  sceneRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 8,
  },
  sceneItem: {
    flex: 1,
    padding: 12,
    borderRadius: 8,
    backgroundColor: '#e2e8f0',
    alignItems: 'center',
    marginHorizontal: 4,
  },
  sceneItemText: {
    fontSize: 12,
    color: '#1e293b',
  },
  infoCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  infoTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  infoText: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 22,
    marginBottom: 8,
  },
  bottomNav: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    backgroundColor: '#ffffff',
    borderTopWidth: 1,
    borderTopColor: '#e2e8f0',
    paddingVertical: 12,
  },
  navItem: {
    alignItems: 'center',
    flex: 1,
  },
  activeNavItem: {
    paddingTop: 4,
    borderTopWidth: 2,
    borderTopColor: '#3b82f6',
  },
  navIcon: {
    fontSize: 20,
    color: '#94a3b8',
    marginBottom: 4,
  },
  activeNavIcon: {
    color: '#3b82f6',
  },
  navText: {
    fontSize: 12,
    color: '#94a3b8',
  },
  activeNavText: {
    color: '#3b82f6',
  },
});

export default FrozenLeftTableApp;

请添加图片描述


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:

请添加图片描述
本文探讨了移动端多列数据展示中左侧冻结列的优化实现与跨端迁移方案。针对React Native开发的冻结列表格组件,文章详细拆解了其核心技术:1)通过类型体系构建三层强类型约束;2)采用"布局分层+绝对定位+空间补偿"实现冻结列布局;3)预留滚动同步机制入口;4)保持排序交互一致性;5)运用FlatList优化列表渲染性能。在跨端迁移方面,重点分析了如何将React Native组件转换为鸿蒙ArkTS组件,强调冻结列布局的等价转换、滚动同步机制适配和渲染性能对齐三大核心维度。文章为移动端多列数据展示及跨端开发提供了可落地的技术参考方案。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐