免费电脑ktv点歌系统(首发!点歌系统也有鸿蒙版了?老王抢先体验了一把)

大家好,我是老王~

鸿蒙前段时间新出了方舟开发框架,声明式开发模式,和react就很相似。此项目基于ArkUI 3.0开发,算是一个比较完善的系统。

在做这个项目过程中遇到了很多问题,能解决的都解决了,不能解决的会在此提出,后面框架升级解决后会再次更新。

接下来将从这几个方面讲解开发的系统,项目目录、开发过程、还有最主要的组件相关内容等。

一、实现效果二、项目目录三、组件实现组件
  • 其实很简单,使用@Component修饰的结构体都具有组件化能力
组件的各种修饰器
  • @Entry:如果你想让该组件作为你这个页面的默认入口就需要使用这个修饰器
  • @Preview: 可以在DevEco的PC预览上进行单组件预览
  • @Builder: 在一个自定义组件内快速生成多个布局内容,我觉得这个很鸡肋,直接使用自定义组件也是可以的
  • @Extend:这个组件比较实用,如果想覆盖自定义组件的一些属性,直接使用这个组件修饰即可修改
组件定义和使用
  • 新建一个Components文件夹,文件夹中放各种组件
  • 组件需要使用es6的import导入、export导出

// 组件定义 @Component export struct Component { build() { Text('我的第一个组件') } } // 组件使用 import {Component} from '你的文件路径' Component()组件的数据通信

  • 父组件传值到子组件,父组件直接传值过去即可,如:Component({value: '传递的值'}),子组件接收的话,需要定义一个value属性接收
  • 如果更改数据后需要页面同步更改需要使用@States修饰进行修饰
  • 父子组件双向通信,父组件需要定义value属性,并使用@States修饰器修饰,子组件定义value属性接收,同时使用@Link修饰器修饰,这样子组件数据修改后父组件会同步修改
  • 子组件调用父组件方法,文档中并没有相关说明,但是如果想要调用父组件方法的话可以先双向通信,然后父组件中使用@Watch修饰器监听属性变化,属性一旦变化执行相应方法
四、项目开发主入口
  • 主入口设定了一个当前展示页面的currentId,点击底部nav时currentId会变化,根据currentId展示不同的页面。
  • 数据改变页面不会刷新,如果想要页面进行刷新,需要给依赖的数据增加装饰器@State,同时@State修饰的数据对组件显隐有一定作用
  • 数据超出限定范围,是无法滚动的,需要给超出组件外层包裹Scroll组件
  • 主要代码

@Entry @Component struct Index { @State currentId: number = 1 build() { Column() { Scroll() { Column() { if (this.currentId === 1) { // 点歌页面 ChooseSong() } else if (this.currentId === 2) { // 遥控页面 RemoteControl() } else { // 我的页面 Home() } }.width('100%') }.height('91%').scrollBarWidth(0) // 底部nav Column() { MyBottomNav({ currentId: $currentId }) }.width('100%').height('9%') }.width('100%').height('100%') } }点歌页面

  • ets框架暂时未出输入框组件,暂时采用布局写的假输入框,等出了输入框组件再行替换
  • 搜索框组件点击事件在父组件中定义,isClick用@Link修饰可实现数据双向绑定,父组件通过监听isClick的变化来触发点击事件
  • prompt.showToastt弹窗事件,ets不支持,使用会报错,添加至歌曲列表的弹窗事件支持后会加入进去。
  • 这个页面有5个组件,搜索框组件、menu组件、title组件、歌曲推荐组件、歌曲列表组件
  • 代码

// 主入口 @Entry @Component export struct ChooseSong { @State currentId: number = 1 @State @Watch("clickHandle") isClick: Boolean = false @State searchValue:string = '' clickHandle() { router.push({ uri: 'pages/search' }) } build() { Column() { // 搜索框 Flex({ justifyContent: FlexAlign.Center }) { MySearch({isClick: $isClick, searchValue: $searchValue}) }.margin({ top: 15 }).width('100%') // 轮播图 Swiper() { ForEach(bannerImage, (item) => { Image(item.url).width('100%').height('100%').border({ radius: 10 }) }, item => item.id) } .width('92%') .height(150) .margin({ top: 15 }) .index(1) .autoPlay(true) // 子menu Flex({ justifyContent: FlexAlign.Center }) { MyMenu({menuList: songMenu}) }.margin({ top: 15 }).width('100%') // 热门推荐 Column() { MyTitle({ title: '热门推荐' }) SongSheet({ dataList: songRecommend, isShowAll: true }) }.margin({ top: 15 }).width('100%') // 新歌速递 Column() { MyTitle({ title: '新歌速递' }) SongList({ dataList: songData }) }.margin({ top: 30 }).width('100%') }.width('100%') } } // 输入框组件 @Component export struct MySearch { @Link isClick: Boolean @Link searchValue: string build() { Flex({alignItems: ItemAlign.Center}) { Image($r('app.media.search')).width(24).height(24).margin({left: 10, right: 10}) Text(this.searchValue ? this.searchValue : '搜索歌曲、歌手').fontSize(16).fontColor(this.searchValue ? '#000' : '#999') }.width('92%').height(40).backgroundColor('#eee').border({radius: 10}).onClick(() => { this.isClick = !this.isClick }) } } // 分类menu组件 @Component export struct MyMenu { @State menuList: object[] = [] @State width: string = '25%' @State height: string | number = 80 @State isShowBorder: boolean = false build() { Flex({ alignItems: ItemAlign.Center, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) { ForEach(this.menuList, (item) => { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Image(item.icon).width(40).height(40).margin({ top: 6, bottom: 6 }) Text(item.name).fontSize(18).fontColor('#000') } .width(this.width) .height(this.height) .backgroundColor(this.isShowBorder ? '#fff' : '') .border({ radius: 15 }) .margin({ bottom: 20 }) .onClick(() => { if (item.url) { router.push({ uri: item.url, params: item.params }) } }) }, item => item.id) }.width('92%').height(100) } } // 标题组件 @Component export struct MyTitle { @State title: string = '热门推荐' build() { Flex({justifyContent: FlexAlign.Center}) { Text('——').fontSize(20).fontColor($r('app.color.base')) Text(this.title).fontColor($r('app.color.base')).fontSize(20).margin({left: 15, right: 15}) Text('——').fontSize(20).fontColor($r('app.color.base')) } } } // 热门推荐组件 @Component export struct SongSheet { @State dataList: object[] = [] @State isShowAll: boolean = false build() { Flex({ alignItems: ItemAlign.Center, wrap: FlexWrap.Wrap }) { ForEach(this.dataList, (item) => { Column() { Image(item.img).width(80).height(80).margin({ top: 6, bottom: 6 }).border({ radius: 5 }) Text(item.name).fontSize(16).fontColor('#000') }.width('33%').margin({ top: 10 }).onClick(() => { router.push({ uri: 'pages/search', params: { searchValue: item.name, isShowAll: this.isShowAll } }) }) }, item => item.id) }.width('92%').margin({ top: 10 }) } } // 歌曲列表组件 @Component export struct SongList { @State dataList: song[] = [] setDataList(id) { console.log(id) this.dataList = this.dataList.map(item => { return item.id === id ? {...item, star: !item.star} : item }) } build() { Column() { ForEach(this.dataList, (item) => { Flex({ alignItems: ItemAlign.Center }) { Column() { Text(item.name).fontSize(16).fontColor('#000') Text(item.author).fontSize(14).fontColor('#666').margin({ top: 3 }) }.width('66%').alignItems(HorizontalAlign.Start).margin({ left: '5%' }) Flex() { Image($r('app.media.xz')).width(30).height(30).margin({ right: 8 }).onClick(() => { console.log('加入列表') }) Image(item.star ? $r('app.media.scY') : $r('app.media.scN')).width(30).height(30).onClick(() => { this.setDataList(item.id) }) }.width('23%') } .width('100%') .border({ width: item.id % 2 ? 1 : 0, color: '#eee' }) .padding({ top: 10, bottom: 10 }) .onClick(() => { item.star = !item.star }) }, item => item.id) }.width('100%').margin({ top: 20 }) } }遥控页面

  • 增减组件主要是有个用@Link修饰的value属性,记录当前的value值,当value值改变会直接改变父组件的值
  • 使用prompt.showToast弹窗显示当前的音量,编辑器底部报错,这个应该是官方问题。等解决了再把项目改下
  • 代码

@Entry @Component export struct RemoteControl { @State @Watch('changeVolume') volume: number = 10 // 音量 @State @Watch('changeTone') tone: number = 10 // 音调 changeVolume() { prompt.showToast({ message: `当前音量为${this.volume}`, duration: 2000, }); } changeTone() { console.log(`当前音调为${this.tone}`) } build() { Column() { // 音量开关 Column() { Column() { Flex({ justifyContent: FlexAlign.Center }) { Flex({ alignItems: ItemAlign.Center }) { Image($r("app.media.yf")).width(30).height(30).margin({ right: 5 }) Text('音乐音量').fontSize(20) } MySwitch({value: $volume}) } Flex({ justifyContent: FlexAlign.Center }) { Flex({ alignItems: ItemAlign.Center }) { Image($r("app.media.tj")).width(30).height(30).margin({ right: 5 }) Text('升降调').fontSize(20) } MySwitch({value: $tone}) }.margin({ top: 40 }) }.padding({ left: 40, right: 40, top: 30, bottom: 50 }) Blank().width('100%').height(2).backgroundColor('#999') }.margin({ top: 15 }).width('100%') Flex({justifyContent: FlexAlign.Center}) { ForEach(menuList, item => { Column() { Flex({justifyContent:FlexAlign.Center, alignItems: ItemAlign.Center}) { Image(item.icon).width(30).height(30) }.width(60).height(60).border({radius: 30}).margin({bottom: 10}).linearGradient({ angle: 0, direction: GradientDirection.Bottom, colors: item.color }) Text(item.name).fontSize(16) }.width('23%') }, item => item.id) }.width('100%').margin({top: 50}) // 轮播图 Swiper() { ForEach(bannerImage, (item) => { Image(item.url).width('100%').height('100%').border({ radius: 10 }) }, item => item.id) } .width('92%') .height(150) .margin({ top: 100 }) .index(1) .autoPlay(true) }.width('100%') } } // 增加减少开关 @Component export struct MySwitch { @Link value: number build() { Flex() { // 减少 Button({ type: ButtonType.Circle, stateEffect: true }) { Text('-').fontSize(40).fontColor('#fff').position({ y: -5, x: 0 }) } .width(45) .height(45) .backgroundColor('#005bea') .margin({ left: 2, top: 2 }) .onClick(() => { this.value -= 1 }) // 增加 Button({ type: ButtonType.Circle, stateEffect: true }) { Text('+').fontSize(40).fontColor('#fff') } .width(45) .height(45) .margin({ left: 18, top: 2 }) .backgroundColor('#fa71cd') .onClick(() => { this.value += 1 }) }.border({ width: 2, color: '#999', radius: 38 }).width(128).height(55) } }我的页面

  • 这个页面有两个组件,个人信息卡片组件,menu组件
  • 使用帮助和意见反馈是同一个页面,通过使用路由传值方式来判断进入的属哪个页面router.push({ uri: item.url,params: item.params})
  • 使用router.getParams()方法获取路由地址里面传递的params的值
  • 代码

@Entry @Component export struct Home { build() { Column() { Image($r('app.media.bg')).width('100%').height(230) // 个人信息卡片 PerInfo() // 子menu Flex({ justifyContent: FlexAlign.Center }) { MyMenu({menuList: homeMenuList, width: '45%', height: 120, isShowBorder: true}) }.margin({ top: -60 }).width('90%') }.width('100%').height('100%').backgroundColor('#eee') } } // 个人信息卡片 @Component export struct PerInfo { build() { Column() { Column() { Image($r('app.media.tx')).width(80).height(80).border({radius: 40, width: 3, color: '#999'}) Text('石 凡').fontSize(20).margin({top: 10}).fontColor($r('app.color.base')).fontWeight(600) }.margin({top: -40}) Flex() { ForEach(homeInfoList, item => { Flex({direction: FlexDirection.Column, alignItems: ItemAlign.Center}) { Text(item.value.toString()).fontSize(22).fontColor($r('app.color.base')).fontWeight(600) Text(item.name).fontSize(18).margin({top: 8}) }.width('33.3%') }, item=>item.id) }.margin({top: -5}) }.width('86%').backgroundColor('#fff').height(200).margin({top: -120}).border({radius: 10}) } }搜索页

  • 搜索框组件点击事件在父组件中定义,isClick用@Link修饰可实现数据双向绑定,父组件通过监听isClick的变化来触发点击事件
  • 通过输入框是否有值来判断是否展示热门搜索
  • 通过监听输入框的值的变化来过滤歌曲库,展示搜索过后的歌曲
  • 代码

@Entry @Component struct SearchPage { @State @Watch("clickHandle") isClick: boolean = false @State @Watch("changeValue") searchValue: string = '' @State isShowAll: boolean = false dataList: object[] = [] setDataList() { if (this.isShowAll) { this.dataList = songData.sort(item => { return Math.random() > 0.5 ? -1 : 1 }) return false; } this.dataList = songData.filter(item => { console.log(item.author.indexOf(this.searchValue) + item.name.indexOf(this.searchValue) + '') return (item.author.indexOf(this.searchValue) !== -1 || item.name.indexOf(this.searchValue) !== -1) ? item : '' }) } changeValue() { this.setDataList() } aboutToAppear() { this.searchValue = router.getParams() ? router.getParams().searchValue : '' this.isShowAll = router.getParams() ? router.getParams().isShowAll : false this.setDataList() } clickHandle() { console.log('弹出输入键盘') } build() { Scroll() { Column() { // 搜索框 Flex({ justifyContent: FlexAlign.Center }) { MySearch({ isClick: $isClick, searchValue: $searchValue }) }.margin({ top: 15 }).width('100%') if (this.searchValue || this.isShowAll) { Column() { MyTitle({ title: '搜索结果' }) SongList({ dataList: this.dataList }) }.margin({ top: 30 }).width('100%') } else { // 热门搜索 Flex({ direction: FlexDirection.Column }) { Text('热门搜索').fontSize(16).fontColor('#999') BreadBlock({searchValue: $searchValue}) }.width('92%').margin({ top: 15 }) } } .width('100%') } } } // 热门搜索 @Component export struct BreadBlock { @Link searchValue: string build() { Flex({ wrap: FlexWrap.Wrap }) { ForEach(hotSearchList, item => { Text(item) .fontSize(16) .padding({ left: 15, right: 15, top: 16 }) .onClick(() => { this.searchValue = item }) }, item => item) } } }

方舟开发框架现在还不是很完善,开发完整大型项目还是很费力,但是一些小dome还是可以开发的。希望框架越来越完善,毕竟声明式开发是未来的开发的主流趋势。

最后附上git地址:https://gitee.com/shi-fan-a/song-ordering-system


——————

电脑ktv点歌系统

原创:老王丨【公众号:鸿蒙开发者老王】华为认证讲师 / 腾讯认证讲师 / 鸿蒙开发先行者

您可以还会对下面的文章感兴趣

最新评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

使用微信扫描二维码后

点击右上角发送给好友