文章目录
- 完整代码
- 最后总结
壁纸页看起来是图片网格,但真正要处理的是内容浏览链路:用户先搜索或按分类缩小范围,再点进某张壁纸预览,最后决定收藏或设为当前壁纸。这个案例把这条链路压在一个页面里,代码不算复杂,但状态关系挺完整。
WallpaperItem是这页的核心模型。它不只是记录壁纸标题,还包含category、colors、isFavorite、isSet、downloads。其中colors用来模拟壁纸渐变背景,isFavorite表示用户资产,isSet表示当前系统选择,downloads则给内容增加一点热度信息。也就是说,一张壁纸同时承担展示、筛选、操作和状态标记四类职责。
wallpapers是主数据,activeCategory和searchText是筛选条件,previewWallpaper是当前预览对象。这三个层级分得比较清楚:主数据负责提供内容全集,筛选状态负责决定网格里出现哪些项,预览状态负责控制弹层。你读这种页面时,先把这三层关系抓住,后面 UI 结构就好理解很多。
getFilteredWallpapers()是内容流的入口。它先从全部壁纸开始,如果分类不是全部,就按category过滤;如果有搜索词,再按标题做二次过滤。这个顺序很自然,分类负责粗筛,搜索负责细筛。最终 Grid 只关心这个函数返回什么,不需要知道筛选规则具体怎么拼。
这类函数最好集中写,不要把过滤逻辑散在ForEach里。原因很简单:壁纸内容流以后很可能继续加条件,比如收藏筛选、下载量排序、颜色标签、最近使用。如果筛选逻辑一开始就收在getFilteredWallpapers(),后面加条件只需要改一个入口。
收藏逻辑用toggleFavorite(id)处理,它根据id找到目标壁纸,把isFavorite取反。网格卡片和预览弹层都能触发这个动作,所以它必须是一个独立函数,而不是写在某个按钮里。这里还有一个细节:预览弹层里收藏后,会重新从wallpapers里找到当前项并更新previewWallpaper,这样弹层按钮文案能立刻同步。
setWallpaper(id)的业务规则更强一点。收藏可以有很多个,但当前壁纸只能有一个,所以它不是简单地修改某一项,而是遍历所有壁纸,让目标项isSet = true,其他项全部变成false。这就是一个典型的“唯一选中”状态迁移。写设置类页面时,这种规则一定要收在函数里,否则很容易出现两个“当前”同时存在的 bug。
预览弹层由previewWallpaper: WallpaperItem | null控制。点击网格项时把它设成当前壁纸,遮罩点击或设置完成后再设回null。这比单独维护showPreview加currentId更直接,因为弹层需要展示完整壁纸信息,直接保存当前对象能少一次查找。
不过这种写法也有取舍。对象如果来自wallpapers数组,后续再改收藏状态时,弹层里的对象可能不是最新引用,所以代码里才需要收藏后重新find一次。正式项目里也可以只保存previewWallpaperId,然后用计算逻辑拿到最新对象。两种方式都能用,关键是要知道自己在处理的是“当前对象”还是“当前对象的 id”。
网格里的视觉实现也挺适合教学。它没有引入真实图片资源,而是用colors.map()生成渐变色,再用 emoji 做中心视觉符号。这样读者不用关心图片加载、缓存、占位图这些额外问题,可以把注意力放在状态和交互上。对于教程示例来说,这是一个挺合理的取舍。
搜索框的searchText和分类条的activeCategory都会影响同一个网格结果,这也是页面最主要的响应链路:输入变化或分类变化后,getFilteredWallpapers()重新计算,Grid 重新渲染。这里没有额外保存“筛选后的列表”,避免了主数据和派生列表不同步的问题。
如果继续往真实壁纸应用扩展,我会优先做三件事。第一,给空结果补一个空状态,不然搜索不到内容时页面会突然变空。第二,把toggleFavorite()和setWallpaper()改成不可变对象返回,减少深层状态更新的风险。第三,把分类列表从写死数组改成从壁纸数据或接口里生成,避免新增分类时忘记同步categories。
这个案例真正值得学的是内容型页面的状态分层:wallpapers管内容,activeCategory和searchText管筛选,previewWallpaper管局部弹层,isFavorite和isSet管用户操作结果。层级清楚之后,壁纸页就不只是一个 Grid,而是一条完整的浏览、预览、决策链路。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。
// 壁纸切换中心: 手机壁纸切换 - 分类浏览、收藏、设置壁纸模拟interfaceWallpaperItem{id:numbertitle:stringcategory:stringemoji:stringcolors:string[]isFavorite:booleanisSet:booleandownloads:number}@Entry@Componentstruct WallpaperSwitchCenter{@Statewallpapers:WallpaperItem[]=[{id:1,title:'极光之夜',category:'自然',emoji:'🌌',colors:['#0D1B2A','#1B4F72','#2ECC71'],isFavorite:true,isSet:true,downloads:12400},{id:2,title:'落日余晖',category:'自然',emoji:'🌅',colors:['#FF6B35','#FF9500','#FFD700'],isFavorite:false,isSet:false,downloads:9800},{id:3,title:'霓虹城市',category:'城市',emoji:'🌃',colors:['#1A1A2E','#16213E','#E94560'],isFavorite:true,isSet:false,downloads:18600},{id:4,title:'雪山清晨',category:'自然',emoji:'🏔️',colors:['#E8F4FD','#AED6F1','#5DADE2'],isFavorite:false,isSet:false,downloads:7200},{id:5,title:'樱花飘落',category:'植物',emoji:'🌸',colors:['#FADADD','#F8BBD9','#F48FB1'],isFavorite:false,isSet:false,downloads:15300},{id:6,title:'深海魅影',category:'自然',emoji:'🌊',colors:['#001F3F','#0074D9','#7FDBFF'],isFavorite:true,isSet:false,downloads:11200},{id:7,title:'简约几何',category:'抽象',emoji:'🔷',colors:['#F5F5F5','#E0E0E0','#007AFF'],isFavorite:false,isSet:false,downloads:6800},{id:8,title:'赛博朋克',category:'抽象',emoji:'⚡',colors:['#0D0D0D','#FF0080','#00FFFF'],isFavorite:false,isSet:false,downloads:22100},{id:9,title:'秋日枫林',category:'自然',emoji:'🍁',colors:['#8B1A1A','#CC5500','#FFB300'],isFavorite:false,isSet:false,downloads:8900},{id:10,title:'粉色梦幻',category:'抽象',emoji:'💕',colors:['#FF9A9E','#FECFEF','#FF6B9D'],isFavorite:false,isSet:false,downloads:13700},]@StateactiveCategory:string='全部'@StatepreviewWallpaper:WallpaperItem|null=null@StatesearchText:string=''privatecategories:string[]=['全部','自然','城市','植物','抽象']getFilteredWallpapers():WallpaperItem[]{letlist=this.wallpapersif(this.activeCategory!=='全部'){list=list.filter((w:WallpaperItem)=>w.category===this.activeCategory)}if(this.searchText){list=list.filter((w:WallpaperItem)=>w.title.includes(this.searchText))}returnlist}toggleFavorite(id:number){this.wallpapers=this.wallpapers.map((w)=>{if(w.id===id){w.isFavorite=!w.isFavorite}returnw})}setWallpaper(id:number){this.wallpapers=this.wallpapers.map((w:WallpaperItem)=>{w.isSet=(w.id===id);returnw})this.previewWallpaper=null}build(){Column(){// 顶部Text('壁纸').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#1A1A1A').padding({left:20,right:20,top:20,bottom:8}).alignSelf(ItemAlign.Start)// 搜索Row(){Text('🔍').fontSize(14)TextInput({placeholder:'搜索壁纸...',text:this.searchText}).layoutWeight(1).height(36).backgroundColor('transparent').fontSize(13).margin({left:8}).onChange((v:string)=>{this.searchText=v})}.height(44).backgroundColor('#F2F2F7').borderRadius(22).padding({left:14,right:14}).margin({left:20,right:20,bottom:12})// 分类筛选Scroll(){Row({space:8}){ForEach(this.categories,(cat:string)=>{Text(cat).fontSize(13).fontColor(this.activeCategory===cat?'#FFFFFF':'#666666').backgroundColor(this.activeCategory===cat?'#1A1A1A':'#F2F2F7').borderRadius(16).padding({left:14,right:14,top:7,bottom:7}).onClick(()=>{this.activeCategory=cat})})}.padding({left:20,right:20})}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).margin({bottom:16})// 壁纸网格Scroll(){Grid(){ForEach(this.getFilteredWallpapers(),(wp:WallpaperItem)=>{GridItem(){Stack(){// 渐变背景代替图片Column().width('100%').height('100%').linearGradient({angle:135,colors:wp.colors.map((c:string,i:number)=>[c,i/(wp.colors.length-1)]as[ResourceColor,number])}).borderRadius(12)// 中心emojiText(wp.emoji).fontSize(36)// 底部信息Column(){Blank()Row(){Column(){Text(wp.title).fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)Text(\`⬇ \${(wp.downloads/1000).toFixed(1)}k\`).fontSize(10).fontColor('#FFFFFF80').margin({top:2})}.alignItems(HorizontalAlign.Start).layoutWeight(1)Text(wp.isFavorite?'❤️':'🤍').fontSize(16).onClick(()=>this.toggleFavorite(wp.id))}.width('100%').padding({left:8,right:8,bottom:8})}.width('100%').height('100%')// 当前使用标识if(wp.isSet){Text('当前').fontSize(10).fontColor('#FFFFFF').backgroundColor('#007AFF').borderRadius(8).padding({left:6,right:6,top:3,bottom:3}).position({x:8,y:8})}}.width('100%').height(160).borderRadius(12).clip(true).onClick(()=>{this.previewWallpaper=wp})}})}.columnsTemplate('1fr 1fr').columnsGap(12).rowsGap(12).padding({left:16,right:16,bottom:24})}.layoutWeight(1).scrollBar(BarState.Off)// 预览弹窗if(this.previewWallpaper!==null){Stack(){Column().width('100%').height('100%').backgroundColor('#000000CC').onClick(()=>{this.previewWallpaper=null})Column({space:16}){// 预览Stack(){Column().width(200).height(300).linearGradient({angle:135,colors:this.previewWallpaper!.colors.map((c:string,i:number)=>[c,i/(this.previewWallpaper!.colors.length-1)]as[ResourceColor,number])}).borderRadius(20)Text(this.previewWallpaper!.emoji).fontSize(60)}.width(200).height(300).shadow({radius:20,color:'#00000060',offsetX:0,offsetY:8})Text(this.previewWallpaper!.title).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')Text(\`\${this.previewWallpaper!.category}· ⬇ \${(this.previewWallpaper!.downloads/1000).toFixed(1)}k\`).fontSize(12).fontColor('#FFFFFF80')Row({space:12}){Button(this.previewWallpaper!.isFavorite?'❤️ 已收藏':'🤍 收藏').fontSize(14).fontColor('#FFFFFF').backgroundColor('#FFFFFF30').borderRadius(22).height(44).padding({left:20,right:20}).onClick(()=>{this.toggleFavorite(this.previewWallpaper!.id)this.previewWallpaper=this.wallpapers.find(w=>w.id===this.previewWallpaper!.id)??null})Button(this.previewWallpaper!.isSet?'✓ 已使用':'设为壁纸').fontSize(14).fontColor('#1A1A1A').backgroundColor('#FFFFFF').borderRadius(22).height(44).padding({left:20,right:20}).onClick(()=>{if(this.previewWallpaper)this.setWallpaper(this.previewWallpaper.id)})}}.alignItems(HorizontalAlign.Center)}.width('100%').height('100%').position({x:0,y:0})}}.width('100%').height('100%').backgroundColor('#FFFFFF')}}最后总结
壁纸切换中心 这个案例并不靠复杂技巧取胜,它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后,你再去写同类型功能,速度会明显快很多。