uni-app x跨平台开发实战:开发鸿蒙HarmonyOS滚动卡片组件,scroll-view无法滚动踩坑全记录

在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!我不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?闲的都想看蚂蚁上树,无聊透顶,百无聊赖,感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的"爱影家"影视app项目,找个跨多端的技术栈再玩一把。本节实战是scroll-view无法滚动踩坑全记录。

本节实战是scroll-view无法滚动踩坑全记录。本以为scroll-view用法挺简单的,参考官方文档用呗。结果想实现个HarmonyOS滚动卡片组件,起初死活无法横向滚动。最终找到了原因和解决办法,特此总结分享下,有用到的小伙伴可以点击收藏。

该免费观影APP,使用uni-app x框架开发跨六端的免费观影app。

项目开源地址:https://gitcode.com/qq8864/uniappx_imovie

下图是实现的热门影视和即将上映等横向滚动卡片组件运行效果。

项目背景

本文基于爱影家(imovie)项目的 uni-app x框架重构实现,介绍在 uni-app x 中封装横向滚动卡片组件时遇到的核心问题及解决办法。(爱影家(imovie)项目我都做烂了,有uniapp版,鸿蒙原生版,还有这里的 uni-app x框架重构实现版本。)

本项目用到的后台影视和音乐接口文档:https://blog.csdn.net/qq8864/article/details/154404554

本项目首页包含两类横向滚动组件:

  • movie-section:通用影片横向列表,展示封面、片名、评分,复用于"正在热映"、"即将上映"等多个区块
  • box-office:院线票房日榜,每张卡片近全屏宽,可左右翻页浏览

开发过程中,box-office 组件一开始就能正常横向滑动,而后来封装的 movie-section 照猫画虎写完后,scroll-view 设置了 direction="horizontal",却死活无法横向滚动。对比两段代码的差异,揭示了 uni-app x 原生布局引擎与 Web CSS 之间一个非常关键的行为差异。

uni-app x 是 DCloud 推出的新一代跨平台开发框架,支持将代码编译为多个平台的原生代码:

  • Android 平台:编译为 Kotlin
  • iOS 平台:编译为 Swift
  • 鸿蒙 Next 平台:编译为 ArkTS
  • Web 和小程序平台:编译为 JS

核心问题:原生引擎不认 flex 溢出

在 Web 浏览器里,横向滚动的惯用写法是:

css 复制代码
.scroll-container {
  overflow-x: auto;
  white-space: nowrap;  /* 或 display:flex + flex-wrap:nowrap */
}

子元素内容超出容器宽度,浏览器就会自动产生横向滚动,这是 Web CSS 规范中溢出即可滚动的逻辑。

但 uni-app x 在 App 端并不跑在浏览器里。底层是原生布局引擎(Android 上类似 Flexbox 但行为有差异),其核心差异在于:

scroll-view 内的行容器如果没有一个明确的、超出 scroll-view 宽度的像素尺寸 ,原生引擎就认为内容没有溢出,不会开启横向滚动

仅靠 flex-direction: row + flex-wrap: nowrap 并不足以让原生引擎识别内容可横向滚动。这是从 Web 迁移到 uni-app x 最容易踩的一个坑。

下面是有问题的页面写法:

html 复制代码
<template>
	<view>
		<!-- 标题栏 -->
		<view class="section-header">
			<view class="section-title-wrap">
				<view class="section-title-bar"></view>
				<text class="section-title">{{ title }}</text>
			</view>
			<text class="section-more" @click="goMore">更多 &gt;</text>
		</view>

		<!-- 横向滚动卡片,flex-shrink:0 是横向滚动生效的关键 -->
		<scroll-view class="movie-scroll" scroll-x>
			<view class="movie-row">
				<view class="movie-card" v-for="(movie, index) in movies" :key="movie.id" @click="goDetail(movie.id)">
					<image class="movie-cover" :src="movie.cover" mode="aspectFill" />
					<view class="movie-card-info">
						<text class="movie-title">{{ movie.title }}</text>
						<text :class="movie.rate > 0 ? 'movie-rate' : 'movie-rate-none'">
							{{ formatRate(movie.rate) }}
						</text>
					</view>
				</view>
				<!-- 右侧留白,防止最后一张紧贴边缘 -->
				<view style="width: 10px; flex-shrink: 0;"></view>
			</view>
		</scroll-view>
	</view>
</template>

<script setup lang="uts">
	import { MovieItem } from '@/api/movie'

	const props = defineProps<{
		title : string
		type : string
		movies : Array<MovieItem>
	}>()

	const formatRate = (rate : number) : string => {
		return rate > 0 ? '★ ' + rate.toFixed(1) : '暂无'
	}

	const goMore = () => {
		uni.navigateTo({ url: `/pages/movie/movie-list?type=${props.type}` })
	}

	const goDetail = (id : string) => {
		uni.navigateTo({ url: `/pages/movie/detail?id=${id}` })
	}
</script>

<style>
	.section-header {
		flex-direction: row;
		justify-content: space-between;
		align-items: center;
		padding: 18px 10px 10px 10px;
	}

	.section-title-wrap {
		flex-direction: row;
		align-items: center;
	}

	.section-title-bar {
		width: 4px;
		height: 16px;
		background-color: #e67e22;
		border-radius: 2px;
		margin-right: 8px;
	}

	.section-title {
		font-size: 16px;
		font-weight: bold;
		color: #ffffff;
	}

	.section-more {
		font-size: 13px;
		color: #f5c518;
	}

	.movie-scroll {
		width: 100%;
		height: 400rpx;
	}

	.movie-row {
		flex-direction: row;
		padding-left: 10px;
	}

	.movie-card {
		width: 220rpx;
		margin-right: 10px;
		background-color: #1c1c2e;
		border-radius: 8px;
		overflow: hidden;
		flex-shrink: 0;
	}

	.movie-cover {
		width: 220rpx;
		height: 300rpx;
	}

	.movie-card-info {
		padding: 6px 8px 8px 8px;
	}

	.movie-title {
		font-size: 12px;
		color: #e8e8e8;
	}

	.movie-rate {
		font-size: 12px;
		color: #f5c518;
		margin-top: 3px;
	}

	.movie-rate-none {
		font-size: 12px;
		color: rgba(255, 255, 255, 0.35);
		margin-top: 3px;
	}
</style>

先不往下看,上述代码界面代码实现有问题吗?能看出scroll-view为何不能横向滚动吗?如果你一眼就看到了问题,那后面的就不用看了。如果没看出来,可往后看下猫哥分享的scroll-view不能横向滚动的踩坑记录。

uniapp-x的scroll-view容器组件相关文档介绍地址:https://doc.dcloud.net.cn/uni-app-x/component/scroll-view.html


票房榜组件(box-office)的实现

box-office 组件展示院线票房日榜,每张卡片占接近全屏宽度,适合左右翻页浏览。其正确工作的关键在于:用 JS 算出行容器的精确像素宽度,通过内联样式绑定给容器。

核心思路

  1. 卡片宽度用像素(px)计算,基于 uni.getWindowInfo().windowWidth 得到屏幕宽度
  2. 行容器总宽度 = 左侧 padding + (卡片宽 + 间距) × 数量 + 右侧留白,通过 computed 响应数据变化
  3. 卡片设置 flex-shrink: 0,防止被压缩

完整代码

html 复制代码
<template>
  <view class="box-office">
    <!-- 标题栏 -->
    <view class="section-header">
      <view class="header-left">
        <view class="title-bar"></view>
        <text class="title">院线票房日榜</text>
      </view>
      <text v-if="day.length > 0" class="day-text">{{ day }}</text>
    </view>

    <!-- 卡片横向滚动 -->
    <!-- 关键:cards-row 必须绑定精确的像素总宽度,原生引擎才能识别横向可滚动内容 -->
    <scroll-view v-if="!loading && list.length > 0" class="cards-scroll" direction="horizontal">
      <view class="cards-row" :style="`width: ${rowWidth}px;`">
        <view
          v-for="item in list"
          :key="item.top"
          class="movie-card"
          :style="`width: ${cardWidth}px;`"
        >
          <!-- 顶部彩色条 -->
          <view class="rank-accent" :style="`background-color: ${getRankColor(item.top)};`"></view>

          <view class="card-body">
            <!-- 排名 + 片名 -->
            <view class="card-head">
              <view class="rank-badge" :style="`background-color: ${getRankColor(item.top)};`">
                <text class="rank-num" :style="`color: ${item.top <= 3 ? '#1a1a2e' : '#ffffff'};`">
                  {{ item.top }}
                </text>
              </view>
              <view class="name-block">
                <text class="movie-name" :numberOfLines="1">{{ item.name }}</text>
                <text class="release-text">{{ item.release_date }}</text>
              </view>
            </view>

            <!-- 分割线 -->
            <view class="divider"></view>

            <!-- 四项指标 2×2 排列 -->
            <view class="metrics">
              <view class="metric">
                <text class="metric-val">{{ item.box_million }}</text>
                <text class="metric-label">今日票房</text>
              </view>
              <view class="metric">
                <text class="metric-val metric-highlight">{{ item.share_box }}</text>
                <text class="metric-label">票房占比</text>
              </view>
              <view class="metric metric-bottom">
                <text class="metric-val">{{ item.row_films }}</text>
                <text class="metric-label">排 片 率</text>
              </view>
              <view class="metric metric-bottom">
                <text class="metric-val">{{ item.row_seats }}</text>
                <text class="metric-label">上 座 率</text>
              </view>
            </view>
          </view>
        </view>

        <!-- 右侧留白 -->
        <view style="width: 14px; flex-shrink: 0;"></view>
      </view>
    </scroll-view>

    <!-- 加载中 -->
    <view v-if="loading" class="placeholder">
      <text class="placeholder-text">加载中...</text>
    </view>
  </view>
</template>

<script setup lang="uts">
  import { ref, computed, onMounted } from 'vue'
  import { MovieApi, PiaoItem } from '@/api/movie'

  const list = ref<PiaoItem[]>([])
  const day = ref<string>('')
  const loading = ref<boolean>(true)

  // 卡片宽度:屏幕宽度减去两侧各 14px padding,单张接近全屏
  const cardWidth = Math.floor(uni.getWindowInfo().windowWidth - 28)

  // 行总宽度:原生布局引擎需要明确的宽度才能识别横向可滚动内容
  // 14(padding-left) + (cardWidth + 12margin) × n + 14(右侧留白)
  const rowWidth = computed(() : number => {
    return 14 + (cardWidth + 12) * list.value.length + 14
  })

  // 按排名返回强调色:金 / 银 / 铜 / 深蓝
  const getRankColor = (top : number) : string => {
    if (top === 1) return '#f5c518'
    if (top === 2) return '#9eb3c2'
    if (top === 3) return '#e67e22'
    return '#3a5085'
  }

  onMounted(() => {
    MovieApi.getPiaomovie().then((result : any) => {
      const raw = result as { list : PiaoItem[], day : string }
      list.value = raw.list.filter((item : PiaoItem) : boolean => item.name.length > 0)
      day.value = raw.day
      loading.value = false
    }).catch((_ : any) => {
      loading.value = false
    })
  })
</script>

<style>
  .box-office {
    margin-top: 4px;
  }

  .section-header {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    padding: 18px 12px 10px 12px;
  }

  .header-left {
    flex-direction: row;
    align-items: center;
  }

  .title-bar {
    width: 4px;
    height: 16px;
    background-color: #e67e22;
    border-radius: 2px;
    margin-right: 8px;
  }

  .title {
    font-size: 16px;
    font-weight: bold;
    color: #ffffff;
  }

  .day-text {
    font-size: 11px;
    color: rgba(255, 255, 255, 0.35);
  }

  /* scroll-view 必须有固定高度 */
  .cards-scroll {
    width: 100%;
    height: 160px;
  }

  .cards-row {
    flex-direction: row;
    flex-wrap: nowrap;
    padding-left: 14px;
  }

  /* flex-shrink: 0 防止卡片被压缩 */
  .movie-card {
    margin-right: 12px;
    background-color: #16213e;
    border-radius: 12px;
    overflow: hidden;
    flex-shrink: 0;
  }

  .rank-accent {
    width: 100%;
    height: 2px;
  }

  .card-body {
    padding: 14px;
  }

  .card-head {
    flex-direction: row;
    align-items: flex-start;
    margin-bottom: 10px;
  }

  .rank-badge {
    width: 34px;
    height: 34px;
    border-radius: 17px;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-right: 12px;
    margin-top: 2px;
  }

  .rank-num {
    font-size: 16px;
    font-weight: bold;
  }

  .name-block {
    flex: 1;
  }

  .movie-name {
    font-size: 17px;
    font-weight: bold;
    color: #ffffff;
    margin-bottom: 5px;
  }

  .release-text {
    font-size: 11px;
    color: rgba(255, 255, 255, 0.45);
  }

  .divider {
    height: 1px;
    background-color: rgba(255, 255, 255, 0.08);
    margin-bottom: 12px;
  }

  .metrics {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .metric {
    width: 50%;
    margin-bottom: 4px;
  }

  .metric-bottom {
    margin-bottom: 0;
  }

  .metric-val {
    font-size: 15px;
    font-weight: bold;
    color: #e8e8e8;
    margin-bottom: 2px;
  }

  .metric-highlight {
    color: #f5c518;
  }

  .metric-label {
    font-size: 10px;
    color: rgba(255, 255, 255, 0.35);
    letter-spacing: 1px;
  }

  .placeholder {
    height: 100px;
    align-items: center;
    justify-content: center;
  }

  .placeholder-text {
    font-size: 13px;
    color: rgba(255, 255, 255, 0.35);
  }
</style>

电影卡片组件(movie-section)的实现

movie-section 是首页各区块(正在热映、即将上映等)的通用横向卡片列表,通过 props 接收标题、分类、影片数组,实现复用。

最初的问题写法

vue 复制代码
<!-- ❌ 问题写法:.movie-row 没有明确宽度,.movie-card 没有 flex-shrink: 0 -->
<scroll-view class="movie-scroll" direction="horizontal">
  <view class="movie-row">
    <view class="movie-card" v-for="(movie) in movies" ...>
      ...
    </view>
  </view>
</scroll-view>
css 复制代码
.movie-row {
  flex-direction: row;
  flex-wrap: nowrap;
  /* ❌ 缺少明确宽度,原生引擎不知道内容有多宽 */
}

.movie-card {
  width: 220rpx;
  /* ❌ 缺少 flex-shrink: 0,卡片会被压缩进容器 */
}

两处关键点

修复1:计算行容器的精确像素宽度

卡片 CSS 中写的是 220rpx,但 JS 计算必须换算成 px

复制代码
1rpx = windowWidth(px) ÷ 750
220rpx = 220 × windowWidth ÷ 750 (px)
ts 复制代码
// 将 220rpx 转换为 px
const cardWidthPx = Math.floor(220 * uni.getWindowInfo().windowWidth / 750)

// 行总宽度 = 10(padding-left) + (卡片宽px + 10间距px) × 数量 + 10(右侧留白px)
const rowWidth = computed(() : number => {
  return 10 + (cardWidthPx + 10) * props.movies.length + 10
})

修复2:给卡片加 flex-shrink: 0

css 复制代码
.movie-card {
  width: 220rpx;
  flex-shrink: 0; /* 防止卡片被 flex 容器压缩 */
}

完整代码

html 复制代码
<template>
  <view>
    <!-- 标题栏 -->
    <view class="section-header">
      <view class="section-title-wrap">
        <view class="section-title-bar"></view>
        <text class="section-title">{{ title }}</text>
      </view>
      <text class="section-more" @click="goMore">更多 &gt;</text>
    </view>

    <!-- 横向滚动卡片:movie-row 必须设置明确的像素宽度,原生引擎才能识别可滚动区域 -->
    <scroll-view class="movie-scroll" direction="horizontal">
      <view class="movie-row" :style="`width: ${rowWidth}px;`">
        <view
          class="movie-card"
          v-for="(movie) in movies"
          :key="movie.id"
          @click="goDetail(movie.id)"
        >
          <image class="movie-cover" :src="movie.cover" mode="aspectFill" />
          <view class="movie-card-info">
            <text class="movie-title">{{ movie.title }}</text>
            <text :class="movie.rate > 0 ? 'movie-rate' : 'movie-rate-none'">
              {{ formatRate(movie.rate) }}
            </text>
          </view>
        </view>
        <!-- 右侧留白,防止最后一张紧贴边缘 -->
        <view style="width: 10px; flex-shrink: 0;"></view>
      </view>
    </scroll-view>
  </view>
</template>

<script setup lang="uts">
  import { computed } from 'vue'
  import { MovieItem } from '@/api/movie'

  const props = defineProps<{
    title : string
    type : string
    movies : Array<MovieItem>
  }>()

  // 将 220rpx 转换为 px(原生引擎需要明确的像素宽度)
  const cardWidthPx = Math.floor(220 * uni.getWindowInfo().windowWidth / 750)
  // 10(padding-left) + (cardWidth + 10margin) × n + 10(右侧留白)
  const rowWidth = computed(() : number => {
    return 10 + (cardWidthPx + 10) * props.movies.length + 10
  })

  const formatRate = (rate : number) : string => {
    return rate > 0 ? '★ ' + rate.toFixed(1) : '暂无'
  }

  const goMore = () => {
    uni.navigateTo({ url: `/pages/movie/movie-list?type=${props.type}` })
  }

  const goDetail = (id : string) => {
    uni.navigateTo({ url: `/pages/movie/detail?id=${id}` })
  }
</script>

<style>
  .section-header {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    padding: 18px 10px 10px 10px;
  }

  .section-title-wrap {
    flex-direction: row;
    align-items: center;
  }

  .section-title-bar {
    width: 4px;
    height: 16px;
    background-color: #e67e22;
    border-radius: 2px;
    margin-right: 8px;
  }

  .section-title {
    font-size: 16px;
    font-weight: bold;
    color: #ffffff;
  }

  .section-more {
    font-size: 13px;
    color: #f5c518;
  }

  /* scroll-view 必须有固定高度 */
  .movie-scroll {
    width: 100%;
    height: 380rpx;
  }

  .movie-row {
    flex-direction: row;
    flex-wrap: nowrap;
    padding-left: 10px;
  }

  .movie-card {
    width: 220rpx;
    margin-right: 10px;
    background-color: #1c1c2e;
    border-radius: 8px;
    overflow: hidden;
    flex-shrink: 0; /* 关键:防止卡片被 flex 容器压缩 */
  }

  .movie-cover {
    width: 220rpx;
    height: 300rpx;
  }

  .movie-card-info {
    padding: 6px 8px 8px 8px;
  }

  .movie-title {
    font-size: 12px;
    color: #e8e8e8;
  }

  .movie-rate {
    font-size: 12px;
    color: #f5c518;
    margin-top: 3px;
  }

  .movie-rate-none {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.35);
    margin-top: 3px;
  }
</style>

踩坑注意事项

坑1:内容行容器必须有明确的像素总宽度(最核心)

这是横向滚动生效的必要条件

平台 触发横向滚动的条件
Web 浏览器 子元素自然溢出即可,overflow-x: auto 自动处理
uni-app x 原生端 必须为行容器声明超出 scroll-view 宽度的明确像素尺寸
html 复制代码
<!-- ❌ 错误:没有明确宽度,无法滚动 -->
<view class="row">...</view>

<!-- ✅ 正确:绑定精确的像素总宽度 -->
<view class="row" :style="`width: ${rowWidth}px;`">...</view>

rowWidth 的计算公式:

复制代码
rowWidth = 左padding + (单卡宽px + 卡间距px) × 卡片数量 + 右留白px

坑2:卡片必须设置 flex-shrink: 0

原生 Flexbox 默认 flex-shrink: 1。当父容器(scroll-view)宽度固定时,如果不禁止收缩,卡片就会被压缩进容器,而不是形成可滚动的溢出内容。

css 复制代码
/* ✅ 必须加,否则卡片实际渲染宽度会失效 */
.card {
  flex-shrink: 0;
}

坑3:rpx 不能直接用于 JS 计算行宽

CSS 中的 rpx 是由渲染引擎在绘制时转换的响应式单位,JS 拿不到这个值。凡是涉及到计算行容器总宽度的地方,必须自己换算:

ts 复制代码
// ✅ 正确:手动换算 rpx → px
const cardWidthPx = Math.floor(220 * uni.getWindowInfo().windowWidth / 750)

// ❌ 错误:220 在 JS 里只是卡片的 rpx 数值,直接相乘结果偏差很大
// const rowWidth = 220 * count  // 这是 rpx 数值,不是 px

坑4:scroll-view 本身必须有固定高度

如果 scroll-view 没有设置固定高度,在某些平台上会塌陷为 0,内容不可见。

css 复制代码
/* ✅ 高度用 rpx 或 px 均可,但必须明确 */
.movie-scroll {
  width: 100%;
  height: 380rpx;
}

坑5:行容器的 padding 必须计入总宽度

padding-left 占用空间,如果计算行宽时忽略它,最后一张卡片会被截断显示不完整。

ts 复制代码
// ✅ 正确:padding-left 和右侧留白都算进去
const rowWidth = 10 /*padding-left*/ + (cardWidthPx + 10) * count + 10 /*trailing*/

// ❌ 错误:漏算 padding,最后一张卡片右侧会被裁剪
const rowWidth = (cardWidthPx + 10) * count

横向滚动组件封装三要素

基于以上踩坑经验,总结出一套在 uni-app x 中封装横向滚动组件必须满足的三要素

复制代码
1. scroll-view 有固定高度
        ↓
2. 内容行容器绑定精确的像素总宽度(:style="width: Xpx")
        ↓
3. 每张卡片设置 flex-shrink: 0

三者缺一不可。对应到代码模板:

html 复制代码
<template>
  <!-- 要素1:scroll-view 有固定高度 -->
  <scroll-view style="width: 100%; height: 200px;" direction="horizontal">
    <!-- 要素2:行容器绑定精确像素宽度 -->
    <view :style="`width: ${rowWidth}px; flex-direction: row;`">
      <view
        v-for="item in list"
        :key="item.id"
        :style="`width: ${cardWidthPx}px; margin-right: ${gap}px; flex-shrink: 0;`"
      >
        <!-- 卡片内容 -->
      </view>
      <view :style="`width: ${trailingSpace}px; flex-shrink: 0;`"></view>
    </view>
  </scroll-view>
</template>

<script setup lang="uts">
  import { computed } from 'vue'

  // 所有尺寸统一换算为 px
  const windowWidth = uni.getWindowInfo().windowWidth
  const paddingLeft = 10
  const gap = 10
  const trailingSpace = 10
  // 设计稿给的 rpx 值 → px:rpx值 × windowWidth / 750
  const cardWidthPx = Math.floor(220 * windowWidth / 750)

  // 要素2 的宽度计算
  const rowWidth = computed(() : number => {
    return paddingLeft + (cardWidthPx + gap) * props.list.length + trailingSpace
  })
</script>

Web 与 uni-app x 原生端的行为对比

维度 Web 浏览器 uni-app x 原生端
横向滚动触发条件 子元素溢出即可,浏览器自动处理 必须为行容器声明超出容器的明确像素宽度
flex 溢出识别 自动识别 不自动识别,需显式声明宽度
rpx 单位 不支持 CSS 中支持;JS 中需手动换算为 px
flex-shrink 默认值 1(会压缩子元素) 1(行为同 Web,同样会压缩)
overflow 属性 支持,控制溢出显示 uni-app x 中 overflow 行为与 Web 有差异,横向滚动不依赖此属性

总结

uni-app x 编译为原生代码后,布局引擎与 Web 浏览器的行为存在差异。实现横向滚动卡片时,踩坑记录,必须牢记:

  1. 行容器要有明确的像素宽度------这是原生引擎识别横向可滚动内容的前提。
  2. 卡片要禁止收缩 ------flex-shrink: 0 让卡片保持固定宽度形成溢出
  3. rpx 转 px 要手动换算------JS 层的计算全部使用 px,rpx 只用于 CSS 样式

掌握这三条,横向滚动组件在 Android、iOS、鸿蒙各端均可正常工作。

相关推荐
不爱吃糖的程序媛2 小时前
Flutter Orientation 插件在鸿蒙平台的使用指南
flutter·华为·harmonyos
郑州光合科技余经理2 小时前
从零到一:构建UberEats式海外版外卖系统
java·开发语言·前端·javascript·架构·uni-app·php
2301_796512522 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:点击组件(跳转快应用)
javascript·react native·react.js·ecmascript·harmonyos
不爱吃糖的程序媛2 小时前
鸿蒙Flutter实战:Windows环境搭建踩坑指南
flutter·华为·harmonyos
2301_796512522 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Sidebar 侧边导航(绑定当前选中项的索引)
javascript·react native·react.js·ecmascript·harmonyos
hellojackjiang20112 小时前
鸿蒙Next原生IM即时通讯RainbowTalk,纯ArkTS编写,基于开源MobileIMSDK框架
网络编程·信息与通信·harmonyos·即时通讯
lbb 小魔仙3 小时前
鸿蒙跨平台项目实战篇01:React Native Bundle版本管理详解
react native·react.js·harmonyos
2301_796512523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Pagination 分页(绑定当前页码)
javascript·react native·react.js·ecmascript·harmonyos
不爱吃糖的程序媛3 小时前
Flutter 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos