当前位置: 首页 > news >正文

HarmonyOS应用<奇妙科学乐园>开发第48篇:空状态组件EmptyState——ResourceStr图标与文案兜底

📖 引言

在上一篇文章中,我们深入拆解了 AnimationDemo 动画演示组件的 setInterval + Math.sin 动画驱动机制,以及六种科普场景(太阳系、植物光合、海洋世界、雨天气象、星空梦境、科技机器人)的实现细节。动画演示为文章详情页带来了生动的交互体验。然而,并非所有页面都能展示丰富内容——当收藏列表为空、浏览历史被清空、网络请求失败时,用户看到的将是一片空白。这时候就需要一个关键的兜底组件:EmptyState 空状态组件

EmptyState 是《奇妙科学乐园》中所有列表页面的"最后一道防线"。它负责在数据为空、加载失败、网络异常等场景下,向用户展示友好的提示信息,并提供操作入口(如"去发现"按钮)引导用户采取行动。在儿童应用中,空状态的设计尤为重要——一个冰冷的"暂无数据"远不如一个带图标、带描述、带操作按钮的温馨提示来得友好。

源码仓库https://atomgit.com/2301_79280419/WonderSciencePark


🎯 学习目标

完成本文后,你将能够:

  • ✅ 掌握 EmptyState 组件的"图标 + 标题 + 描述 + 操作按钮"四层结构设计
  • ✅ 理解 ResourceStr 类型在图标属性中的兼容性设计(字符串 vs Resource)
  • ✅ 运用 EmptyStateAction 接口实现可选操作按钮的类型安全
  • ✅ 掌握 layoutWeight(1) 实现空状态垂直居中的布局技巧
  • ✅ 理解三态设计:空数据 / 加载失败 / 网络异常的统一组件适配
  • ✅ 学会多个页面复用 EmptyState 时的差异化配置策略

💡 需求分析

EmptyState 的功能定位

EmptyState 是一个通用的空状态兜底组件,在项目中被多个页面使用:

收藏页 Favorites ├── 加载中 → SkeletonList 骨架屏 ├── 有数据 → LazyForEach 列表 └── 无数据 → EmptyState ←"还没有收藏哦"+"去发现"按钮 浏览历史 History ├── 加载中 → SkeletonList 骨架屏 ├── 有数据 → LazyForEach 列表 └── 无数据 → EmptyState ←"还没有浏览记录哦"+"去发现"按钮 错题本 WrongQuiz ├── 加载中 → SkeletonList 骨架屏 ├── 有数据 → 列表 / 练习模式 └── 无数据 → EmptyState ←"错题本是空的"+"去做题"按钮 文章详情 TopicDetail(数据异常时) └── topic 为null→ 内联空状态(未使用 EmptyState 组件)

三态设计需求

状态图标标题描述操作按钮
空数据icon_favorite还没有收藏哦遇到喜欢的文章,点击右上角收藏起来吧去发现
空数据icon_history还没有浏览记录哦阅读过的文章会出现在这里去发现
空数据icon_wrong错题本是空的做错的题目会自动收录到这里去做题
加载失败icon_empty加载失败请检查网络后重试重新加载
网络异常icon_empty网络连接异常请检查网络设置重试

Props 设计

Props 名称类型必填默认值说明
iconResourceStr'📭'空状态图标,支持 emoji 字符串和 Resource 资源引用
titlestring'暂无数据'主标题文案
descriptionstring''补充描述文案,为空时不渲染
actionEmptyStateActionundefined操作按钮配置,为空时不渲染按钮

EmptyStateAction 接口

exportinterfaceEmptyStateAction {text:string;// 按钮文案onClick:() =>void;// 点击回调}

🏗️ 整体架构设计

组件结构概览

EmptyState 采用"四层条件渲染"的极简架构:

EmptyState ├── 第一层:图标(Image) │ ├── 支持 emoji 字符串:'📭''🔍'等 │ └── 支持 Resource 引用:$r('app.media.icon_favorite') ├── 第二层:主标题(Text) │ └── 必须展示,fontSize16,Medium 字重 ├── 第三层:描述文案(Text) ← 可选 │ └── description 非空时渲染,最多2行 └── 第四层:操作按钮(Button) ← 可选 └──action非空时渲染,胶囊样式

视觉布局结构

┌────────────────────────────────────┐ │ │ │[64x64]│ ←icon图标 │ 📭 │ │ │ │ 还没有收藏哦 │ ← title 主标题 │ │ │ 遇到喜欢的文章,点击右上角 │ ← description 描述(可选) │ 收藏起来吧 │ │ │ │[ 去发现 ]│ ← action 操作按钮(可选) │ │ └────────────────────────────────────┘ ↑ 整体 layoutWeight(1) + 居中对齐

🔧 核心实现拆解

1. 接口定义与类型设计

export interface EmptyStateAction { text: string; onClick: () => void; }@Componentexport struct EmptyState {icon: ResourceStr ='📭'; title: string ='暂无数据'; description: string =''; action?: EmptyStateAction;

关键设计要点:

  • EmptyStateAction 接口:单独定义操作按钮的类型,包含text(按钮文案)和onClick(点击回调)两个字段。使用 interface 而非内联对象字面量,方便类型复用和 IDE 智能提示。
  • icon: ResourceStr:这是整个组件最精巧的类型选择。ResourceStr是 HarmonyOS ArkUI 中表示"字符串或资源引用"的联合类型,它可以同时接受:
    • 普通字符串:'📭''🔍''暂无图片'
    • 资源引用:$r('app.media.icon_empty')$r('app.media.icon_favorite')
  • action?: EmptyStateAction:使用?可选修饰符,当调用方不传 action 时,按钮区域不渲染。这种"可选属性 + 条件渲染"的模式是 ArkUI 组件设计中非常常见的模式。

ResourceStr 的兼容性价值:

//场景1:使用 emoji 字符串作为图标(简单快捷) EmptyState({ icon:'📭',//emoji 字符串 title:'暂无数据'})//场景2:使用 SVG 资源作为图标(更专业) EmptyState({ icon:$r('app.media.icon_favorite'),//Resource 资源引用 title:'还没有收藏哦', description:'遇到喜欢的文章,点击右上角收藏起来吧', action: { text:'去发现', onClick: () => {/* 跳转到发现页 */} } })

✅ 正确做法:icon 属性声明为ResourceStr类型,兼容字符串和 Resource 两种形式,最大化组件的灵活性。

❌ 错误做法:将 icon 声明为string类型,导致无法传入$r()资源引用;或者声明为Resource类型,导致无法使用简单的 emoji 字符串。

2. 图标层:Image 组件的 ResourceStr 兼容

build(){ Column() { Image(this.icon).width(64).height(64).objectFit(ImageFit.Contain).margin({bottom:16});

Image 组件与 ResourceStr 的兼容机制:

ArkUI 的Image组件本身支持多种类型的 src 参数:

  • string:网络 URL 或本地路径
  • PixelMap:像素图对象
  • Resource:通过$r()引用的资源

当我们将icon声明为ResourceStr并传入Image(this.icon)时:

  • 传入'📭':Image 会尝试加载名为'📭'的资源,由于不是有效的资源路径,会显示为空白或占位符。但在实际使用中,项目更倾向于使用 Resource 类型。

实际项目中的使用情况:

通过搜索项目中所有 EmptyState 的使用位置,发现实际调用均使用Resource类型:

//Favorites.ets 中 EmptyState({ icon:$r('app.media.icon_favorite'),//Resource 类型 title:'还没有收藏哦', description:'遇到喜欢的文章,点击右上角收藏起来吧', action: { text:'去发现', onClick: () => { ... } } });//History.ets 中 EmptyState({ icon:$r('app.media.icon_history'),//Resource 类型 title:'还没有浏览记录哦', ... });//WrongQuiz.ets 中 EmptyState({ icon:$r('app.media.icon_wrong'),//Resource 类型 title:'错题本是空的', ... });

尽管如此,保留ResourceStr类型仍然是正确的设计决策——它为未来可能的 emoji 简写场景预留了扩展空间,且不会带来额外的类型安全风险。

3. 标题层:必展示的主文案

Text(this.title).fontSize(16).fontWeight(FontWeight.Medium).fontColor(ThemeColors.TEXT_PRIMARY).margin({bottom:8});

设计要点:

  • fontSize 16:作为空状态的视觉焦点,标题需要比描述文案(13)更大、更醒目
  • FontWeight.Medium:中等字重,在视觉层级上仅次于页面标题的 Bold
  • TEXT_PRIMARY(#333333):使用最深的主文字色,确保在浅色背景下有足够的对比度
  • **margin({ bottom: 8 })**:与下方描述文案保持 8vp 间距,视觉上紧密关联

标题是 EmptyState 唯一的必渲染元素。即使 description 和 action 都为空,标题也能单独传达"当前没有数据"的核心信息。

4. 描述层:条件渲染的补充文案

if (this.description) { Text(this.description).fontSize(13).fontColor(ThemeColors.TEXT_SECONDARY).textAlign(TextAlign.Center).margin({ bottom:20}).maxLines(2).width('80%'); }

设计要点:

  • **if (this.description)**:当 description 为空字符串''时,整个 Text 组件不渲染,避免出现空行
  • fontSize 13:比标题小 3 号,形成清晰的主次层级
  • TEXT_SECONDARY(#666666):使用次要文字色,在视觉上弱于标题
  • **textAlign(TextAlign.Center)**:居中对齐,与图标和标题保持中轴线一致
  • **maxLines(2)**:限制最多显示 2 行,防止描述文案过长导致布局变形
  • **width('80%')**:限制文本宽度为父容器的 80%,两侧留白,避免长文案贴边

✅ 正确做法:使用width('80%')+textAlign(TextAlign.Center)的组合,确保长文案在居中的同时不会撑满整个屏幕宽度。

❌ 错误做法:不设width,直接用默认全宽。当描述文案很长时,会从左到右铺满,视觉上不够精致。

5. 操作按钮层:可选的行动引导

if(this.action) { Button(this.action.text) .fontSize(14) .fontColor('#ffffff') .backgroundColor(ThemeColors.PRIMARY) .borderRadius(20) .padding({ left:24, right:24, top:8, bottom:8}) .onClick(() => {if(this.action &&this.action.onClick) {this.action.onClick(); } }); }

设计要点:

  • **if (this.action)**:操作按钮是可选的。某些空状态(如页面底部的"没有更多数据"提示)不需要操作按钮
  • ThemeColors.PRIMARY(#ff6b6b):使用项目主题色,与整体视觉风格保持一致
  • **borderRadius(20)**:胶囊形按钮,圆角值为高度(32)的 62.5%,呈现柔和的胶囊形态
  • onClick 中的双重判断if (this.action && this.action.onClick)确保在异步更新场景下不会因为 action 被置空而导致崩溃

操作按钮的安全回调设计:

.onClick(() => {if(this.action &&this.action.onClick) {this.action.onClick(); } });

这行代码包含了两层防御:

  1. this.action:确保 action 对象存在(可能被父组件动态置空)
  2. this.action.onClick:确保回调函数存在(接口字段可能为 undefined)

✅ 正确做法:在 onClick 回调中进行双重判空,防止运行时异常。

❌ 错误做法:直接写this.action.onClick(),当 action 为 undefined 时会抛出Cannot read property 'onClick' of undefined错误。

6. 整体布局:垂直居中策略

Column(){//...图标 + 标题 + 描述 + 按钮 }.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center);

布局策略拆解:

  • **width('100%')**:占满父容器宽度
  • **layoutWeight(1)**:这是实现垂直居中的关键!layoutWeight(1)让 EmptyState 占据父容器中所有剩余的垂直空间。配合justifyContent(FlexAlign.Center),内容自然在可用空间的正中央显示
  • **justifyContent(FlexAlign.Center)**:垂直方向居中
  • **alignItems(HorizontalAlign.Center)**:水平方向居中

为什么不用 height('100%')?

// ❌ 错误:使用固定 height 可能在某些布局中导致溢出Column(){ ... }.width('100%').height('100%')// 如果父容器有其他子组件,100% 可能超出.justifyContent(FlexAlign.Center)// ✅ 正确:使用 layoutWeight 自适应剩余空间Column(){ ... }.width('100%').layoutWeight(1)// 自动占据剩余空间,不会溢出.justifyContent(FlexAlign.Center)

在典型的页面布局中,EmptyState 通常与 AppBar 共存:

Column { AppBar()//固定高度 ~56vp EmptyState()//layoutWeight(1),占据剩余全部空间 }

layoutWeight(1)确保 EmptyState 精确占据 AppBar 下方到屏幕底部的所有空间,不会出现滚动条,也不会溢出。


📊 三态配置对照表

项目中各页面的 EmptyState 配置

页面icontitledescriptionaction.text
Favorites$r('app.media.icon_favorite')还没有收藏哦遇到喜欢的文章,点击右上角收藏起来吧去发现
History$r('app.media.icon_history')还没有浏览记录哦阅读过的文章会出现在这里去发现
WrongQuiz$r('app.media.icon_wrong')错题本是空的做错的题目会自动收录到这里去做题

建议的扩展配置(网络异常、加载失败)

场景icontitledescriptionaction.text
加载失败$r('app.media.icon_empty')加载失败请稍后再试重新加载
网络异常$r('app.media.icon_empty')网络连接异常请检查网络设置后重试重试
搜索无结果'🔍'没有找到相关内容试试其他关键词吧清空搜索

🎨 实际页面集成案例

案例1:收藏页 Favorites 的三段式布局

Favorites 页面采用了经典的"加载中 - 有数据 - 空数据"三段式布局:

build() {Column() {AppBar({barTitle:'我的收藏',showBack:true,onBack:() =>this.goBack() });if(this.isLoading) {// 第一态:加载中 → 骨架屏Scroll() {SkeletonList({count:5,showImage:true}); } .layoutWeight(1) .backgroundColor(ThemeColors.BG_SECONDARY); }elseif(this.favoriteTopics.length>0) {// 第二态:有数据 → 列表List() {ListItem() {Text('共收藏 '+this.favoriteTopics.length+' 篇文章') .fontSize(13) .fontColor(ThemeColors.TEXT_SECONDARY); }LazyForEach(this.topicDataSource,(topic: Topic) =>{ListItem() {TopicCard({topic: topic,onItemClick:(t: Topic) =>this.goToTopicDetail(t) }); } },(topic: Topic) =>topic.id.toString()); } .layoutWeight(1) .backgroundColor(ThemeColors.BG_SECONDARY); }else{// 第三态:空数据 → EmptyStateEmptyState({icon: $r('app.media.icon_favorite'),title:'还没有收藏哦',description:'遇到喜欢的文章,点击右上角收藏起来吧',action: {text:'去发现',onClick:() =>{constparams:RouterParams= {tabIndex:1};constoptions:RouterOptions= {url:RouteUrls.MAIN_TABS,params: params };RouterUtil.replaceUrl(options,'Favorites'); } } }); } } .width('100%') .height('100%') .backgroundColor(ThemeColors.BG_SECONDARY); }

三段式布局的核心逻辑:

if(isLoading) → 骨架屏(用户看到"正在加载"的暗示)elseif(有数据) → 真实列表(用户看到实际内容)else→ EmptyState(用户看到友好的空状态提示)

这种三段式布局是移动端列表页面的标准模式,保证了用户在任何数据状态下都能看到有意义的 UI。

案例2:EmptyState action 中的路由跳转

Favorites 页面的"去发现"按钮使用了RouterUtil.replaceUrl跳转到发现 Tab:

action: { text:'去发现', onClick: () => {// 使用 replaceUrl 而非 pushUrl,避免在导航栈中积累页面constparams: RouterParams = { tabIndex:1};constoptions: RouterOptions = { url: RouteUrls.MAIN_TABS,params:params}; RouterUtil.replaceUrl(options,'Favorites'); } }

路由策略选择:

  • replaceUrl:替换当前页面,用户点击返回时不会回到空白的收藏页
  • pushUrl:在导航栈上叠加新页面,用户需要点两次返回才能退出

在空状态场景中,使用replaceUrl是更合理的策略——既然收藏页是空的,用户返回来还是空页面,不如直接替换。


⚠️ 避坑指南

1. action 回调中的闭包陷阱

// ❌ 错误:在 ForEach 中直接引用循环变量 ForEach(this.pages,(page:string)=>{ EmptyState({action: {text:'去'+ page,onClick:()=>{ // 闭包捕获的是 page 的引用,可能导致所有按钮都跳转到最后一个页面 RouterUtil.pushUrl({url: page }); } } }); },(page:string)=>page); // ✅ 正确:使用 const 固定闭包变量 ForEach(this.pages,(page:string)=>{ consttargetUrl:string= page; // 用 const 锁定值 EmptyState({action: {text:'去'+ targetUrl,onClick:()=>{ RouterUtil.pushUrl({url: targetUrl }); } } }); },(page:string)=>page);

2. description 为空字符串时的渲染判断

// ❌ 错误:使用 !this.description 无法区分空字符串和未传入if(!this.description) { Text(this.description);// 空字符串也会进入此分支}// ✅ 正确:直接用 if (this.description) 判断空字符串if(this.description) { Text(this.description) .fontSize(13) .maxLines(2) .width('80%'); }

在 ArkTS 中,空字符串''是 falsy 值,if (this.description)在 description 为''时不会执行,这正是我们想要的行为。

3. layoutWeight 在非 Flex 容器中无效

//❌ 错误:在非 Column/Row/Flex 容器中使用 layoutWeight Stack() { EmptyState()//layoutWeight(1) 在 Stack 中无效! }//✅ 正确:确保父容器是 Column/Row/Flex Column() { AppBar(); EmptyState()//layoutWeight(1) 在 Column 中正常工作 }

4. Image 组件不支持 emoji 渲染的替代方案

// ⚠️ 注意:Image 组件传入 emoji 字符串可能无法正常渲染// 如果确实需要显示 emoji 图标,建议使用 Text 组件// 方案A:使用 Text 显示 emoji(当前项目实际采用 Resource 方案)Text('📭').fontSize(48).margin({bottom:16});// 方案B:保持 Image + Resource(项目实际使用的方案)Image($r('app.media.icon_favorite')).width(64).height(64).objectFit(ImageFit.Contain).margin({bottom:16});

在本项目中,所有页面的 EmptyState 都使用了$r()Resource 引用方式传入图标,因此 Image 组件是正确的选择。如果未来需要支持 emoji 图标,可以考虑在组件内部做类型判断:

// 进阶方案:根据 icon 类型自动选择渲染组件@BuilderIconBuilder(){if(typeof this.icon==='string') {Text(this.iconasstring).fontSize(48).margin({ bottom:16}); }else{Image(this.iconasResource).width(64) .height(64) .objectFit(ImageFit.Contain).margin({ bottom:16}); } }

🔄 进阶思考:空状态组件的扩展设计

扩展方向一:支持自定义内容插槽

当前 EmptyState 通过 props 配置图标、标题、描述和按钮。如果需要更灵活的内容定制(如添加特定插画、动画等),可以考虑使用@Builder插槽:

// 进阶方案:@Builder 插槽(非项目当前实现,仅供参考)@Component exportstructAdvancedEmptyState { icon: ResourceStr = '📭'; title:string= '暂无数据'; description:string= ''; action?: EmptyStateAction; customContent?:()=>void;// 自定义内容插槽build(){Column(){Image(this.icon).width(64).height(64) .objectFit(ImageFit.Contain).margin({ bottom:16});Text(this.title).fontSize(16).fontWeight(FontWeight.Medium).fontColor(ThemeColors.TEXT_PRIMARY).margin({ bottom:8});if(this.description) {Text(this.description).fontSize(13).fontColor(ThemeColors.TEXT_SECONDARY).textAlign(TextAlign.Center).maxLines(2).width('80%') .margin({ bottom:20}); }// 自定义内容区域if(this.customContent) { this.customContent(); }if(this.action){ Button(this.action.text).fontSize(14).fontColor('#ffffff').backgroundColor(ThemeColors.PRIMARY).borderRadius(20).onClick(()=>{if(this.action?.onClick) { this.action.onClick(); } }); } } .width('100%') .layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center); } }

扩展方向二:支持加载动画

在空状态中添加一个微妙的呼吸动画,让页面看起来"活"着:

// 进阶方案:呼吸动画(非项目当前实现,仅供参考)@StatebreathScale:number=1;aboutToAppear() {// 简单的呼吸动画setInterval(() =>{this.breathScale=1+Math.sin(Date.now() *0.003) *0.05; },50); }// 在图标上应用Image(this.icon) .width(64) .height(64) .scale({x:this.breathScale,y:this.breathScale}) .objectFit(ImageFit.Contain) .margin({bottom:16});

扩展方向三:主题色适配

// 进阶方案:支持深色模式(非项目当前实现,仅供参考)@Prop isDark: boolean =false;// 图标颜色根据主题变化Image(this.icon).width(64) .height(64) .fillColor(this.isDark? '#ffffff' : ThemeColors.TEXT_TERTIARY).objectFit(ImageFit.Contain);

⚠️ 常见问题

Q1: EmptyState 在页面中没有垂直居中,而是紧贴顶部

现象:EmptyState 组件渲染后,图标、标题、按钮全部紧贴页面顶部,没有居中显示。
原因layoutWeight(1)只在 Flex 容器(Column/Row)中生效。如果 EmptyState 的父容器是StackScrolllayoutWeight(1)会被忽略,组件高度由内容撑开而非占据剩余空间。
解决方案:确保 EmptyState 的直接父容器是 Column。

// ❌ 错误写法:在 Stack 中使用 EmptyState,layoutWeight 无效Stack(){AppBar();EmptyState()// layoutWeight(1) 在 Stack 中不生效}// ✅ 正确写法:在 Column 中使用 EmptyState,正确占据剩余空间Column(){AppBar();EmptyState()// layoutWeight(1) 在 Column 中正常工作}

Q2: 点击 EmptyState 的操作按钮时应用崩溃

现象:当 EmptyState 的action属性通过异步条件动态赋值时,点击按钮偶尔抛出Cannot read property 'onClick' of undefined错误。
原因onClick回调中没有对this.action做判空保护。当父组件在异步更新中将action置空,而用户恰好在此时点击按钮,就会触发空引用错误。
解决方案:在 onClick 中进行双重判空。

// ❌ 错误写法:直接调用回调,未做判空保护if(this.action) { Button(this.action.text) .onClick(() => {this.action.onClick();// action 可能在异步中被置空}); }// ✅ 正确写法:onClick 内部再次判空if(this.action) { Button(this.action.text) .onClick(() => {if(this.action &&this.action.onClick) {this.action.onClick();// 双重判空,安全调用} }); }

Q3: Image 组件传入 emoji 字符串时图标区域显示空白

现象:使用EmptyState({ icon: '📭', title: '暂无数据' })时,图标区域渲染为空白。
原因Image组件期望接收string类型的路径或Resource类型的资源引用。传入 emoji 字符串'📭'时,Image 尝试加载名为'📭'的资源文件,自然找不到而显示空白。
解决方案:使用$r()Resource 引用传入 SVG/PNG 图标资源,或改用Text组件渲染 emoji。

//❌ 错误写法:Image 组件无法渲染 emoji 字符串 EmptyState({ icon:'📭',//Image 无法渲染 emoji title:'暂无数据'})//✅ 正确写法:使用 Resource 引用传入矢量图标资源 EmptyState({ icon:$r('app.media.icon_favorite'), title:'还没有收藏哦'})

📝 小结

EmptyState 空状态组件是《奇妙科学乐园》中"不起眼但不可或缺"的基础组件。本文从以下六个方面进行了完整拆解:

  1. 类型设计:ResourceStr 类型的 icon 属性兼容字符串和 Resource 两种形式
  2. 四层结构:图标 + 标题 + 可选描述 + 可选按钮的条件渲染架构
  3. 布局策略:layoutWeight(1) + justifyContent(Center) 实现精确的垂直居中
  4. 安全回调:onClick 中的双重判空防止运行时异常
  5. 三态适配:同一个组件通过 props 差异化配置,适配空数据/加载失败/网络异常
  6. 页面集成:Favorites 等页面的三段式布局(加载中/有数据/空数据)

核心设计理念:用最少的代码(67 行)实现最通用的空状态兜底能力,通过 props 差异化配置适配不同场景,避免在每个页面中重复编写空状态 UI。这是组件化思维在 HarmonyOS ArkUI 开发中的典型实践。


源码仓库https://atomgit.com/2301_79280419/WonderSciencePark
组件路径entry/src/main/ets/components/base/EmptyState.ets

🔗 相关链接

  • 项目源码Atomgit仓库
  • 上一篇HarmonyOS应用<奇妙科学乐园>开发第47篇:动画演示组件AnimationDemo——科普互动设计
  • 下一篇HarmonyOS应用<奇妙科学乐园>开发第49篇:通用列表项ListItem——icon+标题+箭头封装
http://www.cnnetsun.cn/news/3839567.html

相关文章:

  • 算法竞赛中的质数模数1000000007:原理、应用与避坑指南
  • 粉丝空间站第3期:从信息过载到高效实践的技术内容筛选与实战指南
  • 终极网络诊断指南:如何用NatTypeTester快速识别并优化你的NAT类型
  • 程序员外包合同模板:从需求定义到付款验收的完整防坑指南
  • 四层高功率PCB厚铜制造全流程工艺管控与缺陷预防
  • 四层高功率PCB可靠性验证方案 DFM标准化检查清单
  • Vue Devtools 安装与调试全攻略:从基础配置到高级场景实战
  • 单片机GPIO实现倍压电荷泵:原理、设计与实践
  • 如何高效解决Visual C++运行库问题:VisualCppRedist AIO专业指南
  • AI 电动仿真花旋转展示台智能功率覆盖电机驱动、控制逻辑、电源管理的完整选型方案
  • Talkin跨语言实时对话应用:技术原理、实战评测与工程实践
  • 打造高并发个人微信消息发送网关
  • Unity第三人称控制器开发指南:从基础原理到实战调优
  • STM32CubeMX串口配置深度解析:从DMA到中断的实战优化指南
  • 2024年Unity开发者必备:VSCode源码级调试环境配置与实战指南
  • 总谐波失真THD:从概念到测量与优化的完整指南
  • 分布鲁棒优化研究(Matlab代码实现)
  • RedHat系统GCC/G++编译环境配置与多版本管理实战指南
  • 工业自动化系统组态与工程下载:从虚拟设计到物理部署的完整指南
  • 揭秘百家号算法最新变动:AI写稿如何绕过限流雷区,3步通过原创审核
  • AI赋能自动化安全测试平台:从架构设计到工程落地实践
  • CTF逆向工程实战:从静态分析到动态调试的完整解题思路
  • C# WPF窗口任意区域拖动实现:原理、方案与实战避坑指南
  • 【2027最新】基于SpringBoot+Vue的论文管理系统源码+MyBatis+MySQL
  • 《骑在银龙的背上》歌词深度解析与罗马音跟唱指南
  • ClickHouse列式存储引擎:MergeTree系列与向量化执行深度解析
  • 深入解析SSD Trim指令:原理、配置与数据恢复的真相
  • SolidWorks自学指南:从零基础到工程实践
  • 企业网盘选哪个比较好?从技术架构到产品体验的全方位对比
  • 8个可以直接复制的AI提示词:从问清需求到去掉AI味