Change
《仿盒马》app开发技术分享-- 商品详情页(10)

技术栈

Appgallery connect

开发准备

上一节我们实现了自定义标题栏和商品详情的数据接收,我们已经拿到了想要的数据,这一节我们要丰富商品详情页的内容。商品详情页面我们需要展示的是商品的各个属性参数、商品的图片、商品规格、活动详情等

功能分析

商品详情页面的结构需要我们去用比较多的布局去处理,首先因为商品详情页面对的数据足够多,需要他能够实现滚动查看信息,然后我们需要在底部固定加入购物车和立即购买按钮,并且我们加购之后,我们也要在页面中即使响应购物车中的商品数。同时给到用户便捷跳转到购物车的按钮

代码实现

我们先进行数据的填充,因为上一节我们已经接收到数据,所以我们直接吧数据打印到text上,对着数据进行填充,同时还能帮我们暂时丰富一下页面内容,查看滑动的效果,页面完善之后我们再去删掉即可

Text(JSON.stringify(this.productParams))

          .fontColor(Color.Black)

然后我们根据设计的样式进行数据填充,要注意滚动和底部布局的固定,挑选合适的布局容器

Stack({alignContent:Alignment.Bottom}){

  Scroll(this.scroller){

      Column() {

        CommonTopBar({ title: "商品详情", alpha: 0, titleAlignment: TextAlign.Center ,backButton:true})

        Image(this.productParams.url)

          .width('100%')

          .height(300)

        Text(JSON.stringify(this.productParams))

          .fontColor(Color.Black)

        Column({space:10}){

          Row(){

            if (this.productParams.promotion_spread_price>0){

              Text(){

                Span("¥")

                  .fontSize(14)

                  .fontColor(Color.Red)

                Span(this.productParams.promotion_spread_price+"")

                  .fontSize(20)

                  .fontColor(Color.Red)

              }

            }else {

              Text(){

                Span("¥")

                  .fontSize(14)

                  .fontColor(Color.Red)

                Span(this.productParams.price+"")

                  .fontSize(20)

                  .fontColor(Color.Red)

              }

            }

            Text("¥"+this.productParams.original_price+"")

              .fontColor('#999')

              .decoration({

                type: TextDecorationType.LineThrough,

                color: Color.Gray

              })

              .fontSize(16)

              .margin({left:10})

            if (this.productParams.promotion_spread_price>0){

              Row(){

                Text("每件立减"+(this.productParams.price-this.productParams.promotion_spread_price)+"元")

                  .fontSize(14)

                  .borderRadius(20)

                  .backgroundColor("#FB424C")

                  .padding(3)

                  .textAlign(TextAlign.Center)

                Text("每人限购"+this.productParams.max_loop_amount+"件")

                  .margin({left:5})

                  .fontSize(14)

                  .borderRadius(20)

                  .backgroundColor("#FB424C")

                  .padding(3)

                  .textAlign(TextAlign.Center)

              }

              .padding({top:2,bottom:2,left:10})

            }

          }

          .padding(10)

          if (this.productParams.promotion_spread_price>0){

            Text(this.productParams.endTime)

              .fontSize(14)

              .borderRadius(20)

              .backgroundColor("#FB424C")

              .padding(3)

              .margin({left:10})

              .textAlign(TextAlign.Center)

          }

          Text(this.productParams.name)

            .fontSize(20)

            .fontColor(Color.Black)

            .margin({left:10})

            .fontWeight(FontWeight.Bold)

          Text(this.productParams.text_message)

            .fontSize(14)

            .fontColor(Color.Black)

            .margin({left:10})

          Row(){

            Text()

            Text("销量 "+this.productParams.sales_volume)

              .fontSize(14)

              .fontColor(Color.Black)

          }

          .padding(10)

          .width('100%')

          .justifyContent(FlexAlign.SpaceBetween)

          Divider().width('100%')

            .height(5).backgroundColor("#f7f7f7")

          Row(){

            Text("发货")

              .fontColor(Color.Gray)

              .fontSize(14)

            Text(this.productParams.delivery_time+"")

              .fontSize(14)

              .margin({left:20})

              .fontColor(Color.Black)

          }

          .padding(10)

          .width('100%')

          .justifyContent(FlexAlign.Start)

          Divider().width('100%')

            .height(5).backgroundColor("#f7f7f7")

          Row(){

            Text("参数")

              .fontColor(Color.Gray)

              .fontSize(14)

            Text("储藏条件:")

              .margin({left:20})

              .fontSize(14)

              .fontColor(Color.Black)

            Text(this.productParams.parameter)

              .fontSize(14)

              .fontColor(Color.Black)

          }

          .padding(10)

          .width('100%')

          .justifyContent(FlexAlign.Start)

          Divider().width('100%')

            .height(5).backgroundColor("#f7f7f7")

          Row(){

            Text("规格")

              .fontColor(Color.Gray)

              .fontSize(14)

            Column(){

              Text("请选择规格")

            }

          }

          .padding(10)

          .width('100%')

          .justifyContent(FlexAlign.Start)

          Divider().width('100%')

            .height(5).backgroundColor("#f7f7f7")

        }

        .alignItems(HorizontalAlign.Start)

      }

      .alignItems(HorizontalAlign.Start)

      .backgroundColor(Color.White)

  }

  .padding({bottom:80})

  .height('100%')

  .width('100%')

  Row(){

    Image($r('app.media.product_details_cart'))

      .width(35)

      .height(35)

      .objectFit(ImageFit.Contain)

    Blank()

    Text("加入购物车")

      .padding(10)

      .width(100)

      .textAlign(TextAlign.Center)

      .backgroundColor("#FCDB29")

      .fontColor(Color.White)

      .borderRadius({topLeft:15,bottomLeft:15})

    Text(" 立即购买 ")

      .padding(10)

      .textAlign(TextAlign.Center)

      .width(100)

      .backgroundColor(Color.Red)

      .fontColor(Color.White)

      .borderRadius({topRight:15,bottomRight:15})

  }

  .padding(15)

  .justifyContent(FlexAlign.SpaceBetween)

  .width('100%')

  .backgroundColor(Color.White)

}

.backgroundColor(Color.White)

到这里我们的商品详情页面的内容已经比较完善了

Change
《仿盒马》app开发技术分享-- 自定义标题栏&商品详情初探(9)

技术栈

Appgallery connect

开发准备

上一节我们实现了顶部toolbar的地址选择,会员码展示,首页的静态页面就先告一段落,这节我们来实现商品列表item的点击传值、自定义标题栏。

功能分析

1.自定义标题栏

当我们进入二级三级页面的时候,就需要向用户介绍我们当前的页面信息,标题栏很好的实现了这个效果,并且进入的页面级别过多,也要给用户一个可点击的退出按钮。当然了,有些页面是不需要有返回按钮的,这里我们还要兼顾通用性。

2.页面间传值

页面之前的数据传递,是app中比较常见也是比较重要的知识点,这里我们通过点击列表的条目进行数据的传递,然后在详情页进行数据的接收

代码实现

自定义标题栏

import router from '@ohos.router'

@Component

export struct CommonTopBar {

@Prop title: string

@Prop alpha: number

private titleAlignment: TextAlign = TextAlign.Center

private backButton: boolean = true

private onBackClick?: () => void

build() {

Column() {

  Blank()

    .backgroundColor(Color.Red)

    .opacity(this.alpha)

  Stack({ alignContent: Alignment.Start }) {

    Stack()

      .height(50)

      .width("100%")

      .opacity(this.alpha)

      .backgroundColor(Color.Red)

    Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {

      Text(this.title)

        .flexGrow(1)

        .textAlign(this.titleAlignment)

        .fontSize(18)

        .fontColor(Color.Black)

        .align(Alignment.Center)

        .maxLines(1)

        .textOverflow({ overflow: TextOverflow.Ellipsis })

    }

    .height(50)

    .margin({ left: 50, right: 50 })

    .alignSelf(ItemAlign.Center)

    if (this.backButton) {

        Stack() {

          Image($r('app.media.ic_back'))

            .height(16)

            .width(12)

            .objectFit(ImageFit.Contain)

            .align(Alignment.Center)

        }

        .onClick(() => {

          this.onBackClick?.()

          router.back();

        })

        .height(50)

        .width(50)

    }

  }

  .height(50)

  .width("100%")

  Divider().strokeWidth(0.5).color("#E6E6E6")

}.backgroundColor(Color.White)

.height(51)

}

}

在标题栏中我们使用了一些逻辑判断,并且设置标题是外部传入的,而且还预留了一个事件的回调,这能让我们的标题栏更加的灵活

页面间传值

首先我们需要创建一个商品详情页的页面,然后把我们的自定义标题栏引入进去

import { CommonTopBar } from '../widget/CommonTopBar';

@Entry

@Component

struct ProductDetailsPage {

build() {

Column() {

  CommonTopBar({ title: "商品详情", alpha: 0, titleAlignment: TextAlign.Center ,backButton:true})

}

.height('100%')

.width('100%')

}

}

然后在商品流的点击事件里使用router

.onClick(() => {

        router.pushUrl({

          url: 'pages/component/ProductDetailsPage',

          params: item

        }, (err) => {

          if (err) {

            console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);

            return;

          }

          console.info('Invoke pushUrl succeeded.');

        });

      })

      

这里我们把整个item的信息都传递过去,方便我们使用

接收

@State receivedParams: HomeProductList = {} as HomeProductList;

aboutToAppear(): void {

let order= router.getParams() as HomeProductList;

console.info('Received params:',order);

}

在页面上我们先展示出来

Text(JSON.stringify(this.receivedParams))

    .fontColor(Color.Black)

到这里我们就实现了本节的内容了,下一节我们将要丰富商品详情页的内容

Change
《仿盒马》app开发技术分享-- 首页地址选择&会员码(8)

技术栈

Appgallery connect

开发准备

上一节我们实现了商品流标的创建,数据的填充和展示,并且在商品信息表中添加了许多我们后去需要使用到的参数。让我们的首页功能更加的丰富,截至目前首页板块可以说是完成了百分之五十了,跟展示有关的基本都已完成,接下来就是我们对业务逻辑的完善,当然了我们的首页内容还缺少很多,这一节我们来把顶部toolbar的地址选择,会员码展示实现一下。

功能分析

1.地址选择

地址选择我们需要实现的是省市区街道的选择,当我们点击街道信息后,根据区域的不同,我们可能会调整首页相应的活动板块修改,以及不同模块的展示,比如我们的新人领券活动,我们仅在A区域开展活动,当我们切换的B区域就会关闭相应的功能展示。同时我们下次登陆需要加载上一次选中的地址,要实现这个功能我们还需要把地址信息存储到本地。

2.会员码

会员码这个就比较的简单,我们只需要把条形码跟二维码结合用户的id生成,(因为暂时没有登陆功能,所以我们要模拟一下)在进入页面的时候把条形码加载到页面上即可。

代码实现

地址选择

因为鸿蒙中是自带这个组建的,所以我们直接在点击事件中去调用即可

let districtSelectOptions: sceneMap.DistrictSelectOptions= {

      countryCode: "CN",

      subWindowEnabled: false

    };

    sceneMap.selectDistrict(getContext(this), districtSelectOptions).then((data) => {

      if (data.districts.length>5){

        this.locationName=data.districts[5].name!

      }else {

        this.locationName=data.districts[4].name!

      }

      console.info("SelectDistrict", "Succeeded in selecting district."+data);

    }).catch((err: BusinessError) => {

});

然后我们执行一下代码拉起地区选择的页面

然后我们实现会员码页面,这个页面就是一个一维码跟二维码的展示

因为系统不支持直接生成一维码,所以我们用到scankit ,二维码用原生

import { scanCore, generateBarcode } from '@kit.ScanKit';

import { BusinessError } from '@kit.BasicServicesKit';

import { image } from '@kit.ImageKit';

@Entry

@Component

struct QRCodePage {

@State content: string = '1122334455';

@State pixelMap: image.PixelMap | undefined = undefined

aboutToAppear(): void {

this.pixelMap = undefined;

let options: generateBarcode.CreateOptions = {

  scanType: scanCore.ScanType.CODE39_CODE,

  height:200,

  width: 400

}

try {

  generateBarcode.createBarcode(this.content, options).then((pixelMap: image.PixelMap) => {

    this.pixelMap = pixelMap;

  }).catch((error: BusinessError) => {

  })

} catch (error) {

}

}

build() {

Column() {

  Column(){

    if (this.pixelMap) {

      Image(this.pixelMap).width('90%').height(70).objectFit(ImageFit.Fill)

      QRCode(this.content).color(Color.Black).width('90%').height(140)

        .margin({top:20})

    }

  }

  .width('80%')

  .backgroundColor("#ffffff")

  .borderRadius(10)

  .padding(10)

  .alignItems(HorizontalAlign.Center)

  .justifyContent(FlexAlign.Center)

}

.backgroundColor("#ffeceaea")

.width('100%')

.height('100%')

}

}

这样就实现了对应的内容了

Change
《仿盒马》app开发技术分享-- 首页商品流(7)

技术栈

Appgallery connect

开发准备

上一节我们实现了首页banner模块的功能,现在我们的首页还需要添加商品列表,作为一个购物类应用,商品列表是非常重要的一个模块,所以我们尽量把它设计的足够完善,参数能更好的支持我们后期复杂的逻辑,它需要有图片的展示,适配的优惠券列表,限购,立减,划线价等,但他实际的参数还要更多,因为我们的列表是比较紧凑的,更多的数据需要从点击后的商品详情页展示出来。

代码实现

创建商品表

{

"objectTypeName": "home_product_list",

"fields": [

{"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "goods_list_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},

{"fieldName": "url", "fieldType": "String"},

{"fieldName": "name", "fieldType": "Text"},

{"fieldName": "price", "fieldType": "Double"},

{"fieldName": "original_price", "fieldType": "Double"},

{"fieldName": "amount", "fieldType": "Integer"},

{"fieldName": "text_message", "fieldType": "String"},

{"fieldName": "parameter", "fieldType": "String"},

{"fieldName": "delivery_time", "fieldType": "String"},

{"fieldName": "endTime", "fieldType": "String"},

{"fieldName": "sales_volume", "fieldType": "Integer"},

{"fieldName": "space_id", "fieldType": "Integer"},

{"fieldName": "max_loop_amount", "fieldType": "Integer"},

{"fieldName": "promotion_spread_price", "fieldType": "Double"},

{"fieldName": "coupon_id", "fieldType": "Integer"}

],

"indexes": [

{"indexName": "field1IndexId", "indexList": [{"fieldName":"id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

填充数据

{

"cloudDBZoneName": "default",

"objectTypeName": "home_product_list",

"objects": [

{

  "id": 10,

  "goods_list_id": 1,

  "url": "在线图片链接",

  "name": "红颜草莓",

  "price": 10.5,

  "original_price": 18.5,

  "amount": 10,

  "text_message": "特价",

  "parameter": "冷藏",

  "delivery_time": "付款后24小时内发货",

  "endTime": "直降 | 结束时间2025年5月18日 10:00",

  "sales_volume": 9812,

  "space_id": 10,

  "max_loop_amount": 10,

  "promotion_spread_price": 5,

  "coupon_id": 10

},

{

  "id": 20,

  "goods_list_id": 1,

  "url": "在线图片链接",

  "name": "麒麟瓜",

  "price": 2.8,

  "original_price": 5.9,

  "amount": 1,

  "text_message": "当季新品",

  "parameter": "冷藏",

  "delivery_time": "付款后24小时内发货",

  "endTime": "直降 | 结束时间2025年5月18日 10:00",

  "sales_volume": 9812,

  "space_id": 11,

  "max_loop_amount": 10,

  "promotion_spread_price": 0,

  "coupon_id": 10

}

]

}

我们接下来进行数据的查询

@State homeProduct:HomeProductList[]=[]//商品流数据

  let databaseZone = cloudDatabase.zone('default');

  let home_product=new cloudDatabase.DatabaseQuery(home_product_list);

  let list7 = await databaseZone.query(home_product);

  let json7 = JSON.stringify(list7)

  let data7:HomeProductList[]= JSON.parse(json7)

  this.homeProduct=data7

数据查出完成后,完善商品流的页面

import { HomeProductList } from "../entity/home_product_list"

@Component

@Preview

export struct WaterFlowGoods {

@Link goodsList: Array<HomeProductList>

@State columns: number = 2

build() {

WaterFlow() {

  ForEach(this.goodsList, (item:HomeProductList, index) => {

    FlowItem() {

      Column() {

        Image(item.url)

          .width('100%')

          .aspectRatio(1)

          .objectFit(ImageFit.Cover)

          .borderRadius({topLeft:10,topRight:10})

        Column() {

          Text(item.name)

            .fontSize(16)

            .fontColor('#333')

            .margin({ bottom: 4 })

          Text(item.text_message)

            .fontSize(12)

            .fontColor('#666')

            .margin({ bottom: 8 })

          Text("最高立减"+item.promotion_spread_price)

            .fontSize(12)

            .fontColor('#ffffff')

            .visibility(item.promotion_spread_price>0?Visibility.Visible:Visibility.None)

            .margin({ bottom: 8 })

            .padding({left:5,right:5,top:2,bottom:2})

            .linearGradient({

              angle:90,

              colors: [[0xff0000, 0], [0xff6666, 0.2], [0xff6666, 1]]

            })

          Row(){

            Text("限购")

              .width(40)

              .fontSize(12)

              .borderRadius(20)

              .backgroundColor("#FB424C")

              .padding(3)

              .textAlign(TextAlign.Center)

              Text("每人限购"+item.max_loop_amount+"件")

                .margin({left:5})

                .fontSize(12)

                .fontColor("#FB424C")

          }

          .borderRadius(20)

          .padding({top:2,bottom:2,right:10})

          .backgroundColor("#FEE3E3")

          .visibility(item.amount>0?Visibility.Visible:Visibility.None)

          Row() {

            Text(){

              Span("¥")

                .fontColor(Color.Red)

                .fontSize(14)

              Span(String(item.price))

                .fontSize(16)

                .fontColor(Color.Red)

            }

            Text(String(item.original_price))

              .fontSize(12)

              .fontColor('#999')

              .decoration({

                type: TextDecorationType.LineThrough,

                color: Color.Gray

              })

              .margin({left:10})

             Blank()

            Column() {

              Image($r('app.media.cart'))

                .width(20)

                .height(20)

            }

            .justifyContent(FlexAlign.Center)

            .width(36)

            .height(36)

            .backgroundColor("#ff2bd2fa")

            .borderRadius(18)

            }

            .margin({top:10})

            .width('100%')

            .justifyContent(FlexAlign.SpaceBetween)

        }

        .alignItems(HorizontalAlign.Start)

        .padding(12)

      }

      .backgroundColor(Color.White)

      .borderRadius(12)

      .onClick(() => {

      })

    }

    .margin({ bottom: 12 })

  })

}

.padding(10)

.columnsTemplate('1fr 1fr')

.columnsGap(12)

.onAreaChange((oldVal, newVal) => {

  this.columns = newVal.width > 600 ? 2 : 1

})

}

}

然后在首页调用,传入参数

WaterFlowGoods({goodsList:this.homeProduct})

到这里我们就实现了首页商品列表的内容

Change
《仿盒马》app开发技术分享-- 首页banner(6)

技术栈

Appgallery connect

开发准备

上一篇文章中我们实现了项目端云一体化首页商品活动入口列表,现在我们还差一个banner的模块,banner模块不仅可以用于展示一些信息,还可以在点击之后进行,跳转,弹窗,升级提示,信息提示等作用,我们直接坐的完善一些,因为我们事先在banner表中添加了action,我们通过这个action的值来进行对应的处理,同时通过islogin字段来判断是否需要登陆操作

代码实现

创建banner表

{

"objectTypeName": "home_banner",

"fields": [

{"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "banner_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},

{"fieldName": "url", "fieldType": "String"},

{"fieldName": "is_login", "fieldType": "Boolean"},

{"fieldName": "router", "fieldType": "Boolean"},

{"fieldName": "action_id", "fieldType": "Integer"},

{"fieldName": "action", "fieldType": "String"}

],

"indexes": [

{"indexName": "banner_id", "indexList": [{"fieldName":"banner_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

填充数据

banner

{

"cloudDBZoneName": "default",

"objectTypeName": "home_banner",

"objects": [

{

  "id": 10,

  "banner_id": 1,

  "url": "在线图片链接",

  "is_login": true,

  "router": "",

  "action_id": 10,

  "action": "toast"

},

{

  "id": 20,

  "banner_id": 0,

  "url": "在线图片链接",

  "is_login": false,

  "router": "",

  "action_id": 20,

  "action": "dialog"

}

]

}

由于我们缺少banner相关的内容,所以我们还需要创建一个banner的页面

import { HomeBanner } from "../entity/HomeBanner"

import showToast from "../utils/ToastUtils"

@Component

@Preview

export struct HomeBannerPage {

//数据源

@Link bannerList:HomeBanner[]

//tabs 当前数据源的下标

@State swpIndex:number=1

build() {

Column() {

        Swiper(){

          ForEach(this.bannerList, (item: HomeBanner) => {

            Image(item.url)

              .width('100%')

              .height(130)

              .borderRadius(10)

              .onClick(()=>{

                if (item.action=='toast') {

                  showToast("1111")

                }

                if (item.action=='dialog') {

                }

              })

          })

        }

        .borderRadius(10)

        .loop(true)

        .indicator(true)

        .height(130)

        .onChange((index: number) => {

          this.swpIndex=index+1

        })

}

.padding(10)

.margin({top:10})

}

}

我们先判断是否需要is_login,然后根据action去判断,到这里我们就实现了banner的内容

Change
《仿盒马》app开发技术分享-- 首页活动配置(5)

技术栈

Appgallery connect

开发准备

上一篇文章中我们实现了项目端云一体化首页部分模块动态配置,实现了对模块模块的后端控制显示和隐藏,这能让我们的app更加的灵活,也能应对更多的情况。现在我们来对配置模块进行完善,除了已有的模块以外,我们还有一些banner ,活动入口等模块,这些模块的数据并不多,所以我们也归纳到配置中去实现。并且我们在配置表中添加了一些不同的id,我们只需要根据相对应的id 去查询对应的表就可以了

代码实现

实现横幅海报,商品活动入口

创建海报横幅表

{

"objectTypeName": "home_poster",

"fields": [

{"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "poster_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},

{"fieldName": "url", "fieldType": "String"},

{"fieldName": "router", "fieldType": "String"}

],

"indexes": [

{"indexName": "posterIdIndex", "indexList": [{"fieldName":"poster_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

创建商品活动入口表

{

"objectTypeName": "home_good_center",

"fields": [

{"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "good_left_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},

{"fieldName": "title", "fieldType": "String"},

{"fieldName": "url", "fieldType": "String"}

],

"indexes": [

{"indexName": "goodLeftIdIndex", "indexList": [{"fieldName":"good_left_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

分别填充数据

海报

{

"cloudDBZoneName": "default",

"objectTypeName": "home_poster",

"objects": [

{

  "id": 10,

  "poster_id": 1,

  "url": "在线图片链接",

  "router": "string1"

}

]

}

商品活动入口

{

"cloudDBZoneName": "default",

"objectTypeName": "home_good_center",

"objects": [

{

  "id": 10,

  "good_left_id": 1,

  "title": "生鲜严选",

  "url": "在线图片链接"

},

{

  "id": 20,

  "good_left_id": 1,

  "title": "西购新品",

  "url": "在线图片链接"

},

{

  "id": 30,

  "good_left_id": 1,

  "title": "今日推荐",

  "url": "在线图片链接"

}

]

}

都填充完成后,我们把数据提交到云端,然后进行配置类的同步

接下来我们进行数据查询,因为我们在配置表中添加了id ,所以我们要查询出对应id的活动入口。

@State homeActivity:HomeActivitySetting[]=[]//首页活动配置

@State homeGoodCenter:HomeGoodCenter[]=[]//商品活动入口

let listData3 = await databaseZone.query(condition3);

  let json3 = JSON.stringify(listData3)

  let data3:HomeActivitySetting[]= JSON.parse(json3)

  this.homeActivity=data3

  hilog.error(0x0000, 'testTag', `Failed to query data, code: ${this.homeActivity}`);

  let list5 = await databaseZone.query(home_good);

  home_good.equalTo("good_left_id",data3[0].good_left_id);

  let json5 = JSON.stringify(list5)

  let data5:HomeGoodCenter[]= JSON.parse(json5)

  this.homeGoodCenter=data5

  hilog.error(0x0000, 'testTag', `Failed to query data, code: ${this.homeGoodCenter}`);

然后我们修改一下商品活动入口的内容

import { HomeGoodCenter } from "../entity/HomeGoodCenter"

@Component

@Preview

export struct SpecialColumn {

@Link goodInfo: HomeGoodCenter[]

build() {

Column(){

  List({space:10}){

    ForEach(this.goodInfo,(data:HomeGoodCenter)=>{

      ListItem(){

        Column(){

          Text(data.title)

            .fontSize(16)

            .fontWeight(FontWeight.Bold)

            .fontColor(Color.Black)

          Blank()

          Image(data.url)

            .width('28%')

            .height(90)

            .margin({ bottom: 8 })

            .objectFit(ImageFit.Cover)

        }

        .borderRadius(5)

        .backgroundColor("#ffeedeb8")

        .padding(5)

      }

    })

  }

  .listDirection(Axis.Horizontal)

}

.margin({top:10})

}

}

在首页进行调用

SpecialColumn({goodInfo:this.homeGoodCenter})

到这里我们就实现了活动配置相关的内容

Change
《仿盒马》app开发技术分享-- 首页模块配置(4)

技术栈

Appgallery connect

开发准备

上一篇文章中我们实现了项目端云一体化金刚区活动模块,数据也成功的从云端获取,并且我们跟ScrollBar进行关联,能够让用户直观的查看当前滑动的位置。现在我们开始继续向下,随着我们首页的内容越来越多,我们如果因为某些业务需要进行调整和下线,想隐藏和关掉某些模块,就需要每次在打包的时候进行处理,这很明显会非常的麻烦,现在我们通过一张表来对首页的整个模块进行控制,这样每次做展示的时候去进行查询当前状态来实现想要的效果。

代码实现

首先我们先创建一个表把所有的关联id 以及模块的状态字段定义一下

{

"objectTypeName": "home_activity_setting",

"fields": [

{"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "poster_id", "fieldType": "Integer"},

{"fieldName": "banner_id", "fieldType": "Integer"},

{"fieldName": "good_left_id", "fieldType": "Integer"},

{"fieldName": "good_right_id", "fieldType": "Integer"},

{"fieldName": "goods_list_id", "fieldType": "Integer"},

{"fieldName": "new_people_status", "fieldType": "Boolean"},

{"fieldName": "split_layout_status", "fieldType": "Boolean"},

{"fieldName": "banner_status", "fieldType": "Boolean"},

{"fieldName": "goods_list_status", "fieldType": "Boolean"}

],

"indexes": [

{"indexName": "field1IndexId", "indexList": [{"fieldName":"id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

然后我们创建数据

{

"cloudDBZoneName": "default",

"objectTypeName": "home_activity_setting",

"objects": [

{

  "id": 1,

  "poster_id": 1,

  "banner_id": 1,

  "good_left_id": 1,

  "good_right_id": 1,

  "goods_list_id": 1,

  "new_people_status": true,

  "split_layout_status": true,

  "banner_status": true,

  "goods_list_status": true

}

]

}

现在进行实体类的创建

export class HomeActivitySetting {

id: number;

poster_id: number;

banner_id: number;

good_left_id: number;

good_right_id: number;

goods_list_id: number;

new_people_status: boolean;

split_layout_status: boolean;

banner_status: boolean;

goods_list_status: boolean;

constructor() {

}

getFieldTypeMap():  Map<string, string> {

    let fieldTypeMap = new Map<string, string>();

    fieldTypeMap.set('id', 'Integer');

    fieldTypeMap.set('poster_id', 'Integer');

    fieldTypeMap.set('banner_id', 'Integer');

    fieldTypeMap.set('good_left_id', 'Integer');

    fieldTypeMap.set('good_right_id', 'Integer');

    fieldTypeMap.set('goods_list_id', 'Integer');

    fieldTypeMap.set('new_people_status', 'Boolean');

    fieldTypeMap.set('split_layout_status', 'Boolean');

    fieldTypeMap.set('banner_status', 'Boolean');

    fieldTypeMap.set('goods_list_status', 'Boolean');

    return fieldTypeMap;

}

getClassName(): string {

    return 'home_activity_setting';

}

getPrimaryKeyList(): string[] {

    let primaryKeyList: string[] = [];

    primaryKeyList.push('id');

    return primaryKeyList;

}

getIndexList(): string[] {

    let indexList: string[] = [];

    indexList.push('id');

    return indexList;

}

getEncryptedFieldList(): string[] {

    let encryptedFieldList: string[] = [];

    return encryptedFieldList;

}

setId(id: number): void {

    this.id = id;

}

getId(): number  {

    return this.id;

}

setPoster_id(poster_id: number): void {

    this.poster_id = poster_id;

}

getPoster_id(): number  {

    return this.poster_id;

}

setBanner_id(banner_id: number): void {

    this.banner_id = banner_id;

}

getBanner_id(): number  {

    return this.banner_id;

}

setGood_left_id(good_left_id: number): void {

    this.good_left_id = good_left_id;

}

getGood_left_id(): number  {

    return this.good_left_id;

}

setGood_right_id(good_right_id: number): void {

    this.good_right_id = good_right_id;

}

getGood_right_id(): number  {

    return this.good_right_id;

}

setGoods_list_id(goods_list_id: number): void {

    this.goods_list_id = goods_list_id;

}

getGoods_list_id(): number  {

    return this.goods_list_id;

}

setNew_people_status(new_people_status: boolean): void {

    this.new_people_status = new_people_status;

}

getNew_people_status(): boolean  {

    return this.new_people_status;

}

setSplit_layout_status(split_layout_status: boolean): void {

    this.split_layout_status = split_layout_status;

}

getSplit_layout_status(): boolean  {

    return this.split_layout_status;

}

setBanner_status(banner_status: boolean): void {

    this.banner_status = banner_status;

}

getBanner_status(): boolean  {

    return this.banner_status;

}

setGoods_list_status(goods_list_status: boolean): void {

    this.goods_list_status = goods_list_status;

}

getGoods_list_status(): boolean  {

    return this.goods_list_status;

}

static parseFrom(inputObject: any): HomeActivitySetting {

    let result = new HomeActivitySetting();

    if (!inputObject) {

        return result;

    }

    if (inputObject.id) {

        result.id = inputObject.id;

    }

    if (inputObject.poster_id) {

        result.poster_id = inputObject.poster_id;

    }

    if (inputObject.banner_id) {

        result.banner_id = inputObject.banner_id;

    }

    if (inputObject.good_left_id) {

        result.good_left_id = inputObject.good_left_id;

    }

    if (inputObject.good_right_id) {

        result.good_right_id = inputObject.good_right_id;

    }

    if (inputObject.goods_list_id) {

        result.goods_list_id = inputObject.goods_list_id;

    }

    if (inputObject.new_people_status) {

        result.new_people_status = inputObject.new_people_status;

    }

    if (inputObject.split_layout_status) {

        result.split_layout_status = inputObject.split_layout_status;

    }

    if (inputObject.banner_status) {

        result.banner_status = inputObject.banner_status;

    }

    if (inputObject.goods_list_status) {

        result.goods_list_status = inputObject.goods_list_status;

    }

    return result;

}

}

db类

import { cloudDatabase } from '@kit.CloudFoundationKit';

class home_activity_setting extends cloudDatabase.DatabaseObject {

public id: number;

public poster_id: number;

public banner_id: number;

public good_left_id: number;

public good_right_id: number;

public goods_list_id: number;

public new_people_status: boolean;

public split_layout_status: boolean;

public banner_status: boolean;

public goods_list_status: boolean;

public naturalbase_ClassName(): string {

return 'home_activity_setting';

}

}

export { home_activity_setting };

创建完成之后,记得要在开发工具中把数据提交到云端,我们把数据提交到云端后,可以看到数据提交成功并且表也已经创建完成

进行数据查询

  let databaseZone = cloudDatabase.zone('default');

  let condition3 = new cloudDatabase.DatabaseQuery(home_activity_setting);

let listData3 = await databaseZone.query(condition3);

let json3 = JSON.stringify(listData3)

let data3:HomeActivitySetting[]= JSON.parse(json3)

this.homeActivity=data3

} catch (err) {

hilog.error(0x0000, 'testTag', Failed to query data, code: ${err.code}, message: ${err.message});

}

数据也已经查询成功,接下来我们直接运行程序,可以看到金刚区是在显示中的,接下来我们修改金刚区的状态,再执行一下,可以看到金刚区已经不见了,到这里我们首页模块的显示隐藏配置已经实现了。

Change
《仿盒马》app开发技术分享-- 金刚区(3)

技术栈

Appgallery connect

开发准备

上一篇文章中我们实现了项目端云一体化新人专享券活动模块,数据也成功的从云端获取,现在我们开始继续向下,实现金刚区模块

功能分析

金刚区的实现我们之前已经完成了,但是数据的获取都是本地的静态数据,现在我们要获取云端的数据,实现数据的展示,同时要把滚动跟bar 关联起来,让用户能看到当前滑动到什么位置

代码实现

首先我们进行表、数据、实体、db类的创建

{

"objectTypeName": "split_layout",

"fields": [

{"fieldName": "split_id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "txt", "fieldType": "String"},

{"fieldName": "url", "fieldType": "String"},

{"fieldName": "router", "fieldType": "String"},

{"fieldName": "is_login", "fieldType": "Boolean"},

{"fieldName": "bt_state", "fieldType": "Integer"}

],

"indexes": [

{"indexName": "splitId_Index", "indexList": [{"fieldName":"split_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

数据

{

"cloudDBZoneName": "default",

"objectTypeName": "split_layout",

"objects": [

{

  "split_id": 10,

  "txt": "果蔬肉禽",

  "url": "在线图片链接",

  "router": "string1",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 20,

  "txt": "冷冻水产",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 30,

  "txt": "乳品烘焙",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 40,

  "txt": "粮油面点",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 50,

  "txt": "酒水饮料",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 60,

  "txt": "休闲零食",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 70,

  "txt": "婴宠保健",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 80,

  "txt": "美妆个护",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 90,

  "txt": "纸品清洁",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 101,

  "txt": "百货家电",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 102,

  "txt": "家纺服饰",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

},

{

  "split_id": 201,

  "txt": "跨境免税",

  "url": "在线图片链接",

  "router": "string2",

  "is_login": false,

  "bt_state": 0

}

]

}

db类

import { cloudDatabase } from '@kit.CloudFoundationKit';

class split_layout extends cloudDatabase.DatabaseObject {

public split_id: number;

public txt: string;

public url: string;

public router: string;

public is_login: boolean;

public bt_state: number;

public naturalbase_ClassName(): string {

return 'split_layout';

}

}

export { split_layout };

实体类

/*

  • Copyright (c) Huawei Technologies Co., Ltd. 2020-2023. All rights reserved.

  • Generated by the CloudDB ObjectType compiler. DO NOT EDIT!

*/

export class SplitLayoutModel {

split_id: number;

txt: string;

url: string;

router: string;

is_login: boolean;

bt_state: number;

constructor() {

}

getFieldTypeMap():  Map<string, string> {

    let fieldTypeMap = new Map<string, string>();

    fieldTypeMap.set('split_id', 'Integer');

    fieldTypeMap.set('txt', 'String');

    fieldTypeMap.set('url', 'String');

    fieldTypeMap.set('router', 'String');

    fieldTypeMap.set('is_login', 'Boolean');

    fieldTypeMap.set('bt_state', 'Integer');

    return fieldTypeMap;

}

getClassName(): string {

    return 'split_layout';

}

getPrimaryKeyList(): string[] {

    let primaryKeyList: string[] = [];

    primaryKeyList.push('split_id');

    return primaryKeyList;

}

getIndexList(): string[] {

    let indexList: string[] = [];

    return indexList;

}

getEncryptedFieldList(): string[] {

    let encryptedFieldList: string[] = [];

    return encryptedFieldList;

}

setSplit_id(split_id: number): void {

    this.split_id = split_id;

}

getSplit_id(): number  {

    return this.split_id;

}

setTxt(txt: string): void {

    this.txt = txt;

}

getTxt(): string  {

    return this.txt;

}

setUrl(url: string): void {

    this.url = url;

}

getUrl(): string  {

    return this.url;

}

setRouter(router: string): void {

    this.router = router;

}

getRouter(): string  {

    return this.router;

}

setIs_login(is_login: boolean): void {

    this.is_login = is_login;

}

getIs_login(): boolean  {

    return this.is_login;

}

setBt_state(bt_state: number): void {

    this.bt_state = bt_state;

}

getBt_state(): number  {

    return this.bt_state;

}

static parseFrom(inputObject: any): SplitLayoutModel {

    let result = new SplitLayoutModel();

    if (!inputObject) {

        return result;

    }

    if (inputObject.split_id) {

        result.split_id = inputObject.split_id;

    }

    if (inputObject.txt) {

        result.txt = inputObject.txt;

    }

    if (inputObject.url) {

        result.url = inputObject.url;

    }

    if (inputObject.router) {

        result.router = inputObject.router;

    }

    if (inputObject.is_login) {

        result.is_login = inputObject.is_login;

    }

    if (inputObject.bt_state) {

        result.bt_state = inputObject.bt_state;

    }

    return result;

}

}

然后把这些内容同步到云端

一切都完成之后,我们进行页面逻辑的修改

import { SplitLayoutModel } from "../entity/SplitLayoutModel"

@Preview

@Component

export struct SplitLayout {

@Link listData: SplitLayoutModel[]

private scroller: Scroller = new Scroller()

build() {

Column() {

  Grid(this.scroller){

    ForEach(this.listData, (item:SplitLayoutModel) => {

      GridItem(){

        Column() {

          Image(item.url)

            .width(45)

            .height(45)

            .borderRadius(24)

            .margin({ top: 5 })

          Text(item.txt)

            .padding(2)

            .fontSize(16)

            .fontColor(Color.Black)

            .textAlign(TextAlign.Center)

        }

      }

    })

  }

  .scrollBar(BarState.Off)

  .rowsTemplate('1fr 1fr')

  .rowsGap(15)

  .columnsGap(10)

  .height(150)

    ScrollBar({ scroller: this.scroller, direction: ScrollBarDirection.Horizontal,state: BarState.Auto }) {

      Text()

        .width(40)

        .height(10)

        .borderRadius(10)

        .backgroundColor('#ff34a8e5')

    }

    .borderRadius(5)

    .margin({top:10})

    .width(100)

    .backgroundColor('#ededed')

}

.alignItems(HorizontalAlign.Center)

.height(190)

.width('95%')

.margin({top:20})

.backgroundColor('#ffeedeb8')

.padding(16)

.borderRadius(20)

}

}

然后在主页调用组件

先创建一个接收数据变量

@State splitList:SplitLayoutModel[]=[]

                SplitLayout({listData:this.splitList})

进行数据查询和赋值

  let databaseZone = cloudDatabase.zone('default');

 let listData2 = await databaseZone.query(condition2);

  let json2 = JSON.stringify(listData2)

  let data2:SplitLayoutModel[]= JSON.parse(json2)

  this.splitList=data2

到这里我们的金刚区就实现了

Change
《仿盒马》app开发技术分享-- 新人专享券(2)

技术栈

Appgallery connect

开发准备

上一篇文章中我们实现了项目端云一体化的升级,我们的数据后期就要从云侧的数据库去获取了,现在我们从头开始对项目进行端云一体化的改造。我们在首页已经把新人专享券抽离为公共组件

现在我们继续进行功能开发,把这个组建的本地数据展示修改为端侧获取。

功能分析

我们把之前实现的首页功能拿出改造一下。​我们在首页实现了新用户领券中心,数据结构就是 模块的标题、说明、优惠券列表,列表包含优惠券的金额、类型,同时我们还要给券添加一些其他参数,比如领取时间,领取用户等,这时候就又延伸出一个功能,当我们用户没有登录的时候,我们点击立即领取,是需要跳转到用户登录页面的。

因为云数据库不支持外键,所以我们通过多插入id字段来进行数据查询。

代码实现

首先我们进行表的创建。

home_new_people_coupon 这是首页活动模块的表

{

"objectTypeName": "home_new_people_coupon",

"fields": [

{"fieldName": "activity_id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "title", "fieldType": "String", "notNull": true, "defaultValue": 0},

{"fieldName": "msg", "fieldType": "String"},

{"fieldName": "home_coupon_activity_id", "fieldType": "Integer"},

{"fieldName": "router", "fieldType": "String"},

{"fieldName": "activity_time", "fieldType": "String"}

],

"indexes": [

{"indexName": "home_coupon_activity_id_index", "indexList": [{"fieldName":"home_coupon_activity_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

然后我们创建对应的活动id下的优惠券列表表

coupon_info

{

"objectTypeName": "coupon_info",

"fields": [

{"fieldName": "coupon_id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},

{"fieldName": "home_coupon_activity_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},

{"fieldName": "price", "fieldType": "String"},

{"fieldName": "type", "fieldType": "String"},

{"fieldName": "get_time", "fieldType": "String"},

{"fieldName": "limit_amount", "fieldType": "Integer"},

{"fieldName": "txt", "fieldType": "String"},

{"fieldName": "activity_id", "fieldType": "Integer"}

],

"indexes": [

{"indexName": "couponIdIndex", "indexList": [{"fieldName":"coupon_id","sortType":"ASC"}]}

],

"permissions": [

{"role": "World", "rights": ["Read"]},

{"role": "Authenticated", "rights": ["Read", "Upsert"]},

{"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},

{"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}

]

}

完成之后我们插入数据

{

"cloudDBZoneName": "default",

"objectTypeName": "home_new_people_coupon",

"objects": [

{

  "activity_id": 10,

  "title": "新人活动",

  "msg": "前三单免运费",

  "home_coupon_activity_id": 10,

  "router": "路由",

  "activity_time": "2025-4-3"

}

]

}

{

"cloudDBZoneName": "default",

"objectTypeName": "coupon_info",

"objects": [

{

  "coupon_id": 10,

  "home_coupon_activity_id": 10,

  "price": "10",

  "type": "新人专享",

  "get_time": "2025-3-18",

  "limit_amount": 30,

  "txt": "string1",

  "activity_id": 1

},

{

  "coupon_id": 20,

  "home_coupon_activity_id": 10,

  "price": "string2",

  "type": "string2",

  "get_time": "string2",

  "limit_amount": 20,

  "txt": "string2",

  "activity_id": 1

},

{

  "coupon_id": 30,

  "home_coupon_activity_id": 10,

  "price": "string1",

  "type": "string1",

  "get_time": "string1",

  "limit_amount": 10,

  "txt": "string1",

  "activity_id": 1

},

{

  "coupon_id": 40,

  "home_coupon_activity_id": 10,

  "price": "string2",

  "type": "string2",

  "get_time": "string2",

  "limit_amount": 20,

  "txt": "string2",

  "activity_id": 1

}

]

}

数据都插入完之后,我们把内容同步到云端数据库,然后client model 、server model 创建对应的类

都执行完之后我们就可以直接在index 页面进行数据的查询了

首先创建接收数据的对象

@State home_new_people_coupon:homeNewPeopleCoupon|null=null

@State couponList:couponInfo[]=[]

然后进行查询

let databaseZone = cloudDatabase.zone('default');

let condition = new cloudDatabase.DatabaseQuery(home_new_people_coupon);

let condition1 = new cloudDatabase.DatabaseQuery(coupon_info);

let listData = await databaseZone.query(condition);

let json = JSON.stringify(listData)

let data:homeNewPeopleCoupon= JSON.parse(json)

this.home_new_people_coupon=data;

let listData1 = await databaseZone.query(condition1);

condition1.equalTo("home_coupon_activity_id",data.home_coupon_activity_id)

let json1 = JSON.stringify(listData1)

let data1:couponInfo[]= JSON.parse(json1)

this.couponList=data1

可以看到我们的云端数据已经查出来了

我们把数据修改一下提交到云端

{

"cloudDBZoneName": "default",

"objectTypeName": "coupon_info",

"objects": [

{

  "coupon_id": 10,

  "home_coupon_activity_id": 10,

  "price": "10",

  "type": "新人专享",

  "get_time": "2025-3-18",

  "limit_amount": 30,

  "txt": "string1",

  "activity_id": 1

},

{

  "coupon_id": 20,

  "home_coupon_activity_id": 10,

  "price": "15",

  "type": "新人专享",

  "get_time": "string2",

  "limit_amount": 20,

  "txt": "string2",

  "activity_id": 1

},

{

  "coupon_id": 30,

  "home_coupon_activity_id": 10,

  "price": "20",

  "type": "新人专享",

  "get_time": "string1",

  "limit_amount": 10,

  "txt": "string1",

  "activity_id": 1

},

{

  "coupon_id": 40,

  "home_coupon_activity_id": 10,

  "price": "30",

  "type": "新人专享",

  "get_time": "string2",

  "limit_amount": 20,

  "txt": "string2",

  "activity_id": 1

}

]

}

然后修改我们之间创建的新人活动的组件

import { couponInfo } from "../entity/couponInfo"

import { homeNewPeopleCoupon } from "../entity/homeNewPeopleCoupon"

@Component

@Preview

export struct CouponComponent {

@Link home_activity:homeNewPeopleCoupon|null

@Link couponList:couponInfo[]

build() {

Column() {

  Row() {

    Text(this.home_activity?.title)

      .fontSize(20)

      .fontColor('#FF0000')

    Text(this.home_activity?.msg)

      .fontSize(14)

      .fontColor('#888888')

      .margin({left:10})

  }

  .width('100%')

  .padding(16)

  List({ space: 10 }) {

    ForEach(this.couponList, (item:couponInfo) => {

      ListItem() {

        Column() {

          Text(item.price)

            .fontSize(22)

            .fontColor('#FF4444')

            .margin({ bottom: 8 })

          Text(item.type)

            .fontSize(12)

            .fontColor('#FF4444')

        }

        .padding(10)

        .backgroundColor("#ffffff")

        .borderRadius(8)

      }

    })

  }

  .margin({left:50})

  .listDirection(Axis.Horizontal)

  .width('100%')

  .height(80)

  Button('立即领取', { type: ButtonType.Normal })

    .width(240)

    .height(40)

    .backgroundColor('#FF0000')

    .fontColor(Color.White)

    .borderRadius(20)

    .margin({ bottom: 16 })

    .onClick(()=>{

      console.log(`"router"`)

    })

}

.backgroundColor("#fffce2be")

.width('95%')

.margin({top:20})

.borderRadius(20)

}

}

首页调用组件进行参数的传入

CouponComponent({home_activitythis.home_new_people_coupon,couponListthis.couponList})

到这里我们的新人活动就完成了

鸿蒙小林
曾经的小众宝藏网站就这样落幕了吗?

好久没来,之前的账号也已经忘记了,再回来看看

发布一个隐私优先视频分析工具 - VidLoad.cc

大家好!我想跟大家分享一个我(AI)开发的开源项目 - VidLoad.cc ,这是一个完全基于浏览器的视频播放器和分析工具。

✨ 核心特性

  • 🔒 隐私优先 - 所有视频处理都在本地浏览器中完成,绝不上传到服务器

  • 🌐 通用支持 - 支持 MP4 、WebM 、HLS(M3U8)、DASH 等多种格式

  • ⚡ 实时分析 - 使用 WebAssembly FFmpeg 进行元数据提取

  • 🛡️ 合规友好 - GDPR/CCPA 合规,适合敏感内容处理

🎯 适用场景

  • 内容创作者:上传前质量检测,多平台格式优化

  • 开发者:HLS 流调试,视频播放器集成测试

  • 隐私敏感行业:医疗、法律、教育等领域的视频分析

  • 企业应用:员工培训视频验证,会议录制审查

🛠️ 技术栈

  • Next.js 14 + React 18 + TypeScript

  • FFmpeg.wasm (WebAssembly)

  • HLS.js + Tailwind CSS

  • 完全静态部署,支持 Cloudflare Pages

🚀 在线体验

#源码开放在 GitHub ,欢迎 Star ⭐ 和贡献!

#开源 #视频处理 #隐私保护 #WebAssembly #Next.js

用 AI 重构 HackerTalk 后端,代码量大幅下降,性能提升

HackerTalk 的后端代码有 n 年没改动了 sweat_smile ,Java 稳如老狗,k8s 上的 docker 连续跑了 1 年多都没故障重启过(7h 那个是今天调试更新),花时间一直没有动力升级。

新项目大多数用 hono + drizzle + lambda 的技术栈了,想着借助 AI 的能力把 Java 给迁移了,也是几天的技术验证,效果非常好。效果 ⬇️

Java Spring Boot: 16461 lines

  • monster: 7618
  • model: 3620
  • channel: 2056
  • notification: 1847
  • websocket: 637
  • gateway: 683

Nodejs: 5486 lines

  • api: 4697
  • notification: 789

代码量为原来的 1/3,打包后的体积 500kb 左右,使用 AWS CDK 进行部署,40 秒就能上线一个版本,更新速度非常快。

大部份简单的 API 延迟性能没有变化(语言影响不大,主要在 db 和网络上),部份 API 重构后更加轻量,延迟降低 50-100ms。

语言迁移的原因

Java + Spring Boot 确实很好用,而且非常稳定,各种后端方案都能找到,AWS、GCP、阿里云之类的云厂商也会优先发布 Java SDK,版本质量和功能完善程度都会比其他语言的 SDK 好。

但随着 AI 发展和 Serverless 技术成熟,产品迭代的速度要更快,hono + drizzle 的出现让我觉得 ts 后端非常有戏,bun 也让我看到 js 性能是没问题的,积累一整套的 SDK 代码替代 Spring Boot 大部分常用功能。

  • ts 的类型系统无敌,可以解决 Java NPE 问题。
  • monorepo 管理方便,前后端可以共享大量代码。
  • AWS CDK + Lambda 管理方便,上线速度超快。
  • 几乎所有需要常驻的场景都有 Lambda 的方案轻量替代。
  • ts 做营销方便,能够找到各种库,比如 react-email 邮件营销。
  • 2025 各个云厂商的 js SDK 已经很成熟。

这些点积累下来与其说 Java 不行,不如说 js/ts 的生态太强,在未来要吃掉很大部份的后端开发,优势不止相对于 java,像 go/rust 也很难赶上 ts 的优点。

可能的问题

我用 ts 实现了一套兼容 spring sercurity 的 session 管理机制,切换到 ts 后端后可能有部份用户需要重新登录,如果有其他意外情况,欢迎评论反馈,谢谢。

j2go
还有几个人在,出来打个招呼

上次发帖都一年多前了~~~

Xiangzhen Zhang
软件开发模板

我在初始化Spring Boot+MySQL+MyBatis Plus项目时,通常会利用MyBatisX插件来生成mapper、entity、service层的代码,但是增删改查等逻辑还是需要自己编写,有没有可以提高工作效率的方法?比如说代码生成器、项目模板等。

前端项目是否也有类似的模板?

Xiangzhen Zhang
如何通俗易懂地介绍GRPO算法?

DeepSeek里用的GRPO算法。

Xiangzhen Zhang
本站缺少一个热榜功能

目前是按时间排序,但是大家喜欢看热门帖子,应该加个按热度排序。

ycy
想看看大家最近都在想什么

有点无聊 想看看大家最近都在思考些什么 烦恼也好开心事也好 都可以回复在下面 stuck_out_tongue_winking_eye

Xiangzhen Zhang
如何实现网站在不同大小屏幕下的UI适配?
  1. 在桌面端访问本网站
  2. 按下f12
  3. 通过中间的分界条调整窗口的大小

可以发现本站在不同大小的窗口下会有不同的样式,比如宽度很小时消息卡片与边界的间距为0,而且卡片的圆角消失了,请问这个是怎么实现的?

0
中文互联网病了

国内的互联网生态怎么了?一个个都拿着手机APP刷抖音。用户完全被几个大型APP圈养了。这么好的网站都没人气。