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


在移动应用开发领域,跨端技术已成为主流趋势,尤其随着鸿蒙系统的兴起,如何实现一套代码覆盖多平台(iOS、Android、鸿蒙)成为开发者关注的焦点。React Native 作为成熟的跨端框架,其组件化设计理念天然支持多平台适配。本次解析的 MusicDailyRecommendations 组件,展示了如何构建一个功能完整、交互流畅的音乐每日推荐页面,并实现鸿蒙系统的无缝适配。

组件采用了 React Native 的经典布局结构,从外到内依次为:

  • SafeAreaView:作为根容器,确保内容在不同设备的安全区域内显示,自动适配刘海屏、状态栏和底部导航栏。在鸿蒙系统中,React Native 会调用系统 API 获取安全区域信息,确保内容不被遮挡,这一机制与原生应用保持一致。

  • ScrollView:主体内容滚动容器,处理长列表和复杂布局的滚动显示。在鸿蒙系统中,ScrollView 会映射为 ArkUI 的 scroll-view 组件,保持原生的滚动体验,包括惯性滚动和回弹效果,确保跨平台视觉和交互一致性。

  • TouchableOpacity:实现可点击区域,提供触摸反馈效果。在鸿蒙系统中,TouchableOpacity 会转换为具有点击效果的 ArkUI 组件,通过 stateStyles 实现按压状态的样式变化,保持原生的触摸反馈。

条件渲染

组件包含一个条件渲染的模态框,用于显示歌曲的详细信息:

{detailVisible && (
  <View style={styles.detailOverlay}>
    {/* 模态框内容 */}
  </View>
)}

模态框采用绝对定位(position: 'absolute')覆盖整个屏幕,背景使用半透明黑色(rgba(0,0,0,0.25))营造层次感。在鸿蒙系统中,绝对定位会转换为 ArkUI 的 position: 'fixed',保持相同的视觉效果。模态框的显示/隐藏通过 detailVisible 状态控制,这是 React 基于状态的条件渲染核心特性,在鸿蒙系统上同样高效运行。


Hooks 状态管理

组件使用 useState Hook 管理两个核心状态:

const [detailVisible, setDetailVisible] = useState(false);
const [detailTitle, setDetailTitle] = useState<string | null>(null);

在鸿蒙系统中,React Native 的 Hook 机制会被转换为对应的 ArkUI 状态管理机制。例如,useState 会映射为 ArkUI 的 @State 装饰器,实现状态的响应式更新,当状态变化时,相关组件会自动重新渲染。这种映射机制确保了开发者可以使用熟悉的 React Hooks API,同时获得原生的性能表现。

交互函数

组件定义了多个交互函数,处理用户的各种操作:

  • onPlay:点击播放按钮时调用,使用 Alert.alert 显示播放信息
  • onFav:点击收藏按钮时调用,显示收藏结果
  • onMore:点击详情按钮时调用,更新状态显示模态框
  • onCloseDetail:关闭详情模态框时调用,重置状态

Alert.alert 是 React Native 提供的跨平台 API,会根据运行平台自动选择合适的弹窗样式。在鸿蒙系统中,React Native 会调用系统原生的 AlertDialog API,确保弹窗样式与系统一致,提供原生的用户体验。这种跨平台 API 的封装,大大简化了跨端开发的复杂度,开发者无需为不同平台编写不同的弹窗逻辑。


StyleSheet

组件使用 StyleSheet.create 方法定义样式,将所有样式集中管理:

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f7fbff' },
  // 其他样式定义
});

StyleSheet 的优势在于:

  1. 性能优化:StyleSheet 在编译时会被处理,生成唯一的样式 ID,减少运行时的样式计算,提高渲染性能。在鸿蒙系统中,这种优化同样有效,能够减少 ArkUI 组件的样式计算开销。

  2. 类型安全:TypeScript 会检查样式属性,减少运行时错误。对于跨端开发而言,类型安全尤为重要,能够提前发现潜在的样式兼容性问题。

  3. 模块化:便于样式复用和主题管理,适合大型应用的样式维护。在跨端开发中,统一的样式管理能够确保应用在不同平台上具有一致的视觉风格。

样式属性

React Native 的样式属性会被转换为对应平台的样式规则。例如:

  • flexDirection: 'row' 转换为 CSS/ArkUI 的 flex-direction: row
  • justifyContent: 'space-between' 转换为 justify-content: space-between
  • borderRadius: 12 转换为 border-radius: 12px

在鸿蒙系统中,React Native 的样式会被转换为 ArkUI 的样式规则,确保跨平台视觉一致性。例如,组件中使用的阴影效果:

shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2

在鸿蒙系统中,这些阴影属性会被转换为 ArkUI 的 shadow 样式,或者通过 box-shadow 实现类似效果,保持相同的视觉层次感。


Base64 图标

组件使用 Base64 编码的图标,通过 ICONS_BASE64 对象集中管理:

const ICONS_BASE64 = {
  daily: 'data:image/png;base64,...',
  play: 'data:image/png;base64,...',
  heart: 'data:image/png;base64,...',
};

这种方式在跨端开发中具有明显优势:

  1. 减少网络请求:Base64 图标直接嵌入代码,无需额外的网络请求,提高加载速度。

  2. 避免资源适配问题:无需为不同平台(iOS、Android、鸿蒙)准备不同格式的图标资源,简化了资源管理流程。

  3. 统一打包:图标资源与代码一起打包,无需处理不同平台的资源目录结构,适合跨端开发。

在鸿蒙系统中,React Native 会将 Base64 图标转换为系统支持的图片格式,确保正常显示。这种方式尤其适合小图标,能够显著提高应用的加载速度和性能。


组件还使用了表情符号作为辅助图标,例如 ☀️ 表示每日推荐。表情符号是 Unicode 标准的一部分,在不同平台上都能保持一致的显示效果,无需额外的资源适配。这种方式简单高效,能够减少图标资源的使用,同时保持良好的跨端兼容性。


组件映射

React Native 组件到鸿蒙 ArkUI 组件的映射是跨端适配的核心机制。以下是主要组件的映射关系:

React Native 组件 鸿蒙 ArkUI 组件 说明
SafeAreaView Stack 安全区域容器
View Div 基础容器组件
Text Text 文本组件
ScrollView ScrollView 滚动容器
TouchableOpacity Button 可点击组件
Image Image 图片组件
Alert AlertDialog 弹窗组件

这种映射机制确保了 React Native 组件在鸿蒙系统上的原生表现,同时保持了开发体验的一致性。开发者可以使用熟悉的 React Native 组件,无需学习全新的 ArkUI 组件库,降低了跨端开发的学习成本。

平台特定代码处理

在跨端开发中,不可避免地会遇到平台特定的功能需求。React Native 提供了 Platform API 用于检测当前运行平台:

import { Platform } from 'react-native';

if (Platform.OS === 'harmony') {
  // 鸿蒙平台特定代码
} else if (Platform.OS === 'ios') {
  // iOS平台特定代码
} else if (Platform.OS === 'android') {
  // Android平台特定代码
}

在本次解析的组件中,没有使用平台特定代码,确保了良好的跨端兼容性。在实际开发中,应尽量减少平台特定代码,提高代码的可移植性。


在鸿蒙系统上开发 React Native 应用时,需要关注应用的性能表现。以下是一些性能优化建议:

  1. 合理使用 FlatList:对于长列表数据,优先使用 FlatList 组件,它实现了虚拟列表功能,能够高效渲染大量数据。

  2. 组件缓存:使用 React.memo 优化组件渲染,减少不必要的重渲染。

  3. 状态管理优化:避免在渲染函数中创建新对象或函数,减少组件重渲染次数。

  4. 样式优化:使用 StyleSheet.create 定义样式,避免内联样式,提高渲染性能。

  5. 图片优化:使用合适的图片格式和尺寸,避免大图加载导致的性能问题。


React Native 鸿蒙跨端开发为开发者提供了一种高效的解决方案,能够使用一套代码构建出在多平台上表现一致的高质量应用。本次解析的音乐每日推荐组件,展示了如何利用 React Native 的组件化设计、状态管理和样式系统,构建一个功能完整、交互流畅的页面,并实现鸿蒙系统的无缝适配。


音乐类应用的每日推荐页面是提升用户粘性的核心场景,其设计既要保证个性化推荐内容的清晰呈现,又要兼顾轻量化的播放、收藏交互与沉浸式的详情弹窗体验。本文以 React Native 实现的音乐每日推荐页为例,拆解其核心技术逻辑——包括状态管理、卡片式布局、弹窗交互设计等,并深度剖析向鸿蒙(HarmonyOS)ArkTS 跨端迁移的技术路径、适配要点与最佳实践,为音乐类应用的跨端开发提供可落地的参考范式。

1. 状态管理

音乐每日推荐页的核心交互围绕“歌曲播放-收藏-详情弹窗”展开,该实现采用 React 基础的 useState 钩子完成状态管理,既满足业务需求又避免了重型状态库的引入,是轻量级音乐页面的最优解:

(1)核心状态
  • detailVisible:布尔型状态作为推荐详情弹窗的显隐开关,是移动端“基础内容浏览-深度信息查看”交互模式的核心控制变量。该状态仅在用户触发“更多”操作时渲染弹窗,避免初始加载时的 DOM 冗余,同时符合用户“先试听再查看详情”的操作习惯;
  • detailTitle:字符串型状态动态绑定弹窗标题,实现不同详情场景与弹窗内容的精准关联,保证用户查看推荐详情时的上下文一致性,避免“无标题弹窗”导致的用户认知混乱。
(2)交互逻辑
  • onPlay/onFav 方法分别封装歌曲播放、收藏的核心业务逻辑,通过 Alert 弹窗提供即时操作反馈,预留了后续对接原生音频播放 API、用户收藏列表的扩展空间,符合音乐类应用“操作即时反馈”的交互原则;
  • onMore/onCloseDetail 方法形成“打开详情-关闭弹窗”的完整交互链路,通过 setDetailTitle(null) 置空状态,避免内存泄漏与状态残留,符合 React 函数式组件的状态管理最佳实践;
  • 所有交互方法均为纯函数风格,参数化传递歌曲名称,保证逻辑的复用性与可维护性,例如 onPlay('晴空下的旋律') 可适配任意歌曲的播放触发。

这种轻量化的状态管理模式,将业务逻辑与状态更新解耦,为跨端迁移保留了无框架依赖的纯逻辑层:

const onPlay = (song: string) => Alert.alert('每日推荐', `播放:${song}`);
const onFav = (song: string) => Alert.alert('收藏', `已收藏:${song}`);
const onMore = (title: string) => {
  setDetailTitle(title);
  setDetailVisible(true);
};
const onCloseDetail = () => {
  setDetailVisible(false);
  setDetailTitle(null);
};

2. 布局系统

每日推荐页的核心设计诉求是“内容分层展示、操作按钮视觉区分、沉浸式弹窗体验”,其布局与样式系统体现了 React Native 音乐类页面的设计最佳实践:

(1)头部与内容区
  • header 采用横向 Flex 布局实现“页面标题-图标+emoji”的经典移动端头部结构,通过 justifyContent: 'space-between' 实现左右内容的视觉平衡,浅灰色边框(#e2e8f0)区分头部与内容区,符合音乐类应用“简洁、轻量化”的视觉调性;
  • content 区域包裹在 ScrollView 中,支持长列表滚动,同时通过 padding: 16 保证内容与屏幕边缘的间距,适配不同尺寸设备。
(2)推荐内容

section 容器采用卡片式设计,内部嵌套 card 组件实现单首推荐歌曲的展示:

  • card 组件通过白色背景+浅灰色边框(#e2e8f0)+ 圆角(12px)模拟移动端卡片质感,marginBottom: 12 控制卡片间距,保证推荐列表的呼吸感;
  • 每首歌曲的操作区(actionsRow)采用横向 Flex 布局,actionBtn/actionBtnPrimary 通过差异化的背景色(#f1f5f9/#dbeafe)区分“播放”与“收藏”操作的优先级,收藏按钮采用品牌蓝系(#2563eb 文字色)突出核心操作,符合移动端交互设计的“视觉权重”原则;
  • 操作按钮内部通过“图标+文字”的横向布局,Base64 编码的图标(16px)作为视觉锚点,快速传递操作意图,12px 的文字保证可读性。
(3)推荐说明

sectionAlt 采用差异化的浅蓝背景(#eff6ff)与内容区卡片形成视觉区分,既保持页面风格统一性,又实现“核心推荐内容-辅助说明内容”的层级划分,12px 的浅灰色文字(#475569)保证说明文字的可读性,同时不抢夺核心内容的视觉焦点。

(4)沉浸式弹窗

detailOverlay 采用绝对定位+半透明遮罩(rgba(0,0,0,0.25))实现沉浸式的详情弹窗效果:

  • detailPanel 通过 maxWidth: 420 限制弹窗宽度,适配手机、平板等不同尺寸设备,14px 的圆角+阴影提升弹窗的视觉层次感;
  • 弹窗内部分为“头部(标题+关闭按钮)-内容区(推荐详情)-操作区(收藏合集+随机播放)”三层结构,符合移动端弹窗的交互规范;
  • 操作按钮延续“播放/收藏”的视觉风格,通过差异化的背景色区分操作优先级,与推荐卡片的操作按钮保持视觉一致性,降低用户认知成本。

3. 资源管理

页面采用“基础组件 + 轻量状态”的架构设计,贴合 React Native 性能优化原则:

  • 资源内联优化:所有图标采用 Base64 编码内联,避免网络请求加载图标带来的延迟,同时适配不同设备的分辨率,保证图标显示清晰度,尤其适合音乐类应用“离线可用”的业务诉求;
  • 条件渲染优化:弹窗的显隐完全由 detailVisible 状态控制,未显示时不渲染 DOM 节点,减少页面初始渲染开销,提升加载性能;
  • 组件复用优化:推荐歌曲卡片的布局逻辑完全复用,仅需传递不同的歌曲名称即可渲染不同内容,避免重复代码编写;
  • Flex 布局优化:通过 flex: 1justifyContentalignItems 等属性实现自适应布局,避免固定宽度/高度导致的内容溢出问题,适配不同屏幕尺寸的设备。

1. 技术映射

音乐每日推荐页的跨端适配核心在于“状态逻辑复用最大化,UI 层改动最小化”,React Native 与鸿蒙 ArkTS 的核心能力映射如下:

React Native 核心能力 鸿蒙 ArkTS 对应实现 适配要点
函数式组件 + useState @Component + @State/@Link 状态逻辑完全复用,useState 替换为 @State 装饰器,状态更新逻辑不变
JSX 声明式 UI TSX 声明式 UI 语法几乎完全兼容,ViewColumn/RowTextTextImageImageTouchableOpacityTextButton
StyleSheet 样式系统 @Styles/@Extend 样式 Flex 布局属性完全复用,绝对定位改为 Position.FIXED + Stack 布局,阴影属性调整为 shadow 统一配置,颜色/间距等样式常量可直接复用
模态弹窗(条件渲染) if/else 条件渲染 + Stack 布局 保留 detailVisible 状态控制显隐,遮罩层改为 Stack 组件实现层级覆盖,弹窗内容结构完全复用
Alert 弹窗 promptAction 弹窗 封装统一的弹窗工具函数,屏蔽平台 API 差异,音乐业务提示文案完全复用
Base64 图片 Base64 图片直接复用 鸿蒙 Image 组件原生支持 Base64 编码,无需额外处理

2. 核心模块

以推荐歌曲卡片和详情弹窗模块为例,展示 React Native 代码迁移到鸿蒙 ArkTS 的核心改动:

(1)推荐歌曲

React Native 原代码

<View style={styles.card}>
  <Text style={styles.song}>晴空下的旋律 · 艺人D</Text>
  <View style={styles.actionsRow}>
    <TouchableOpacity style={styles.actionBtn} onPress={() => onPlay('晴空下的旋律')}>
      <Image source={{ uri: ICONS_BASE64.play }} style={styles.actionIcon} />
      <Text style={styles.actionText}>播放</Text>
    </TouchableOpacity>
    <TouchableOpacity style={[styles.actionBtn, styles.actionBtnPrimary]} onPress={() => onFav('晴空下的旋律')}>
      <Image source={{ uri: ICONS_BASE64.heart }} style={styles.actionIcon} />
      <Text style={styles.actionTextPrimary}>收藏</Text>
    </TouchableOpacity>
  </View>
</View>

鸿蒙 ArkTS 迁移后代码

// 推荐歌曲卡片渲染函数
@Builder
renderSongCard(songName: string, artist: string) {
  Column() {
    Text(`${songName} · ${artist}`)
      .fontSize(13)
      .color('#0f172a')
      .fontWeight(FontWeight.SemiBold);
    
    Row({ space: 8 }) {
      TextButton({
        onClick: () => this.onPlay(songName)
      }) {
        Row() {
          Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=')
            .width(16)
            .height(16)
            .marginRight(6);
          Text('播放')
            .fontSize(12)
            .color('#334155')
            .fontWeight(FontWeight.Medium);
        }
      }
      .backgroundColor('#f1f5f9')
      .borderRadius(10)
      .padding({ top: 8, bottom: 8, left: 12, right: 12 });
      
      TextButton({
        onClick: () => this.onFav(songName)
      }) {
        Row() {
          Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=')
            .width(16)
            .height(16)
            .marginRight(6);
          Text('收藏')
            .fontSize(12)
            .color('#2563eb')
            .fontWeight(FontWeight.SemiBold);
        }
      }
      .backgroundColor('#dbeafe')
      .borderRadius(10)
      .padding({ top: 8, bottom: 8, left: 12, right: 12 });
    }
    .width('100%')
    .marginTop(8);
  }
  .width('100%')
  .backgroundColor('#ffffff')
  .borderRadius(12)
  .padding(12)
  .marginBottom(12)
  .border({ width: 1, color: '#e2e8f0' });
}

// 调用示例
this.renderSongCard('晴空下的旋律', '艺人D');
this.renderSongCard('轻风与树影', '艺人E');
(2)详情弹窗

React Native 原代码

<View style={styles.detailOverlay}>
  <View style={styles.detailPanel}>
    <View style={styles.detailHeader}>
      <Text style={styles.detailTitle}>{detailTitle}</Text>
      <TouchableOpacity onPress={onCloseDetail}>
        <Text style={styles.detailClose}>关闭</Text>
      </TouchableOpacity>
    </View>
    {/* 弹窗内容与操作区 */}
  </View>
</View>

鸿蒙 ArkTS 迁移后代码

// 详情弹窗渲染函数
@Builder
renderDetailPanel() {
  Stack() {
    // 遮罩层
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(0,0,0,0.25)');
    
    // 弹窗内容
    Column() {
      // 弹窗头部
      Row() {
        Text(this.detailTitle || '')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .color('#0f172a');
        
        TextButton({
          onClick: () => this.onCloseDetail()
        }) {
          Text('关闭')
            .fontSize(12)
            .color('#2563eb')
            .backgroundColor('#dbeafe')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(10);
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(Alignment.Center);
      
      // 弹窗内容区
      Column() {
        Row() {
          Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=')
            .width(18)
            .height(18)
            .marginRight(6);
          Text('来源:基于偏好与历史的个性化推荐。')
            .fontSize(12)
            .color('#475569');
        }
        .width('100%')
        .marginTop(8)
        .alignItems(Alignment.Center);
        
        Row() {
          Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=')
            .width(18)
            .height(18)
            .marginRight(6);
          Text('操作:收藏加入清单或立即播放。')
            .fontSize(12)
            .color('#475569');
        }
        .width('100%')
        .marginTop(8)
        .alignItems(Alignment.Center);
      }
      .width('100%')
      .marginTop(10);
      
      // 弹窗操作区
      Row() {
        TextButton({
          onClick: () => promptAction.showAlert({ title: '收藏合集', message: '已收藏今日推荐合集' })
        }) {
          Text('收藏合集')
            .fontSize(12)
            .color('#334155')
            .fontWeight(FontWeight.SemiBold);
        }
        .backgroundColor('#f1f5f9')
        .borderRadius(10)
        .padding({ top: 8, bottom: 8, left: 12, right: 12 })
        .marginRight(8);
        
        TextButton({
          onClick: () => promptAction.showAlert({ title: '随机播放', message: '已加入播放队列' })
        }) {
          Text('随机播放')
            .fontSize(12)
            .color('#2563eb')
            .fontWeight(FontWeight.Bold);
        }
        .backgroundColor('#dbeafe')
        .borderRadius(10)
        .padding({ top: 8, bottom: 8, left: 12, right: 12 });
      }
      .width('100%')
      .marginTop(12)
      .justifyContent(FlexAlign.FlexEnd);
    }
    .width('100%')
    .maxWidth(420)
    .backgroundColor('#ffffff')
    .borderRadius(14)
    .padding(14)
    .shadow({ radius: 4, color: '#000', opacity: 0.12, offsetX: 0, offsetY: 2 });
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(Alignment.Center)
  .padding(16)
  .position(Position.Fixed);
}

3. 鸿蒙代码

以下是音乐每日推荐页的完整鸿蒙迁移代码,展示端到端的迁移思路:

import { promptAction } from '@kit.ArkUI';

// Base64 图标常量(直接复用 React Native 定义)
const ICONS_BASE64 = {
  daily: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
  heart: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
};

@Entry
@Component
struct MusicDailyRecommendations {
  // 状态定义(对应 React Native 的 useState)
  @State detailVisible: boolean = false;
  @State detailTitle: string | null = null;

  // 业务逻辑完全复用,仅调整函数定义方式
  onPlay(song: string) {
    promptAction.showAlert({
      title: '每日推荐',
      message: `播放:${song}`
    });
  }

  onFav(song: string) {
    promptAction.showAlert({
      title: '收藏',
      message: `已收藏:${song}`
    });
  }

  onMore(title: string) {
    this.detailTitle = title;
    this.detailVisible = true;
  }

  onCloseDetail() {
    this.detailVisible = false;
    this.detailTitle = null;
  }

  build() {
    SafeArea() {
      Column() {
        // 头部区域
        Row() {
          Text('音乐播放器 · 每日推荐')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .color('#0f172a');
          
          Row() {
            Image(ICONS_BASE64.daily)
              .width(24)
              .height(24);
            Text('☀️')
              .fontSize(18)
              .marginLeft(8);
          }
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#ffffff')
        .borderBottom({ width: 1, color: '#e2e8f0' })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(Alignment.Center);

        // 核心内容区
        Scroll() {
          Column() {
            // 今日精选区
            this.renderRecommendSection();
            
            // 推荐说明区
            this.renderDescSection();
          }
          .width('100%')
          .padding(16);
        }
        .flexGrow(1);

        // 详情弹窗(条件渲染)
        if (this.detailVisible) {
          this.renderDetailPanel();
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#f7fbff');
    }
  }

  // 今日精选区渲染函数
  @Builder
  renderRecommendSection() {
    Column() {
      Text('今日精选')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .color('#0f172a')
        .marginBottom(10);
      
      this.renderSongCard('晴空下的旋律', '艺人D');
      this.renderSongCard('轻风与树影', '艺人E');
    }
    .width('100%')
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .padding(14)
    .shadow({ radius: 2, color: '#000', opacity: 0.08, offsetX: 0, offsetY: 1 });
  }

  // 推荐歌曲卡片渲染函数
  @Builder
  renderSongCard(songName: string, artist: string) {
    // 实现同前文示例
  }

  // 推荐说明区渲染函数
  @Builder
  renderDescSection() {
    Column() {
      Text('推荐说明')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .color('#0f172a')
        .marginBottom(10);
      
      Text('根据你的播放历史与收藏偏好,每日为你生成 10 首适合曲目。')
        .fontSize(12)
        .color('#475569');
    }
    .width('100%')
    .backgroundColor('#eff6ff')
    .borderRadius(12)
    .padding(14)
    .marginTop(16);
  }

  // 详情弹窗渲染函数
  @Builder
  renderDetailPanel() {
    // 实现同前文示例
  }
}

1. 业务逻辑

音乐每日推荐页的核心价值在于推荐内容展示与交互逻辑(播放、收藏、查看详情),应将纯 TypeScript 编写的交互方法封装为独立工具函数,脱离框架依赖,实现跨端 100% 复用:

// 独立的业务逻辑文件 musicLogic.ts
export const handlePlaySong = (song: string) => {
  return `播放:${song}`;
};

export const handleFavoriteSong = (song: string) => {
  return `已收藏:${song}`;
};

export const handleShowDetail = (title: string, setDetailTitle: (title: string | null) => void, setDetailVisible: (visible: boolean) => void) => {
  setDetailTitle(title);
  setDetailVisible(true);
};

export const handleCloseDetail = (setDetailTitle: (title: string | null) => void, setDetailVisible: (visible: boolean) => void) => {
  setDetailVisible(false);
  setDetailTitle(null);
};

export const handleCollectCollection = () => {
  return '已收藏今日推荐合集';
};

export const handleRandomPlay = () => {
  return '已加入播放队列';
};

React Native 中调用:

const onPlay = (song: string) => Alert.alert('每日推荐', handlePlaySong(song));

鸿蒙中调用:

onPlay(song: string) {
  promptAction.showAlert({
    title: '每日推荐',
    message: handlePlaySong(song)
  });
}

2. 样式常量

将页面的核心样式常量(品牌色、圆角、间距、字号)抽离为独立文件,实现跨端样式风格的一致性,尤其适合音乐类页面“简洁、轻量化”的视觉调性:

// styles/musicConstants.ts
export const MUSIC_COLORS = {
  background: '#f7fbff',      // 页面背景色
  cardBg: '#ffffff',          // 卡片背景色
  descBg: '#eff6ff',          // 说明区背景色
  border: '#e2e8f0',          // 边框色
  primary: '#2563eb',         // 主色(品牌蓝)
  primaryLight: '#dbeafe',    // 主色浅背景
  textPrimary: '#0f172a',     // 主要文本色
  textSecondary: '#475569',   // 次要文本色
  textTertiary: '#334155',    // 按钮文本色
  btnNormalBg: '#f1f5f9',     // 普通按钮背景
};

export const MUSIC_SIZES = {
  borderRadiusXL: 14,         // 弹窗圆角
  borderRadiusL: 12,          // 卡片圆角
  borderRadiusM: 10,          // 按钮圆角
  paddingBase: 16,            // 基础内边距
  paddingCard: 14,            // 卡片内边距
  paddingBtnM: 8,             // 中按钮内边距
  paddingBtnS: 4,             // 小按钮内边距
  iconSizeXS: 16,             // 按钮图标尺寸
  iconSizeS: 18,              // 弹窗图标尺寸
  iconSizeL: 24,              // 头部图标尺寸
  fontSizeTitle: 18,          // 页面标题字号
  fontSizeSection: 16,        // 区块标题字号
  fontSizeItem: 13,           // 歌曲名称字号
  fontSizeSub: 12,            // 按钮/说明文本字号
};

3. 原生能力

音乐类应用常需调用音频播放、本地收藏、推荐数据同步等原生能力,封装统一的适配层可大幅降低跨端适配成本:

// utils/musicAdapter.ts
// 弹窗适配
export const showMusicAlert = (title: string, message: string) => {
  // React Native 环境
  if (typeof Alert !== 'undefined') {
    Alert.alert(title, message);
  } 
  // 鸿蒙环境
  else if (typeof promptAction !== 'undefined') {
    promptAction.showAlert({ title, message });
  }
};

// 音频播放适配
export const playAudio = async (audioUrl: string) => {
  if (typeof Audio !== 'undefined') {
    // React Native 音频播放逻辑
    const sound = new Audio(audioUrl);
    await sound.play();
  } else if (typeof audioPlayer !== 'undefined') {
    // 鸿蒙音频播放逻辑
    audioPlayer.play(audioUrl);
  }
};

// 收藏歌曲适配
export const favoriteSong = async (songId: string, userId: string) => {
  if (typeof AsyncStorage !== 'undefined') {
    // React Native 本地存储
    const favorites = await AsyncStorage.getItem(`favorites_${userId}`) || '[]';
    const favoritesList = JSON.parse(favorites);
    if (!favoritesList.includes(songId)) {
      favoritesList.push(songId);
      await AsyncStorage.setItem(`favorites_${userId}`, JSON.stringify(favoritesList));
    }
  } else if (typeof storage !== 'undefined') {
    // 鸿蒙本地存储
    const favorites = await storage.get(`favorites_${userId}`) || '[]';
    const favoritesList = JSON.parse(favorites);
    if (!favoritesList.includes(songId)) {
      favoritesList.push(songId);
      await storage.set(`favorites_${userId}`, JSON.stringify(favoritesList));
    }
  }
};
  1. React Native 端的核心价值在于极简的状态管理、卡片式的推荐内容布局、沉浸式的弹窗交互设计,为音乐每日推荐页提供了“内容清晰、操作便捷、体验流畅”的核心体验,同时为跨端迁移奠定了良好基础;
  2. 鸿蒙端的适配核心是状态逻辑无改动复用、Flex 布局直接迁移、Base64 图标原生兼容,核心的音乐推荐展示与交互逻辑可 100% 复用,仅需调整组件语法与样式属性,适配成本极低;
  3. 音乐类页面跨端开发的关键是“业务逻辑抽离复用、样式常量统一管理、原生能力适配层封装”,实现高复用率的同时,保证音乐类应用“轻量化、沉浸式、交互一致”的核心体验在不同平台的落地。

音乐每日推荐页的跨端迁移实践表明,React Native 开发的音乐类页面向鸿蒙迁移时,80% 以上的核心代码可直接复用,仅需 20% 左右的 UI 层适配工作。这种高复用率的迁移模式,不仅大幅提升了跨端开发效率,更重要的是保证了音乐类应用“个性化推荐、轻量化交互、沉浸式体验”的核心诉求在不同平台的一致性。


在现代音乐流媒体应用中,个性化推荐功能已经从简单的热门榜单演变为基于用户行为分析、机器学习算法的智能推荐系统。MusicDailyRecommendations组件展示了如何在移动端实现一套完整的每日推荐功能,从用户偏好分析、推荐算法到界面交互,形成了一个完整的个性化音乐发现体验。

从技术架构的角度来看,这个组件不仅是一个界面展示,更是推荐系统前端集成的典型案例。它需要协调用户行为数据的收集、推荐算法的调用、个性化内容的展示等多个技术维度。当我们将这套架构迁移到鸿蒙平台时,需要深入理解其推荐逻辑和数据流转机制,才能确保跨端实现的完整性和可靠性。

推荐数据

interface Recommendation {
  id: string;
  title: string;
  artist: string;
  reason: string;
  score: number; // 推荐分数 0-1
  features: string[]; // 音乐特征标签
  playbackCount: number;
  favoriteCount: number;
}

interface UserPreference {
  favoriteGenres: string[];
  favoriteArtists: string[];
  listeningHistory: ListeningRecord[];
  dislikeArtists: string[];
  favoriteFeatures: string[];
}

interface ListeningRecord {
  songId: string;
  playCount: number;
  lastPlayed: Date;
  completionRate: number; // 完成率 0-1
}

虽然当前实现使用硬编码数据,但从代码结构可以推断出完整的推荐数据模型:

// 完整的推荐系统数据结构
interface DailyRecommendation {
  date: string;
  recommendations: RecommendedSong[];
  generationReason: string;
  userPreferenceSnapshot: UserPreference;
}

interface RecommendedSong {
  id: string;
  title: string;
  artists: ArtistInfo[];
  album?: AlbumInfo;
  duration: number;
  previewUrl: string;
  recommendationScore: number;
  recommendationReasons: string[];
  audioFeatures: {
    energy: number;
    valence: number;
    danceability: number;
    acousticness: number;
  };
}

推荐卡片

card: {
  backgroundColor: '#ffffff',
  borderRadius: 12,
  padding: 12,
  marginBottom: 12,
  borderWidth: 1,
  borderColor: '#e2e8f0'
}

推荐卡片采用了清晰的三层结构设计:

  1. 歌曲信息区域:歌名和艺人信息展示
  2. 推荐理由区域:个性化的推荐解释
  3. 操作按钮区域:播放和收藏等用户交互

在鸿蒙ArkUI中,这种卡片设计可以通过Column布局实现:

// 鸿蒙推荐卡片组件
@Component
struct RecommendationCard {
  @Prop recommendation: RecommendedSong;
  @State isPlaying: boolean = false;
  
  build() {
    Column() {
      // 歌曲信息
      Text(this.recommendation.title)
        .fontSize(14)
        .fontWeight(FontWeight.SemiBold)
        .fontColor('#0f172a')
      
      Text(this.recommendation.artists.map(a => a.name).join(', '))
        .fontSize(12)
        .fontColor('#64748b')
        .margin({ top: 2 })
      
      // 操作按钮区域
      Row() {
        Button('播放')
          .fontSize(12)
          .fontColor('#334155')
          .backgroundColor('#f1f5f9')
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .margin({ right: 8 })
          .onClick(() => this.playSong())
        
        Button('收藏')
          .fontSize(12)
          .fontColor('#2563eb')
          .backgroundColor('#dbeafe')
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .onClick(() => this.addToFavorites())
      }
      .margin({ top: 8 })
    }
    .padding(12)
    .backgroundColor(Color.White)
    .border({ width: 1, color: '#e2e8f0' })
    .borderRadius(12)
    .margin({ bottom: 12 })
  }
  
  private playSong(): void {
    // 播放逻辑实现
  }
  
  private addToFavorites(): void {
    // 收藏逻辑实现
  }
}

行为数据

// 用户行为跟踪服务
class UserBehaviorTracker {
  private static instance: UserBehaviorTracker;
  private behaviorData: UserBehavior[] = [];
  
  static getInstance(): UserBehaviorTracker {
    if (!UserBehaviorTracker.instance) {
      UserBehaviorTracker.instance = new UserBehaviorTracker();
    }
    return UserBehaviorTracker.instance;
  }
  
  trackPlay(songId: string, duration: number): void {
    this.behaviorData.push({
      type: 'play',
      songId,
      duration,
      timestamp: new Date()
    });
  }
  
  trackFavorite(songId: string): void {
    this.behaviorData.push({
      type: 'favorite',
      songId,
      timestamp: new Date()
    });
  }
  
  trackSkip(songId: string): void {
    this.behaviorData.push({
      type: 'skip',
      songId,
      timestamp: new Date()
    });
  }
  
  getBehaviorSummary(): BehaviorSummary {
    // 生成行为摘要用于推荐算法
    return {
      favoriteGenres: this.extractFavoriteGenres(),
      favoriteArtists: this.extractFavoriteArtists(),
      listeningPattern: this.analyzeListeningPattern()
    };
  }
}

推荐算法

// 推荐服务客户端
class RecommendationClient {
  private apiBaseUrl: string;
  private userId: string;
  
  constructor(apiBaseUrl: string, userId: string) {
    this.apiBaseUrl = apiBaseUrl;
    this.userId = userId;
  }
  
  async getDailyRecommendations(): Promise<DailyRecommendation> {
    const behaviorSummary = UserBehaviorTracker.getInstance().getBehaviorSummary();
    
    const response = await fetch(`${this.apiBaseUrl}/recommendations/daily`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        userId: this.userId,
        behaviorSummary
      })
    });
    
    if (!response.ok) {
      throw new Error('Failed to fetch recommendations');
    }
    
    return response.json();
  }
  
  async sendFeedback(
    recommendationId: string, 
    feedback: 'positive' | 'negative'
  ): Promise<void> {
    await fetch(`${this.apiBaseUrl}/recommendations/feedback`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        userId: this.userId,
        recommendationId,
        feedback
      })
    });
  }
}

推荐状态

const [detailVisible, setDetailVisible] = useState(false);
const [detailTitle, setDetailTitle] = useState<string | null>(null);

在真实的推荐系统中,状态管理需要覆盖更多推荐相关的维度:

// 推荐系统状态管理
interface RecommendationState {
  dailyRecommendations: RecommendedSong[];
  isLoading: boolean;
  lastUpdated: Date | null;
  error: string | null;
  currentPlaying: string | null;
  userFeedback: Map<string, 'positive' | 'negative'>;
}

// 用户偏好状态
interface PreferenceState {
  favoriteSongs: Set<string>;
  dislikedSongs: Set<string>;
  preferredGenres: Set<string>;
  preferredArtists: Set<string>;
}

在鸿蒙ArkUI中,这种复杂的状态管理可以通过@State和@Link装饰器实现:

// 鸿蒙推荐状态管理
@Observed
class RecommendationState {
  dailyRecommendations: RecommendedSong[] = [];
  isLoading: boolean = false;
  lastUpdated: Date | null = null;
  
  async refreshRecommendations(): Promise<void> {
    this.isLoading = true;
    try {
      const client = new RecommendationClient('https://api.example.com', 'user123');
      const recommendations = await client.getDailyRecommendations();
      this.dailyRecommendations = recommendations;
      this.lastUpdated = new Date();
    } catch (error) {
      console.error('Failed to refresh recommendations:', error);
    } finally {
      this.isLoading = false;
    }
  }
}

@Component
struct DailyRecommendationsPage {
  @State recommendationState: RecommendationState = new RecommendationState();
  
  aboutToAppear() {
    this.recommendationState.refreshRecommendations();
  }
  
  build() {
    Column() {
      if (this.recommendationState.isLoading) {
        LoadingIndicator()
      } else if (this.recommendationState.dailyRecommendations.length > 0) {
        RecommendationList({
          recommendations: this.recommendationState.dailyRecommendations
        })
      } else {
        EmptyState()
      }
    }
  }
}

推荐列表

// React Native虚拟化列表
<FlatList
  data={recommendations}
  renderItem={renderRecommendationItem}
  keyExtractor={item => item.id}
  initialNumToRender={5}
  maxToRenderPerBatch={10}
  windowSize={21}
/>

推荐列表需要考虑性能优化和用户体验:

  1. 虚拟化渲染:只渲染可见区域的推荐项
  2. 分页加载:分批加载推荐内容,减少初始加载时间
  3. 缓存策略:缓存已加载的推荐内容,提升二次访问速度

在鸿蒙ArkUI中,推荐列表可以通过LazyForEach实现:

// 鸿蒙懒加载推荐列表
@Component
struct RecommendationList {
  @Prop recommendations: RecommendedSong[];
  
  build() {
    List() {
      LazyForEach(this.recommendations, (recommendation: RecommendedSong) => {
        ListItem() {
          RecommendationCard({ recommendation: recommendation })
        }
      })
    }
    .cachedCount(10)
  }
}

详情弹窗

detailPanel: {
  width: '100%',
  maxWidth: 420,
  backgroundColor: '#ffffff',
  borderRadius: 14,
  padding: 14
}

详情弹窗提供了推荐系统的透明度,包含:

  1. 推荐理由:算法生成的具体推荐原因
  2. 用户反馈:收集用户对推荐质量的评价
  3. 相关操作:批量操作和个性化设置
container: { backgroundColor: '#f7fbff' }
sectionAlt: { backgroundColor: '#eff6ff' }
actionBtnPrimary: { backgroundColor: '#dbeafe' }
actionTextPrimary: { color: '#2563eb' }

组件采用了蓝色调为主的设计语言,这种色彩在推荐系统中具有专业和可信的暗示:

  1. 背景色(#f7fbff):极浅蓝色,营造清新推荐氛围
  2. 次要区域(#eff6ff):浅蓝色背景,区分功能区块
  3. 操作按钮(#dbeafe):蓝色背景,强调主要操作
  4. 强调色(#2563eb):蓝色文字,传达专业可信

在跨端开发中,这种色彩系统需要建立统一的资源映射:

// 统一的推荐系统色彩系统
const RecommendationColors = {
  // 背景色
  background: '#f7fbff',      // 主背景 - 极浅蓝
  surfacePrimary: '#ffffff',   // 卡片背景
  surfaceSecondary: '#eff6ff', // 次要背景
  
  // 操作色
  primary: '#2563eb',         // 主色 - 蓝色
  primaryLight: '#dbeafe',    // 主色浅 - 按钮背景
  primaryDark: '#1d4ed8',     // 主色深 - 交互状态
  
  // 功能色
  positive: '#10b981',        // 正面反馈
  negative: '#ef4444',        // 负面反馈
  neutral: '#64748b'          // 中性信息
};
sectionTitle: { fontSize: 16, fontWeight: 'bold' }
song: { fontSize: 13, fontWeight: '600' }
actionText: { fontSize: 12, fontWeight: '500' }

组件的字体系统呈现出清晰的层次结构:

  • 区块标题(16px, bold):最高层级,引导用户注意力
  • 歌曲信息(13px, semibold):主要内容层级,视觉焦点
  • 操作文本(12px, medium):辅助操作层级,清晰可读

推荐算法

推荐逻辑需要在两个平台上保持一致的行为:

// 推荐算法抽象
interface RecommendationAlgorithm {
  getRecommendations(userId: string, context: RecommendationContext): Promise<Recommendation[]>;
  processFeedback(feedback: UserFeedback): Promise<void>;
  updateUserPreference(preference: UserPreference): Promise<void>;
}

// React Native实现
class RNRecommendationAlgorithm implements RecommendationAlgorithm {
  // 实现...
}

// 鸿蒙实现  
class HarmonyRecommendationAlgorithm implements RecommendationAlgorithm {
  // 实现...
}

// 工厂方法创建实例
const createRecommendationAlgorithm = (): RecommendationAlgorithm => {
  return Platform.OS === 'harmony' 
    ? new HarmonyRecommendationAlgorithm()
    : new RNRecommendationAlgorithm();
};

跨平台

// 用户行为跟踪抽象
interface BehaviorTracker {
  trackEvent(event: UserEvent): void;
  getBehaviorSummary(): BehaviorSummary;
  clearOldData(daysToKeep: number): void;
}

// 统一的用户事件定义
interface UserEvent {
  type: 'play' | 'pause' | 'favorite' | 'skip' | 'share';
  songId: string;
  timestamp: Date;
  metadata?: Record<string, any>;
}

推荐结果

// 推荐结果缓存管理器
class RecommendationCache {
  private static instance: RecommendationCache;
  private cache: Map<string, CachedRecommendation> = new Map();
  private maxCacheSize: number = 100;
  
  static getInstance(): RecommendationCache {
    if (!RecommendationCache.instance) {
      RecommendationCache.instance = new RecommendationCache();
    }
    return RecommendationCache.instance;
  }
  
  getRecommendations(userId: string): CachedRecommendation | null {
    const key = this.getCacheKey(userId);
    const cached = this.cache.get(key);
    
    if (cached && this.isCacheValid(cached)) {
      return cached;
    }
    
    return null;
  }
  
  cacheRecommendations(userId: string, recommendations: Recommendation[]): void {
    const key = this.getCacheKey(userId);
    const cached: CachedRecommendation = {
      recommendations,
      timestamp: new Date(),
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24小时有效期
    };
    
    this.cache.set(key, cached);
    this.cleanupOldEntries();
  }
  
  private cleanupOldEntries(): void {
    if (this.cache.size > this.maxCacheSize) {
      // 清理最旧的条目
      const oldestKey = this.findOldestEntry();
      if (oldestKey) {
        this.cache.delete(oldestKey);
      }
    }
  }
}

实时推荐

// 实时推荐引擎
class RealTimeRecommendationEngine {
  private userPreferences: Map<string, UserPreference> = new Map();
  private songFeatures: Map<string, AudioFeatures> = new Map();
  private similarityMatrix: Map<string, Map<string, number>> = new Map();
  
  getRealTimeRecommendations(
    currentSong: string, 
    userPreference: UserPreference
  ): Recommendation[] {
    const similarSongs = this.findSimilarSongs(currentSong);
    return this.rankRecommendations(similarSongs, userPreference);
  }
  
  private findSimilarSongs(songId: string): string[] {
    const similarities = this.similarityMatrix.get(songId);
    if (!similarities) return [];
    
    return Array.from(similarities.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([songId]) => songId);
  }
  
  private rankRecommendations(
    songIds: string[], 
    userPreference: UserPreference
  ): Recommendation[] {
    return songIds.map(songId => ({
      id: songId,
      score: this.calculateRecommendationScore(songId, userPreference),
      reasons: this.generateRecommendationReasons(songId, userPreference)
    }))
    .sort((a, b) => b.score - a.score);
  }
}

MusicDailyRecommendations组件展示了推荐系统前端集成的核心技术要点:

  1. 架构设计完善:前后端分离的推荐系统架构
  2. 数据处理专业:用户行为分析和偏好建模
  3. 界面交互流畅:清晰的卡片设计和操作流程
  4. 性能优化到位:缓存策略和实时推荐优化
  5. 跨端适配系统:统一的推荐算法和行为跟踪

随着推荐算法技术的不断发展和用户对个性化体验需求的提升,每日推荐功能将成为音乐应用的核心竞争力之一。其技术实现的质量和跨端一致性,将直接影响产品的用户粘性和市场表现。


真实演示案例代码:

import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Alert, Image } from 'react-native';

const ICONS_BASE64 = {
  daily: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
  heart: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
};

const MusicDailyRecommendations: React.FC = () => {
  const [detailVisible, setDetailVisible] = useState(false);
  const [detailTitle, setDetailTitle] = useState<string | null>(null);
  const onPlay = (song: string) => Alert.alert('每日推荐', `播放:${song}`);
  const onFav = (song: string) => Alert.alert('收藏', `已收藏:${song}`);
  const onMore = (title: string) => {
    setDetailTitle(title);
    setDetailVisible(true);
  };
  const onCloseDetail = () => {
    setDetailVisible(false);
    setDetailTitle(null);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>音乐播放器 · 每日推荐</Text>
        <View style={styles.headerIcons}>
          <Image source={{ uri: ICONS_BASE64.daily }} style={styles.headerIconImg} />
          <Text style={styles.headerEmoji}>☀️</Text>
        </View>
      </View>

      <ScrollView style={styles.content}>
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>今日精选</Text>
          <View style={styles.card}>
            <Text style={styles.song}>晴空下的旋律 · 艺人D</Text>
            <View style={styles.actionsRow}>
              <TouchableOpacity style={styles.actionBtn} onPress={() => onPlay('晴空下的旋律')}>
                <Image source={{ uri: ICONS_BASE64.play }} style={styles.actionIcon} />
                <Text style={styles.actionText}>播放</Text>
              </TouchableOpacity>
              <TouchableOpacity style={[styles.actionBtn, styles.actionBtnPrimary]} onPress={() => onFav('晴空下的旋律')}>
                <Image source={{ uri: ICONS_BASE64.heart }} style={styles.actionIcon} />
                <Text style={styles.actionTextPrimary}>收藏</Text>
              </TouchableOpacity>
            </View>
          </View>
          <View style={styles.card}>
            <Text style={styles.song}>轻风与树影 · 艺人E</Text>
            <View style={styles.actionsRow}>
              <TouchableOpacity style={styles.actionBtn} onPress={() => onPlay('轻风与树影')}>
                <Image source={{ uri: ICONS_BASE64.play }} style={styles.actionIcon} />
                <Text style={styles.actionText}>播放</Text>
              </TouchableOpacity>
              <TouchableOpacity style={[styles.actionBtn, styles.actionBtnPrimary]} onPress={() => onFav('轻风与树影')}>
                <Image source={{ uri: ICONS_BASE64.heart }} style={styles.actionIcon} />
                <Text style={styles.actionTextPrimary}>收藏</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>

        <View style={styles.sectionAlt}>
          <Text style={styles.sectionTitle}>推荐说明</Text>
          <Text style={styles.desc}>根据你的播放历史与收藏偏好,每日为你生成 10 首适合曲目。</Text>
        </View>
      </ScrollView>
      {detailVisible && (
        <View style={styles.detailOverlay}>
          <View style={styles.detailPanel}>
            <View style={styles.detailHeader}>
              <Text style={styles.detailTitle}>{detailTitle}</Text>
              <TouchableOpacity onPress={onCloseDetail}>
                <Text style={styles.detailClose}>关闭</Text>
              </TouchableOpacity>
            </View>
            <View style={styles.detailBody}>
              <View style={styles.detailRow}>
                <Image source={{ uri: ICONS_BASE64.daily }} style={styles.detailIcon} />
                <Text style={styles.detailText}>来源:基于偏好与历史的个性化推荐。</Text>
              </View>
              <View style={styles.detailRow}>
                <Image source={{ uri: ICONS_BASE64.heart }} style={styles.detailIcon} />
                <Text style={styles.detailText}>操作:收藏加入清单或立即播放。</Text>
              </View>
            </View>
            <View style={styles.detailFooter}>
              <TouchableOpacity style={styles.detailBtn} onPress={() => Alert.alert('收藏合集', '已收藏今日推荐合集')}>
                <Text style={styles.detailBtnText}>收藏合集</Text>
              </TouchableOpacity>
              <TouchableOpacity style={[styles.detailBtn, styles.detailBtnPrimary]} onPress={() => Alert.alert('随机播放', '已加入播放队列')}>
                <Text style={styles.detailBtnTextPrimary}>随机播放</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f7fbff' },
  header: { padding: 16, backgroundColor: '#ffffff', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
  title: { fontSize: 18, fontWeight: 'bold', color: '#0f172a' },
  headerIcons: { flexDirection: 'row', alignItems: 'center' },
  headerEmoji: { fontSize: 18, marginLeft: 8 },
  headerIconImg: { width: 24, height: 24 },
  content: { padding: 16 },
  section: { backgroundColor: '#ffffff', borderRadius: 12, padding: 14, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2 },
  sectionAlt: { backgroundColor: '#eff6ff', borderRadius: 12, padding: 14, marginTop: 16 },
  sectionTitle: { fontSize: 16, fontWeight: 'bold', color: '#0f172a', marginBottom: 10 },
  card: { backgroundColor: '#ffffff', borderRadius: 12, padding: 12, marginBottom: 12, borderWidth: 1, borderColor: '#e2e8f0' },
  song: { fontSize: 13, color: '#0f172a', fontWeight: '600' },
  actionsRow: { flexDirection: 'row', marginTop: 8 },
  actionBtn: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f1f5f9', borderRadius: 10, paddingVertical: 8, paddingHorizontal: 12, marginRight: 8 },
  actionBtnPrimary: { backgroundColor: '#dbeafe' },
  actionIcon: { width: 16, height: 16, marginRight: 6 },
  actionText: { fontSize: 12, color: '#334155', fontWeight: '500' },
  actionTextPrimary: { fontSize: 12, color: '#2563eb', fontWeight: '600' },
  desc: { fontSize: 12, color: '#475569' },
  detailOverlay: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.25)', justifyContent: 'center', alignItems: 'center', padding: 16 },
  detailPanel: { width: '100%', maxWidth: 420, backgroundColor: '#ffffff', borderRadius: 14, padding: 14, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.12, shadowRadius: 4 },
  detailHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
  detailTitle: { fontSize: 16, fontWeight: '700', color: '#0f172a' },
  detailClose: { fontSize: 12, color: '#2563eb', backgroundColor: '#dbeafe', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 },
  detailBody: { marginTop: 10 },
  detailRow: { flexDirection: 'row', alignItems: 'center', marginTop: 8 },
  detailIcon: { width: 18, height: 18, marginRight: 6 },
  detailText: { fontSize: 12, color: '#475569' },
  detailFooter: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 12 },
  detailBtn: { backgroundColor: '#f1f5f9', borderRadius: 10, paddingVertical: 8, paddingHorizontal: 12, marginRight: 8 },
  detailBtnPrimary: { backgroundColor: '#dbeafe' },
  detailBtnText: { fontSize: 12, color: '#334155', fontWeight: '600' },
  detailBtnTextPrimary: { fontSize: 12, color: '#2563eb', fontWeight: '700' },
});

export default MusicDailyRecommendations;

请添加图片描述


打包

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

在这里插入图片描述

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

在这里插入图片描述

最后运行效果图如下显示:
请添加图片描述

本文解析了使用React Native构建音乐每日推荐页面的跨端开发实践,重点分析了组件设计、状态管理和鸿蒙系统适配。该组件采用SafeAreaView、ScrollView等核心布局,利用Hooks管理状态,通过条件渲染实现模态框交互。文章详细探讨了样式系统优化、Base64图标应用以及React Native到鸿蒙ArkUI的组件映射机制,为音乐类应用跨平台开发提供了性能优化建议和技术实现方案,展示了如何实现一套代码覆盖iOS、Android和鸿蒙多平台的开发范式。

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐