技术栈
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)
到这里我们的商品详情页面的内容已经比较完善了
技术栈
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)
到这里我们就实现了本节的内容了,下一节我们将要丰富商品详情页的内容
技术栈
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%')
}
}
这样就实现了对应的内容了
技术栈
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})
到这里我们就实现了首页商品列表的内容
技术栈
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的内容
技术栈
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})
到这里我们就实现了活动配置相关的内容
技术栈
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}
);
}
数据也已经查询成功,接下来我们直接运行程序,可以看到金刚区是在显示中的,接下来我们修改金刚区的状态,再执行一下,可以看到金刚区已经不见了,到这里我们首页模块的显示隐藏配置已经实现了。
技术栈
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
到这里我们的金刚区就实现了
技术栈
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.couponList})
到这里我们的新人活动就完成了
大家好!我想跟大家分享一个我(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
🚀 在线体验
- 🌐 立即试用: https://vidload.cc
#源码开放在 GitHub ,欢迎 Star ⭐ 和贡献!
#开源 #视频处理 #隐私保护 #WebAssembly #Next.js
HackerTalk 的后端代码有 n 年没改动了 ,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 后端后可能有部份用户需要重新登录,如果有其他意外情况,欢迎评论反馈,谢谢。
我在初始化Spring Boot+MySQL+MyBatis Plus项目时,通常会利用MyBatisX插件来生成mapper、entity、service层的代码,但是增删改查等逻辑还是需要自己编写,有没有可以提高工作效率的方法?比如说代码生成器、项目模板等。
前端项目是否也有类似的模板?
- 在桌面端访问本网站
- 按下f12
- 通过中间的分界条调整窗口的大小
可以发现本站在不同大小的窗口下会有不同的样式,比如宽度很小时消息卡片与边界的间距为0,而且卡片的圆角消失了,请问这个是怎么实现的?