如何实现上下切换的页面间跳转动画
使用pageTransition函数实现页面转场效果。通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画效果。将slide属性设置为SlideEffect.Bottom,页面入场时从下方滑入,出场时滑出到下方,从而实现上下切换效果。参考代码如下:
// Index.ets
@Entry
@Component
struct PageTransition1 {
pageInfos: NavPathStack = new NavPathStack();
build() {
Stack({ alignContent: Alignment.Bottom }) {
Navigation(this.pageInfos) {
Image($r('app.media.ic_banner01')).width('100%').height(200) // The image is stored in the media folder
}
}.height('100%').width('100%')
}
pageTransition() {
PageTransitionEnter({ duration: 500, curve: Curve.Linear }).slide(SlideEffect.Bottom)
PageTransitionExit({ duration: 500, curve: Curve.Ease }).slide(SlideEffect.Bottom)
}
}
// Page1.ets
@Entry
@Component
struct PageTransition2 {
pageInfos: NavPathStack = new NavPathStack();
build() {
Stack({ alignContent: Alignment.Bottom }) {
Navigation(this.pageInfos) {
Image($r('app.media.ic_banner02')).width('100%').height(200) // The image is stored in the media folder
}
}.height('100%').width('100%')
}
pageTransition() {
PageTransitionEnter({ duration: 500, curve: Curve.Linear }).slide(SlideEffect.Bottom)
PageTransitionExit({ duration: 500, curve: Curve.Ease }).slide(SlideEffect.Bottom)
}
}
https://coub.com/stories/5089937-coventry
https://coub.com/stories/5089936-city
https://coub.com/stories/5089935-derby
https://coub.com/stories/5089934-chester
https://coub.com/stories/5089932-uclan
https://coub.com/stories/5089930-cardiff
https://coub.com/stories/5089928-uwic
https://coub.com/stories/5089925-certificate-uc
https://coub.com/stories/5089923-degree-cantab
https://coub.com/stories/5089921-graduation-ucb
https://coub.com/stories/5089918-brunel
https://coub.com/stories/5089916-bristol
https://coub.com/stories/5089914-brighton
https://coub.com/stories/5089911-bradford
https://coub.com/stories/5089909-bpp
https://coub.com/stories/5089906-bu
https://coub.com/stories/5089904-bolton
https://coub.com/stories/5089903-ucb
https://coub.com/stories/5089902-bcu
https://coub.com/stories/5089901-uob
https://coub.com/stories/5089900-bbk
https://coub.com/stories/5089899-bedfordhire
https://coub.com/stories/5089898-1-1bath
https://coub.com/stories/5089897-bangor
https://coub.com/stories/5089896-aston
https://coub.com/stories/5089895-copyual
https://coub.com/stories/5089894-aub
https://coub.com/stories/5089893-aber
https://coub.com/stories/5089892-abertay
https://coub.com/stories/5089890-au
https://coub.com/stories/5089888-langara
https://coub.com/stories/5089885-uofs
https://coub.com/stories/5089883-uofr
https://coub.com/stories/5089881-mcgill
https://coub.com/stories/5089878-concordia
https://coub.com/stories/5089876-mcmaste
https://coub.com/stories/5089874-yu
https://coub.com/stories/5089871-wlu
https://coub.com/stories/5089868-windsor
https://coub.com/stories/5089866-certificate-uwo
https://coub.com/stories/5089864-degree-waterloo
https://coub.com/stories/5089861-graduation-uoft
https://coub.com/stories/5089859-cbu
https://coub.com/stories/5089857-uoit
https://coub.com/stories/5089855-uottawa
https://coub.com/stories/5089852-uofg
https://coub.com/stories/5089850-carleton
https://coub.com/stories/5089847-brock
https://coub.com/stories/5089846-msvu
https://coub.com/stories/5089845-dalhousie
https://coub.com/stories/5089835
https://coub.com/stories/5089838-degree-mun
https://coub.com/stories/5089837-graduation-tru
https://coub.com/stories/5089836-ubc
https://coub.com/stories/5089835
https://coub.com/stories/5089834-bu
https://coub.com/stories/5089833-vancouver
https://coub.com/stories/5089832-ufv
https://coub.com/stories/5089831-unbc
https://coub.com/stories/5089829-ubc
https://coub.com/stories/5089828-sfu
https://coub.com/stories/5089827-kpu
https://coub.com/stories/5089826-macewan
https://coub.com/stories/5089825-uofl
https://coub.com/stories/5089824-certificate-uc
https://coub.com/stories/5089823-degree-ua
https://coub.com/stories/5089822-graduation-ua
https://coub.com/stories/5089821-asu
https://coub.com/stories/5089820-uh
https://coub.com/stories/5089819-ucsd
https://coub.com/stories/5089818-stanford
https://coub.com/stories/5089817-fsu
https://coub.com/stories/5089816-ohio
https://coub.com/stories/5089815-ku
https://coub.com/stories/5089814-missouri
https://coub.com/stories/5089813-iowa
https://coub.com/stories/5089812-twincities
https://coub.com/stories/5089811-mississippi
https://coub.com/stories/5089810-um
https://coub.com/stories/5089808-temple
https://coub.com/stories/5089809-gwu
https://coub.com/stories/5089807-ucsb
https://coub.com/stories/5089806-uci
https://coub.com/stories/5089805-brandeis
https://coub.com/stories/5089804-wfu
https://coub.com/stories/5089803-tufts
https://coub.com/stories/5089802-uva
https://coub.com/stories/5089801-ucb
https://coub.com/stories/5089800-vandy
https://coub.com/stories/5089799-emory
https://coub.com/stories/5089798-rice
https://coub.com/stories/5089797-duke
https://coub.com/stories/5089796-princeton
https://coub.com/stories/5089795-pace
https://coub.com/stories/5089794-utk
https://coub.com/stories/5089793-mit
https://coub.com/stories/5089792-msu
https://coub.com/stories/5089791-psu
https://coub.com/stories/5089790-usc
https://coub.com/stories/5089789-seattle
https://coub.com/stories/5089788-nyu
https://coub.com/stories/5089787-wsu
https://coub.com/stories/5089786-ucla
https://coub.com/stories/5089785-osu
https://coub.com/stories/5089784-uts
https://coub.com/stories/5089783-unsw
https://coub.com/stories/5089782-uow
https://coub.com/stories/5089781-usyd
https://coub.com/stories/5089780-uon
https://coub.com/stories/5089779-une
https://coub.com/stories/5089778-uws
https://coub.com/stories/5089777-scu
https://coub.com/stories/5089776-monash
https://coub.com/stories/5089775-sut
https://coub.com/stories/5089774-latrobe
https://coub.com/stories/5089773-unimelb
https://coub.com/stories/5089772-rmit
https://coub.com/stories/5089771-deakin
https://coub.com/stories/5089770-swinburne
https://coub.com/stories/5089769-utas
https://coub.com/stories/5089768-victoria
https://coub.com/stories/5089767-adelaide
https://coub.com/stories/5089766-unisa
https://coub.com/stories/5089765-uc
https://coub.com/stories/5089764-acu
https://coub.com/stories/5089763-anu
https://coub.com/stories/5089762-uc
https://coub.com/stories/5089761-cdu
https://coub.com/stories/5089760-ecu
https://coub.com/stories/5089759-murdoch
https://coub.com/stories/5089758-uwa
https://coub.com/stories/5089757-curtin
https://coub.com/stories/5089756-utas
https://coub.com/stories/5089755-usq
https://coub.com/stories/5089754-bond
https://coub.com/stories/5089753-jcu
https://coub.com/stories/5089752-qut
https://coub.com/stories/5089751-cqu
https://coub.com/stories/5089750-jcu
https://coub.com/stories/5089749-griffith
https://coub.com/stories/5089748-uq
使用pageTransition函数实现页面转场效果。通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画效果。将slide属性设置为SlideEffect.Bottom,页面入场时从下方滑入,出场时滑出到下方,从而实现上下切换效果。参考代码如下:
// Index.ets
@Entry
@Component
struct PageTransition1 {
pageInfos: NavPathStack = new NavPathStack();
build() {
Stack({ alignContent: Alignment.Bottom }) {
Navigation(this.pageInfos) {
Image($r('app.media.ic_banner01')).width('100%').height(200) // The image is stored in the media folder
}
}.height('100%').width('100%')
}
pageTransition() {
PageTransitionEnter({ duration: 500, curve: Curve.Linear }).slide(SlideEffect.Bottom)
PageTransitionExit({ duration: 500, curve: Curve.Ease }).slide(SlideEffect.Bottom)
}
}
// Page1.ets
@Entry
@Component
struct PageTransition2 {
pageInfos: NavPathStack = new NavPathStack();
build() {
Stack({ alignContent: Alignment.Bottom }) {
Navigation(this.pageInfos) {
Image($r('app.media.ic_banner02')).width('100%').height(200) // The image is stored in the media folder
}
}.height('100%').width('100%')
}
pageTransition() {
PageTransitionEnter({ duration: 500, curve: Curve.Linear }).slide(SlideEffect.Bottom)
PageTransitionExit({ duration: 500, curve: Curve.Ease }).slide(SlideEffect.Bottom)
}
}
https://coub.com/stories/5089937-coventry
https://coub.com/stories/5089936-city
https://coub.com/stories/5089935-derby
https://coub.com/stories/5089934-chester
https://coub.com/stories/5089932-uclan
https://coub.com/stories/5089930-cardiff
https://coub.com/stories/5089928-uwic
https://coub.com/stories/5089925-certificate-uc
https://coub.com/stories/5089923-degree-cantab
https://coub.com/stories/5089921-graduation-ucb
https://coub.com/stories/5089918-brunel
https://coub.com/stories/5089916-bristol
https://coub.com/stories/5089914-brighton
https://coub.com/stories/5089911-bradford
https://coub.com/stories/5089909-bpp
https://coub.com/stories/5089906-bu
https://coub.com/stories/5089904-bolton
https://coub.com/stories/5089903-ucb
https://coub.com/stories/5089902-bcu
https://coub.com/stories/5089901-uob
https://coub.com/stories/5089900-bbk
https://coub.com/stories/5089899-bedfordhire
https://coub.com/stories/5089898-1-1bath
https://coub.com/stories/5089897-bangor
https://coub.com/stories/5089896-aston
https://coub.com/stories/5089895-copyual
https://coub.com/stories/5089894-aub
https://coub.com/stories/5089893-aber
https://coub.com/stories/5089892-abertay
https://coub.com/stories/5089890-au
https://coub.com/stories/5089888-langara
https://coub.com/stories/5089885-uofs
https://coub.com/stories/5089883-uofr
https://coub.com/stories/5089881-mcgill
https://coub.com/stories/5089878-concordia
https://coub.com/stories/5089876-mcmaste
https://coub.com/stories/5089874-yu
https://coub.com/stories/5089871-wlu
https://coub.com/stories/5089868-windsor
https://coub.com/stories/5089866-certificate-uwo
https://coub.com/stories/5089864-degree-waterloo
https://coub.com/stories/5089861-graduation-uoft
https://coub.com/stories/5089859-cbu
https://coub.com/stories/5089857-uoit
https://coub.com/stories/5089855-uottawa
https://coub.com/stories/5089852-uofg
https://coub.com/stories/5089850-carleton
https://coub.com/stories/5089847-brock
https://coub.com/stories/5089846-msvu
https://coub.com/stories/5089845-dalhousie
https://coub.com/stories/5089835
https://coub.com/stories/5089838-degree-mun
https://coub.com/stories/5089837-graduation-tru
https://coub.com/stories/5089836-ubc
https://coub.com/stories/5089835
https://coub.com/stories/5089834-bu
https://coub.com/stories/5089833-vancouver
https://coub.com/stories/5089832-ufv
https://coub.com/stories/5089831-unbc
https://coub.com/stories/5089829-ubc
https://coub.com/stories/5089828-sfu
https://coub.com/stories/5089827-kpu
https://coub.com/stories/5089826-macewan
https://coub.com/stories/5089825-uofl
https://coub.com/stories/5089824-certificate-uc
https://coub.com/stories/5089823-degree-ua
https://coub.com/stories/5089822-graduation-ua
https://coub.com/stories/5089821-asu
https://coub.com/stories/5089820-uh
https://coub.com/stories/5089819-ucsd
https://coub.com/stories/5089818-stanford
https://coub.com/stories/5089817-fsu
https://coub.com/stories/5089816-ohio
https://coub.com/stories/5089815-ku
https://coub.com/stories/5089814-missouri
https://coub.com/stories/5089813-iowa
https://coub.com/stories/5089812-twincities
https://coub.com/stories/5089811-mississippi
https://coub.com/stories/5089810-um
https://coub.com/stories/5089808-temple
https://coub.com/stories/5089809-gwu
https://coub.com/stories/5089807-ucsb
https://coub.com/stories/5089806-uci
https://coub.com/stories/5089805-brandeis
https://coub.com/stories/5089804-wfu
https://coub.com/stories/5089803-tufts
https://coub.com/stories/5089802-uva
https://coub.com/stories/5089801-ucb
https://coub.com/stories/5089800-vandy
https://coub.com/stories/5089799-emory
https://coub.com/stories/5089798-rice
https://coub.com/stories/5089797-duke
https://coub.com/stories/5089796-princeton
https://coub.com/stories/5089795-pace
https://coub.com/stories/5089794-utk
https://coub.com/stories/5089793-mit
https://coub.com/stories/5089792-msu
https://coub.com/stories/5089791-psu
https://coub.com/stories/5089790-usc
https://coub.com/stories/5089789-seattle
https://coub.com/stories/5089788-nyu
https://coub.com/stories/5089787-wsu
https://coub.com/stories/5089786-ucla
https://coub.com/stories/5089785-osu
https://coub.com/stories/5089784-uts
https://coub.com/stories/5089783-unsw
https://coub.com/stories/5089782-uow
https://coub.com/stories/5089781-usyd
https://coub.com/stories/5089780-uon
https://coub.com/stories/5089779-une
https://coub.com/stories/5089778-uws
https://coub.com/stories/5089777-scu
https://coub.com/stories/5089776-monash
https://coub.com/stories/5089775-sut
https://coub.com/stories/5089774-latrobe
https://coub.com/stories/5089773-unimelb
https://coub.com/stories/5089772-rmit
https://coub.com/stories/5089771-deakin
https://coub.com/stories/5089770-swinburne
https://coub.com/stories/5089769-utas
https://coub.com/stories/5089768-victoria
https://coub.com/stories/5089767-adelaide
https://coub.com/stories/5089766-unisa
https://coub.com/stories/5089765-uc
https://coub.com/stories/5089764-acu
https://coub.com/stories/5089763-anu
https://coub.com/stories/5089762-uc
https://coub.com/stories/5089761-cdu
https://coub.com/stories/5089760-ecu
https://coub.com/stories/5089759-murdoch
https://coub.com/stories/5089758-uwa
https://coub.com/stories/5089757-curtin
https://coub.com/stories/5089756-utas
https://coub.com/stories/5089755-usq
https://coub.com/stories/5089754-bond
https://coub.com/stories/5089753-jcu
https://coub.com/stories/5089752-qut
https://coub.com/stories/5089751-cqu
https://coub.com/stories/5089750-jcu
https://coub.com/stories/5089749-griffith
https://coub.com/stories/5089748-uq
开发自定义键盘,HarmonyOS中有没有长按按键子项生成气泡效果的API
问题现象
按键子项生成气泡效果用什么组件实现好?
效果预览
点击放大
背景知识
使用自定义键盘实现官网示例代码进行自定义键盘调试,然后用LongPressGesture增加长按手势事件,最后使用Popup控制为组件绑定Popup气泡,使用Popup气泡,如果按键处于边缘位置,组件会自动偏移,完整的显示在屏幕中间。
解决方案
自定义键盘没有直接长按生成气泡的接口,需要自定义长按逻辑,手指在长按小键盘组件时,使用Popup生成一个气泡,在气泡生成之后,使用onTouch获取手指位置并且根据气泡组件的位置和横向大小,计算当前手指划过的距离从而标记手指当前选择的内容。
代码实现思路如下:
创建变量控制气泡显隐样式以及气泡在边缘的位置:
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;
增加LongPressGesture长按控制手势,在onAction事件中增加逻辑,让气泡仅显示在指定按键上方;长按手势默认最短长按时间为500毫秒,可配置duration参数控制最短长按时长:
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
增加bindPopup气泡控制,设置placement: Placement.TopRight,让气泡显示在按钮上方,通过this.popupBuilder(item)给自定义气泡组件传参然后自定义气泡内容:
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
使用onAreaChange监听新生成的气泡组件的宽度,再使用onTouch事件获取当前手指的坐标,然后根据手指的坐标和气泡组件的宽度,获取当前手指在气泡组件中的位置:
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
参考官网自定义键盘实现,将demo中的NumberKeyboard文件替换为以下示例代码:
// 该场景代码基于自定义键盘实现官网示例代码https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-custom-keyboard#section1680905412256进行调试;
// 修改demo中NumberKeyboard文件如下
@Component
export struct NumberKeyboard {
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;
build() {
Row() {
Scroll() {
Column() {
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[0], (item: Menu) => {
EnglishButton({ item: item })
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
}, (item: Menu) => JSON.stringify(item));
};
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[1], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({
left: 18,
right: 18,
top: 11
});
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[2], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({ top: 11 });
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[3], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({ top: 11 });
}
.onAreaChange((oldVal, newVal) => {
// 键盘宽度
this.inputWidthRect = Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off);
};
}
@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
}
@Styles
function globalFancy() {
.borderRadius(10)
.width('25%')
.height('100%');
}
// [Start EnglishButton_start]
@Component
struct EnglishButton {
@Consume inputText: string;
// [StartExclude EnglishButton_start]
@Prop item: Menu;
@Consume keyboardController: KeyboardController;
getEnglishText(item: Menu): string | Resource {
if (this.keyboardController.isUpperCase && item.upperText) {
return item.upperText;
} else {
return item.text;
}
}
// [EndExclude EnglishButton_start]
build() {
Button({ type: ButtonType.Normal }) {
if (typeof this.getEnglishText(this.item) === 'string' ||
(this.getEnglishText(this.item) as Resource).type !== Constants.RESOURCE_TYPE_MEDIA) {
Text(this.getEnglishText(this.item));
} else {
Image(this.getEnglishText(this.item))
.width(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.height(Constants.KEYBOARD_BUTTON_FONTSIZE_18);
}
}
.fontColor(Color.Black)
.backgroundColor(this.item.backgroundColor)
.borderRadius(Constants.KEYBOARD_BUTTON_RADIUS)
.fontSize(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.padding(0)
.width(this.item.width)
.height(this.item.height)
.onClick(() => {
this.inputText = this.keyboardController.onInput(this.getEnglishText(this.item));
});
}
}
// [End EnglishButton_start]
https://coub.com/view/4b8l5y
https://coub.com/view/4b8l5w
https://coub.com/view/4b8l5v
https://coub.com/view/4b8l5t
https://coub.com/view/4b8l5r
https://coub.com/view/4b8l5p
https://coub.com/view/4b8l5l
https://coub.com/view/4b8l5h
https://coub.com/view/4b8l5e
https://coub.com/view/4b8l5c
https://coub.com/view/4b8l5b
https://coub.com/view/4b8l5a
https://coub.com/view/4b8l58
https://coub.com/view/4b8l57
https://coub.com/view/4b8l54
https://coub.com/view/4b8l51
https://coub.com/view/4b8l50
https://coub.com/view/4b8l4z
https://coub.com/view/4b8l4w
https://coub.com/view/4b8l4t
https://coub.com/view/4b8l4r
https://coub.com/view/4b8l4o
https://coub.com/view/4b8l4l
https://coub.com/view/4b8l4j
https://coub.com/view/4b8l4f
https://coub.com/view/4b8l4d
https://coub.com/view/4b8l4b
https://coub.com/view/4b8l47
https://coub.com/view/4b8l46
https://coub.com/view/4b8l41
https://coub.com/view/4b8l3y
https://coub.com/view/4b8l3v
https://coub.com/view/4b8l3s
https://coub.com/view/4b8l3r
https://coub.com/view/4b8l3q
https://coub.com/view/4b8l3h
https://coub.com/view/4b8l3f
https://coub.com/view/4b8l3c
https://coub.com/view/4b8l39
https://coub.com/view/4b8l36
https://coub.com/view/4b8l30
https://coub.com/view/4b8l2y
https://coub.com/view/4b8l2v
https://coub.com/view/4b8l2t
https://coub.com/view/4b8l2q
https://coub.com/view/4b8l2n
https://coub.com/view/4b8l2j
https://coub.com/view/4b8l2f
https://coub.com/view/4b8l2c
https://coub.com/view/4b8l2b
https://coub.com/view/4b8l28
https://coub.com/view/4b8l27
https://coub.com/view/4b8l23
https://coub.com/view/4b8l20
https://coub.com/view/4b8l1v
https://coub.com/view/4b8l16
https://coub.com/view/4b8l12
https://coub.com/view/4b8l0w
https://coub.com/view/4b8l0o
https://coub.com/view/4b8l0j
https://coub.com/view/4b8l0g
https://coub.com/view/4b8l0f
https://coub.com/view/4b8l0d
https://coub.com/view/4b8l08
https://coub.com/view/4b8l06
https://coub.com/view/4b8l04
https://coub.com/view/4b8l01
https://coub.com/view/4b8kzx
https://coub.com/view/4b8kzv
https://coub.com/view/4b8kzr
https://coub.com/view/4b8kzl
https://coub.com/view/4b8kzi
https://coub.com/view/4b8kzg
https://coub.com/view/4b8kze
https://coub.com/view/4b8kzc
https://coub.com/view/4b8kpg
https://coub.com/view/4b8kpf
https://coub.com/view/4b8kpe
https://coub.com/view/4b8kpc
https://coub.com/view/4b8kpa
https://coub.com/view/4b8kp7
https://coub.com/view/4b8kp5
https://coub.com/view/4b8kp1
https://coub.com/view/4b8kox
https://coub.com/view/4b8kov
https://coub.com/view/4b8kot
https://coub.com/view/4b8kor
https://coub.com/view/4b8kop
https://coub.com/view/4b8kol
https://coub.com/view/4b8koj
https://coub.com/view/4b8kog
https://coub.com/view/4b8koe
https://coub.com/view/4b8koc
https://coub.com/view/4b8knx
https://coub.com/view/4b8knw
https://coub.com/view/4b8knv
https://coub.com/view/4b8knu
https://coub.com/view/4b8knt
https://coub.com/view/4b8kns
https://coub.com/view/4b8knq
https://coub.com/view/4b8knp
https://coub.com/view/4b8kno
https://coub.com/view/4b8knn
https://coub.com/view/4b8knm
https://coub.com/view/4b8knl
https://coub.com/view/4b8knk
https://coub.com/view/4b8knj
https://coub.com/view/4b8kni
https://coub.com/view/4b8knd
https://coub.com/view/4b8knb
https://coub.com/view/4b8kna
https://coub.com/view/4b8kn9
https://coub.com/view/4b8kn8
问题现象
按键子项生成气泡效果用什么组件实现好?
效果预览
点击放大
背景知识
使用自定义键盘实现官网示例代码进行自定义键盘调试,然后用LongPressGesture增加长按手势事件,最后使用Popup控制为组件绑定Popup气泡,使用Popup气泡,如果按键处于边缘位置,组件会自动偏移,完整的显示在屏幕中间。
解决方案
自定义键盘没有直接长按生成气泡的接口,需要自定义长按逻辑,手指在长按小键盘组件时,使用Popup生成一个气泡,在气泡生成之后,使用onTouch获取手指位置并且根据气泡组件的位置和横向大小,计算当前手指划过的距离从而标记手指当前选择的内容。
代码实现思路如下:
创建变量控制气泡显隐样式以及气泡在边缘的位置:
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;
增加LongPressGesture长按控制手势,在onAction事件中增加逻辑,让气泡仅显示在指定按键上方;长按手势默认最短长按时间为500毫秒,可配置duration参数控制最短长按时长:
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
增加bindPopup气泡控制,设置placement: Placement.TopRight,让气泡显示在按钮上方,通过this.popupBuilder(item)给自定义气泡组件传参然后自定义气泡内容:
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
使用onAreaChange监听新生成的气泡组件的宽度,再使用onTouch事件获取当前手指的坐标,然后根据手指的坐标和气泡组件的宽度,获取当前手指在气泡组件中的位置:
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
参考官网自定义键盘实现,将demo中的NumberKeyboard文件替换为以下示例代码:
// 该场景代码基于自定义键盘实现官网示例代码https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-custom-keyboard#section1680905412256进行调试;
// 修改demo中NumberKeyboard文件如下
@Component
export struct NumberKeyboard {
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;
build() {
Row() {
Scroll() {
Column() {
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[0], (item: Menu) => {
EnglishButton({ item: item })
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
}, (item: Menu) => JSON.stringify(item));
};
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[1], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({
left: 18,
right: 18,
top: 11
});
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[2], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({ top: 11 });
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[3], (item: Menu) => {
EnglishButton({ item: item });
}, (item: Menu) => JSON.stringify(item));
}
.padding({ top: 11 });
}
.onAreaChange((oldVal, newVal) => {
// 键盘宽度
this.inputWidthRect = Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off);
};
}
@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
}
@Styles
function globalFancy() {
.borderRadius(10)
.width('25%')
.height('100%');
}
// [Start EnglishButton_start]
@Component
struct EnglishButton {
@Consume inputText: string;
// [StartExclude EnglishButton_start]
@Prop item: Menu;
@Consume keyboardController: KeyboardController;
getEnglishText(item: Menu): string | Resource {
if (this.keyboardController.isUpperCase && item.upperText) {
return item.upperText;
} else {
return item.text;
}
}
// [EndExclude EnglishButton_start]
build() {
Button({ type: ButtonType.Normal }) {
if (typeof this.getEnglishText(this.item) === 'string' ||
(this.getEnglishText(this.item) as Resource).type !== Constants.RESOURCE_TYPE_MEDIA) {
Text(this.getEnglishText(this.item));
} else {
Image(this.getEnglishText(this.item))
.width(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.height(Constants.KEYBOARD_BUTTON_FONTSIZE_18);
}
}
.fontColor(Color.Black)
.backgroundColor(this.item.backgroundColor)
.borderRadius(Constants.KEYBOARD_BUTTON_RADIUS)
.fontSize(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.padding(0)
.width(this.item.width)
.height(this.item.height)
.onClick(() => {
this.inputText = this.keyboardController.onInput(this.getEnglishText(this.item));
});
}
}
// [End EnglishButton_start]
https://coub.com/view/4b8l5y
https://coub.com/view/4b8l5w
https://coub.com/view/4b8l5v
https://coub.com/view/4b8l5t
https://coub.com/view/4b8l5r
https://coub.com/view/4b8l5p
https://coub.com/view/4b8l5l
https://coub.com/view/4b8l5h
https://coub.com/view/4b8l5e
https://coub.com/view/4b8l5c
https://coub.com/view/4b8l5b
https://coub.com/view/4b8l5a
https://coub.com/view/4b8l58
https://coub.com/view/4b8l57
https://coub.com/view/4b8l54
https://coub.com/view/4b8l51
https://coub.com/view/4b8l50
https://coub.com/view/4b8l4z
https://coub.com/view/4b8l4w
https://coub.com/view/4b8l4t
https://coub.com/view/4b8l4r
https://coub.com/view/4b8l4o
https://coub.com/view/4b8l4l
https://coub.com/view/4b8l4j
https://coub.com/view/4b8l4f
https://coub.com/view/4b8l4d
https://coub.com/view/4b8l4b
https://coub.com/view/4b8l47
https://coub.com/view/4b8l46
https://coub.com/view/4b8l41
https://coub.com/view/4b8l3y
https://coub.com/view/4b8l3v
https://coub.com/view/4b8l3s
https://coub.com/view/4b8l3r
https://coub.com/view/4b8l3q
https://coub.com/view/4b8l3h
https://coub.com/view/4b8l3f
https://coub.com/view/4b8l3c
https://coub.com/view/4b8l39
https://coub.com/view/4b8l36
https://coub.com/view/4b8l30
https://coub.com/view/4b8l2y
https://coub.com/view/4b8l2v
https://coub.com/view/4b8l2t
https://coub.com/view/4b8l2q
https://coub.com/view/4b8l2n
https://coub.com/view/4b8l2j
https://coub.com/view/4b8l2f
https://coub.com/view/4b8l2c
https://coub.com/view/4b8l2b
https://coub.com/view/4b8l28
https://coub.com/view/4b8l27
https://coub.com/view/4b8l23
https://coub.com/view/4b8l20
https://coub.com/view/4b8l1v
https://coub.com/view/4b8l16
https://coub.com/view/4b8l12
https://coub.com/view/4b8l0w
https://coub.com/view/4b8l0o
https://coub.com/view/4b8l0j
https://coub.com/view/4b8l0g
https://coub.com/view/4b8l0f
https://coub.com/view/4b8l0d
https://coub.com/view/4b8l08
https://coub.com/view/4b8l06
https://coub.com/view/4b8l04
https://coub.com/view/4b8l01
https://coub.com/view/4b8kzx
https://coub.com/view/4b8kzv
https://coub.com/view/4b8kzr
https://coub.com/view/4b8kzl
https://coub.com/view/4b8kzi
https://coub.com/view/4b8kzg
https://coub.com/view/4b8kze
https://coub.com/view/4b8kzc
https://coub.com/view/4b8kpg
https://coub.com/view/4b8kpf
https://coub.com/view/4b8kpe
https://coub.com/view/4b8kpc
https://coub.com/view/4b8kpa
https://coub.com/view/4b8kp7
https://coub.com/view/4b8kp5
https://coub.com/view/4b8kp1
https://coub.com/view/4b8kox
https://coub.com/view/4b8kov
https://coub.com/view/4b8kot
https://coub.com/view/4b8kor
https://coub.com/view/4b8kop
https://coub.com/view/4b8kol
https://coub.com/view/4b8koj
https://coub.com/view/4b8kog
https://coub.com/view/4b8koe
https://coub.com/view/4b8koc
https://coub.com/view/4b8knx
https://coub.com/view/4b8knw
https://coub.com/view/4b8knv
https://coub.com/view/4b8knu
https://coub.com/view/4b8knt
https://coub.com/view/4b8kns
https://coub.com/view/4b8knq
https://coub.com/view/4b8knp
https://coub.com/view/4b8kno
https://coub.com/view/4b8knn
https://coub.com/view/4b8knm
https://coub.com/view/4b8knl
https://coub.com/view/4b8knk
https://coub.com/view/4b8knj
https://coub.com/view/4b8kni
https://coub.com/view/4b8knd
https://coub.com/view/4b8knb
https://coub.com/view/4b8kna
https://coub.com/view/4b8kn9
https://coub.com/view/4b8kn8
用CodeBuddy做了几个药店小程序的功能,电子处方,用药安全,药品追溯码
随着线上售药监管规则持续细化,线上药店小程序不再只是简单的下单工具,电子处方、用药安全确认、药品追溯码是满足药监核查的三大核心基础功能,也是线上经营必备合规模块。
元岳科技在原电子处方的基础上,新增了用药安全日志和本地药品追溯的功能。
一、电子处方闭环功能:规范处方药线上流转
根据《处方药网络零售合规指南》要求,线上售卖处方药必须形成完整审方流程,该模块核心作用是搭建标准化处方留存链路。
使用流程:用户选购处方药后,需完成实名登记,上传复诊处方;系统仅开放执业药师实名账号审核,全程记录操作人、操作时间,日志不可篡改。
//创建处方
public function createRxinfo($tmpId,$name,$phone,$age,$sex){
$url = $this->url."createRxinfo";
list($s1, $s2) = explode(' ', microtime());
$timestamp = (float)sprintf('%.0f',((float)$s1 + (float)$s2) * 1000);
$disease_json = $this->queryIcdPage();
$rx_item_json = $this->get_drug();
$data = [
"ywid" => "xz".$timestamp,//自定义的id
"status" => -1,
"patient_name" => $name,
"patient_age" => $age,
"patient_sex" => $sex,
"patient_phone" => $phone,
"rx_item_json" => $rx_item_json,
"disease_json" => $disease_json,
"timestamp" =>$timestamp,
"api_code"=>$this->api_code,
"temp_id"=>$tmpId
];
$sign_str = $this->getSign($data);
$b_md5 = $sign_str . md5( $this->api_s);
$api_sign = strtolower( md5($b_md5) );
$data['api_sign'] = $api_sign;
$Data = $this->request($url,"POST",$data);
//var_dump($Data);die;
$Arr = json_decode($Data,1);
//var_dump($Arr);
return $Arr;
}
风控机制:系统自动拦截重复复用处方、超剂量购药、禁忌药搭配等异常订单,杜绝先购药后补处方的违规行为。
留存规范:问诊记录、处方原件、审方记录自动加密存储,保存周期满足法规 5 年留存要求,监管抽查可一键导出审计台账。
市面上简易线上工具普遍缺少实名留痕、订单熔断机制,一旦核查无法提供完整凭证,极易产生处罚风险。
二、用药安全确认功能:完善线上药学服务凭证
《药品经营质量管理规范》明确,药品销售需留存用药风险告知记录,线上渠道必须设置标准化确认环节。
分层风险提示:系统自动识别药品类目,针对处方药、特殊人群用药,弹窗展示过敏禁忌、肝肾负担、孕期慎用等专业提示。
强制确认机制:用户必须勾选阅读知情同意、确认知晓用药风险,未完成确认无法提交订单,自动留存电子确认凭证。
配套咨询通道:订单页面内置药师咨询入口,购药前后用药沟通记录自动归档,若出现用药不良反应纠纷,可作为门店履行药学服务的有效依据。
传统线上商城仅设置付款流程,缺少该环节会存在民事赔偿与行政处罚双重隐患。
/**
* 用药安全日志详情
* 通过 Service.getLogPage 获取数据(含 user/order 关联,含 nickname、phone)
* @param int $id
* @return mixed
*/
public function info($id)
{
if (!$id) {
return app('json')->fail('参数错误');
}
try {
// 使用 Service 层 getLogPage + id 搜索器(利用新增 searchIdAttr)获取含关联数据的记录
$result = $this->service->getLogPage(['id' => (int)$id]);
$list = $result['list'] ?? [];
if (empty($list)) {
return app('json')->fail('日志不存在');
}
$log = is_object($list[0]) ? $list[0]->toArray() : (array)$list[0];
} catch (\Throwable $e) {
Log::error('查看用药安全日志详情失败:' . $e->getMessage() . ',id=' . $id);
return app('json')->fail('查询失败');
}
// 格式化
$log['create_time_text'] = isset($log['create_time']) && $log['create_time']
? date('Y-m-d H:i:s', (int)$log['create_time'])
: '—';
$log['pop_confirm_type_text'] = self::POP_CONFIRM_TYPE_MAP[$log['pop_confirm_type'] ?? 'none'] ?? '无弹窗';
$log['nickname'] = $log['nickname'] ?? '—';
$log['phone'] = $log['phone'] ?? '—';
$log['patient_name'] = $log['patient_name'] ?? '—';
if ($log['patient_name'] === '—' && !empty($log['patient_id'])) {
$p = UserInquiry::find((int)$log['patient_id']);
$log['patient_name'] = $p ? ($p['patient_name'] ?? '—') : '—';
}
if ($log['patient_name'] === '—') {
$log['patient_name'] = $log['order_real_name'] ?? '—';
}
// 4项安全字段中文标注
$log['safe_fields_text'] = [];
foreach (self::SAFE_FIELD_MAP as $field => $label) {
$log['safe_fields_text'][] = [
'field' => $field,
'label' => $label,
'value' => isset($log[$field]) ? ($log[$field] ? '是' : '否') : '—',
];
}
return app('json')->success($log);
}
三、国标药品追溯码功能:实现药品一盒一码可查 依据《药品管理法》,药品经营主体需搭建全链路追溯体系,统一采用 Code128-C 20 位标准条码。
// 按商品分组获取可用追溯码,只获取所需数量
Db::startTrans();
try {
$allBoundCodes = [];
foreach ($orderGoods as $product) {
$productId = $product['product_id'];
$cartId = $product['cart_id'];
$needNum = $product['cart_num'] ?? 1;
if ($needNum <= 0) {
continue;
}
// 获取该商品的可用追溯码(在库且未删除),限制数量
$traceCodes = TraceStock::where('product_id', $productId)
->where('trace_status', TraceStock::TRACE_STATUS_IN_STOCK)
->where('is_deleted', 0)
->limit($needNum)
->select();
if ($traceCodes->isEmpty()) {
Log::warning('订单' . $orderId . ' 商品' . $productId . '无可用追溯码库存');
continue;
}
$boundCodes = [];
foreach ($traceCodes as $code) {
$success = TraceStock::lockSale($code['id'], $orderId);
if ($success) {
$boundCodes[] = $code['id'];
}
}
if (!empty($boundCodes)) {
$allBoundCodes = array_merge($allBoundCodes, $boundCodes);
$codeIdsStr = implode(',', $boundCodes);
// 更新该订单明细的追溯码ID字段
StoreOrderCartInfo::where('id', $cartId)
->update([
'trace_code_ids' => Db::raw("CONCAT(trace_code_ids, '" . $codeIdsStr . "')")
]);
}
}
Db::commit();
Log::info('订单' . $orderId . ' 溯源绑定完成,绑定码数:' . count($allBoundCodes));
} catch (\Exception $e) {
Db::rollback();
Log::error('订单' . $orderId . ' 溯源绑定事务失败:' . $e->getMessage());
return false;
}
} catch (\Throwable $e) {
Log::error('订单' . $orderId . ' 溯源绑定异常:' . $e->getMessage());
// 仅记录日志,不阻断订单流程
return false;
}
}
商家后台使用逻辑
双录入模式:支持扫码枪批量录入、文本手动导入追溯码,自动匹配对应药品规格;
双重校验规则:录入码数量与订单库存比对,货码不符、库存不足时锁定发货按钮;
商品分类适配:药品强制绑定追溯码,保健品、器械等非药品可跳过录入,提升发货效率。
用户前端查询逻辑
订单详情内置追溯码查看入口,弹窗展示标准条码,支持微信、支付宝扫码,一键调取药品生产、流通、销售全链条信息,满足公众溯源查询法定要求。
四、三大功能整体行业价值
作者:元岳ossign
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
随着线上售药监管规则持续细化,线上药店小程序不再只是简单的下单工具,电子处方、用药安全确认、药品追溯码是满足药监核查的三大核心基础功能,也是线上经营必备合规模块。
元岳科技在原电子处方的基础上,新增了用药安全日志和本地药品追溯的功能。
一、电子处方闭环功能:规范处方药线上流转
根据《处方药网络零售合规指南》要求,线上售卖处方药必须形成完整审方流程,该模块核心作用是搭建标准化处方留存链路。
使用流程:用户选购处方药后,需完成实名登记,上传复诊处方;系统仅开放执业药师实名账号审核,全程记录操作人、操作时间,日志不可篡改。
//创建处方
public function createRxinfo($tmpId,$name,$phone,$age,$sex){
$url = $this->url."createRxinfo";
list($s1, $s2) = explode(' ', microtime());
$timestamp = (float)sprintf('%.0f',((float)$s1 + (float)$s2) * 1000);
$disease_json = $this->queryIcdPage();
$rx_item_json = $this->get_drug();
$data = [
"ywid" => "xz".$timestamp,//自定义的id
"status" => -1,
"patient_name" => $name,
"patient_age" => $age,
"patient_sex" => $sex,
"patient_phone" => $phone,
"rx_item_json" => $rx_item_json,
"disease_json" => $disease_json,
"timestamp" =>$timestamp,
"api_code"=>$this->api_code,
"temp_id"=>$tmpId
];
$sign_str = $this->getSign($data);
$b_md5 = $sign_str . md5( $this->api_s);
$api_sign = strtolower( md5($b_md5) );
$data['api_sign'] = $api_sign;
$Data = $this->request($url,"POST",$data);
//var_dump($Data);die;
$Arr = json_decode($Data,1);
//var_dump($Arr);
return $Arr;
}
风控机制:系统自动拦截重复复用处方、超剂量购药、禁忌药搭配等异常订单,杜绝先购药后补处方的违规行为。
留存规范:问诊记录、处方原件、审方记录自动加密存储,保存周期满足法规 5 年留存要求,监管抽查可一键导出审计台账。
市面上简易线上工具普遍缺少实名留痕、订单熔断机制,一旦核查无法提供完整凭证,极易产生处罚风险。
二、用药安全确认功能:完善线上药学服务凭证
《药品经营质量管理规范》明确,药品销售需留存用药风险告知记录,线上渠道必须设置标准化确认环节。
分层风险提示:系统自动识别药品类目,针对处方药、特殊人群用药,弹窗展示过敏禁忌、肝肾负担、孕期慎用等专业提示。
强制确认机制:用户必须勾选阅读知情同意、确认知晓用药风险,未完成确认无法提交订单,自动留存电子确认凭证。
配套咨询通道:订单页面内置药师咨询入口,购药前后用药沟通记录自动归档,若出现用药不良反应纠纷,可作为门店履行药学服务的有效依据。
传统线上商城仅设置付款流程,缺少该环节会存在民事赔偿与行政处罚双重隐患。
/**
* 用药安全日志详情
* 通过 Service.getLogPage 获取数据(含 user/order 关联,含 nickname、phone)
* @param int $id
* @return mixed
*/
public function info($id)
{
if (!$id) {
return app('json')->fail('参数错误');
}
try {
// 使用 Service 层 getLogPage + id 搜索器(利用新增 searchIdAttr)获取含关联数据的记录
$result = $this->service->getLogPage(['id' => (int)$id]);
$list = $result['list'] ?? [];
if (empty($list)) {
return app('json')->fail('日志不存在');
}
$log = is_object($list[0]) ? $list[0]->toArray() : (array)$list[0];
} catch (\Throwable $e) {
Log::error('查看用药安全日志详情失败:' . $e->getMessage() . ',id=' . $id);
return app('json')->fail('查询失败');
}
// 格式化
$log['create_time_text'] = isset($log['create_time']) && $log['create_time']
? date('Y-m-d H:i:s', (int)$log['create_time'])
: '—';
$log['pop_confirm_type_text'] = self::POP_CONFIRM_TYPE_MAP[$log['pop_confirm_type'] ?? 'none'] ?? '无弹窗';
$log['nickname'] = $log['nickname'] ?? '—';
$log['phone'] = $log['phone'] ?? '—';
$log['patient_name'] = $log['patient_name'] ?? '—';
if ($log['patient_name'] === '—' && !empty($log['patient_id'])) {
$p = UserInquiry::find((int)$log['patient_id']);
$log['patient_name'] = $p ? ($p['patient_name'] ?? '—') : '—';
}
if ($log['patient_name'] === '—') {
$log['patient_name'] = $log['order_real_name'] ?? '—';
}
// 4项安全字段中文标注
$log['safe_fields_text'] = [];
foreach (self::SAFE_FIELD_MAP as $field => $label) {
$log['safe_fields_text'][] = [
'field' => $field,
'label' => $label,
'value' => isset($log[$field]) ? ($log[$field] ? '是' : '否') : '—',
];
}
return app('json')->success($log);
}
三、国标药品追溯码功能:实现药品一盒一码可查 依据《药品管理法》,药品经营主体需搭建全链路追溯体系,统一采用 Code128-C 20 位标准条码。
// 按商品分组获取可用追溯码,只获取所需数量
Db::startTrans();
try {
$allBoundCodes = [];
foreach ($orderGoods as $product) {
$productId = $product['product_id'];
$cartId = $product['cart_id'];
$needNum = $product['cart_num'] ?? 1;
if ($needNum <= 0) {
continue;
}
// 获取该商品的可用追溯码(在库且未删除),限制数量
$traceCodes = TraceStock::where('product_id', $productId)
->where('trace_status', TraceStock::TRACE_STATUS_IN_STOCK)
->where('is_deleted', 0)
->limit($needNum)
->select();
if ($traceCodes->isEmpty()) {
Log::warning('订单' . $orderId . ' 商品' . $productId . '无可用追溯码库存');
continue;
}
$boundCodes = [];
foreach ($traceCodes as $code) {
$success = TraceStock::lockSale($code['id'], $orderId);
if ($success) {
$boundCodes[] = $code['id'];
}
}
if (!empty($boundCodes)) {
$allBoundCodes = array_merge($allBoundCodes, $boundCodes);
$codeIdsStr = implode(',', $boundCodes);
// 更新该订单明细的追溯码ID字段
StoreOrderCartInfo::where('id', $cartId)
->update([
'trace_code_ids' => Db::raw("CONCAT(trace_code_ids, '" . $codeIdsStr . "')")
]);
}
}
Db::commit();
Log::info('订单' . $orderId . ' 溯源绑定完成,绑定码数:' . count($allBoundCodes));
} catch (\Exception $e) {
Db::rollback();
Log::error('订单' . $orderId . ' 溯源绑定事务失败:' . $e->getMessage());
return false;
}
} catch (\Throwable $e) {
Log::error('订单' . $orderId . ' 溯源绑定异常:' . $e->getMessage());
// 仅记录日志,不阻断订单流程
return false;
}
}
商家后台使用逻辑
双录入模式:支持扫码枪批量录入、文本手动导入追溯码,自动匹配对应药品规格;
双重校验规则:录入码数量与订单库存比对,货码不符、库存不足时锁定发货按钮;
商品分类适配:药品强制绑定追溯码,保健品、器械等非药品可跳过录入,提升发货效率。
用户前端查询逻辑
订单详情内置追溯码查看入口,弹窗展示标准条码,支持微信、支付宝扫码,一键调取药品生产、流通、销售全链条信息,满足公众溯源查询法定要求。
四、三大功能整体行业价值
作者:元岳ossign
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »DCloud 内置引擎库libnative-imagetranscoder.so、libweexcore.so、libweexjss.so 中的检测报警
libjpeg,mbed_tls,zlib这三个组件分别依赖于:
xxxx.apk.extracted/lib/armeabi-v7a/libnative-imagetranscoder.so
xxxx.apk.extracted/lib/armeabi-v7a/libweexcore.so
xxxx.apk.extracted/lib/armeabi-v7a/libweexjss.so
xxxx.apk.extracted/lib/arm64-v8a/libnative-imagetranscoder.so
xxxx.apk.extracted/lib/arm64-v8a/libweexcore.so
xxxx.apk.extracted/lib/arm64-v8a/libweexjss.so
我们程序做第三方漏洞审计,这些组件版本均被检测出有漏洞修复更新
具体审计问题见附件详情,官方重视一下尽快给出解决方案并修复问题漏洞
libjpeg,mbed_tls,zlib这三个组件分别依赖于:
xxxx.apk.extracted/lib/armeabi-v7a/libnative-imagetranscoder.so
xxxx.apk.extracted/lib/armeabi-v7a/libweexcore.so
xxxx.apk.extracted/lib/armeabi-v7a/libweexjss.so
xxxx.apk.extracted/lib/arm64-v8a/libnative-imagetranscoder.so
xxxx.apk.extracted/lib/arm64-v8a/libweexcore.so
xxxx.apk.extracted/lib/arm64-v8a/libweexjss.so
我们程序做第三方漏洞审计,这些组件版本均被检测出有漏洞修复更新
具体审计问题见附件详情,官方重视一下尽快给出解决方案并修复问题漏洞
收起阅读 »【Alpha 体验邀请】luch-request v4:面向 uni-app 重新设计的 TypeScript 请求库
大家好,我是 luch-request 的维护者。
luch-request v4 Alpha 现已发布到 npm,想邀请正在使用 uni-app 的开发者参与体验,帮助验证不同平台下的请求、上传下载、取消和原生 Task 行为。
npm install luch-request@alpha
v4 不是对 v3 的内部重构,而是一个 breaking version。它围绕 uni API、原生 Task、TypeScript 类型边界和多平台行为重新设计,希望在保留 uni 原生能力的同时,让请求配置、错误处理和取消行为更加明确。
当前版本为
4.0.0-alpha.1,公共 API 在稳定版前仍可能调整。建议先在测试项目或非核心业务中体验,并在自己的目标平台完成验证。
为什么重新设计 v4
uni-app 可以运行在 H5、App 和不同小程序平台,但各平台支持的请求参数、原生 Task、错误对象和取消能力并不完全一致。
如果直接套用浏览器 HTTP client 的设计,会遇到一些实际问题:
- 浏览器的 API 和类型不能代表所有 uni-app 平台;
request、uploadFile、downloadFile拥有不同的原生参数和 Task;- 平台新增参数的速度可能快于请求库和类型声明的更新;
- 网络失败、HTTP 状态码失败、取消和 JSON 解析失败需要不同的判断依据;
- TypeScript 类型过于宽松会失去约束,过于严格又可能阻碍平台新增能力。
因此,v4 没有把 v3 的内部结构继续扩展,而是重新确定了公共 API、配置合并、Interceptor、错误、取消和跨平台边界。
v4 带来了什么
1. TypeScript-first
请求响应、请求体和查询参数可以分别建模,常见调用保持直接:
import { createLuchRequest } from 'luch-request'
const http = createLuchRequest({
baseURL: 'https://api.example.com',
timeout: 10_000
})
interface User {
id: number
name: string
}
interface UserListParams {
page: number
keyword?: string
}
const response = await http.get<User[], UserListParams>('/users', {
params: {
page: 1
}
})
console.log(response.data)
JavaScript 项目仍然可以使用 v4;TypeScript 项目则能获得更完整的配置、响应、错误和公共导出类型。
2. 保留 uni 原生能力
v4 分别支持:
requestuploaddownload
三类操作共用实例配置、Interceptor 和错误契约,但在派发边界保留各自的原生参数,不使用 UPLOAD、DOWNLOAD 这样的伪 HTTP method。
3. 可以访问原生 Task
请求 Promise 提供 abort()、task 和 onTask(),单次请求配置也可以使用 onTask:
const request = http.get('/users', {
onTask(task) {
console.log('原生 Task:', task)
}
})
// 需要取消时
request.abort()
如果取消能力需要跨 service 层传递,可以使用 createCancelSource(),不必把请求 Promise 暴露给每一层调用方。
4. 明确的配置边界与 nativeOptions
v4 将配置分成不同职责:
- 常用 uni 请求参数可以直接配置;
- luch-request 自身行为放在
luchOptions; - 平台新增参数或插件尚未声明的参数通过
nativeOptions透传。
nativeOptions 是面向平台新能力的逃生窗口。当比较新的 uni API、某个平台参数或第三方插件参数还没有进入请求库类型时,使用方仍然可以显式透传,而不需要等待库发布新版本。
5. Interceptor 区分操作类型
v4 提供 request/response interceptor,支持同步和异步处理。Interceptor 上下文可以使用 LuchOperation 判断当前执行的是 request、upload 还是 download,避免依赖字符串或伪 method。
6. 统一错误与 JSON 解析策略
请求失败时统一抛出 LuchRequestError,并保留可用于判断的上下文,例如:
codeconfigresponsetaskcauserawcancelMode
网络失败、HTTP 状态码失败、取消和 JSON 解析失败可以分别判断。对于 JSON 解析失败,还可以通过 JSONParsingMode 选择抛出错误、保留文本等处理方式。
Alpha 阶段暂不包含什么
为了先稳定核心请求契约,首个版本不会内置以下能力:
- 自动重试
- 请求缓存
- 请求去重
- 并发控制
- Token 自动刷新
- WebSocket
uni_modules- uni-app x
这些能力并不是简单地全部塞进核心库。后续会根据真实使用场景,判断应该由 Interceptor、独立扩展还是核心能力提供。
v3 用户需要立即升级吗
不需要。
v3 稳定项目可以继续使用现有版本。v4 是 breaking version,适合以下开发者优先体验:
- 正在开发新项目;
- 希望获得更完整的 TypeScript 类型;
- 需要统一错误和取消行为;
- 需要访问原生 Task 或进度事件;
- 希望分别处理 request、upload 和 download;
- 愿意在自己的目标平台验证 Alpha 行为。
正式迁移前,请先阅读 v3 到 v4 的迁移文档,并在实际运行平台完成 smoke test。
希望大家重点帮助验证
自动化测试能够验证请求库逻辑,但不能代替所有真实设备、运行平台和 uni-app 版本。特别希望大家帮助验证:
- H5、App、微信小程序及其他小程序中的基础请求;
upload、download及进度事件;abort()是否触发平台原生中断;onTask()是否能取得预期的原生 Task;- 不同平台返回的错误结构是否存在差异;
nativeOptions是否能正确传递平台新增参数;- TypeScript 类型是否存在过严、过松或无法表达的场景;
- JavaScript 项目中的配置和错误行为是否足够清晰。
如何反馈
提交问题时,如果方便,请尽量提供以下信息:
运行平台:
HBuilderX / uni-app 版本:
luch-request 版本:
使用 TypeScript 还是 JavaScript:
最小复现代码:
预期结果:
实际结果:
错误对象或控制台日志:
如果问题与取消、上传下载或原生 Task 有关,也请说明实际运行平台,以及是否能取得对应 Task。
相关链接
- v4 文档:https://luch-request.quanzhan.co/
- GitHub:https://github.com/lei-mu/luch-request
- 问题反馈:https://github.com/lei-mu/luch-request/issues
- npm:https://www.npmjs.com/package/luch-request
- v3 稳定文档:https://v3.luch-request.quanzhan.co/
写在最后
v4 的核心设计和自动化测试已经完成,但 uni-app 请求库真正困难的部分始终是多平台运行差异。
希望正在使用 uni-app 的开发者帮助验证 request、upload、download、取消和原生 Task 等能力。无论是兼容性问题、类型设计建议,还是某个平台特有的行为差异,都欢迎在 GitHub Issue 或本帖中反馈。
感谢每一位愿意安装、运行和提供反馈的开发者。
大家好,我是 luch-request 的维护者。
luch-request v4 Alpha 现已发布到 npm,想邀请正在使用 uni-app 的开发者参与体验,帮助验证不同平台下的请求、上传下载、取消和原生 Task 行为。
npm install luch-request@alpha
v4 不是对 v3 的内部重构,而是一个 breaking version。它围绕 uni API、原生 Task、TypeScript 类型边界和多平台行为重新设计,希望在保留 uni 原生能力的同时,让请求配置、错误处理和取消行为更加明确。
当前版本为
4.0.0-alpha.1,公共 API 在稳定版前仍可能调整。建议先在测试项目或非核心业务中体验,并在自己的目标平台完成验证。
为什么重新设计 v4
uni-app 可以运行在 H5、App 和不同小程序平台,但各平台支持的请求参数、原生 Task、错误对象和取消能力并不完全一致。
如果直接套用浏览器 HTTP client 的设计,会遇到一些实际问题:
- 浏览器的 API 和类型不能代表所有 uni-app 平台;
request、uploadFile、downloadFile拥有不同的原生参数和 Task;- 平台新增参数的速度可能快于请求库和类型声明的更新;
- 网络失败、HTTP 状态码失败、取消和 JSON 解析失败需要不同的判断依据;
- TypeScript 类型过于宽松会失去约束,过于严格又可能阻碍平台新增能力。
因此,v4 没有把 v3 的内部结构继续扩展,而是重新确定了公共 API、配置合并、Interceptor、错误、取消和跨平台边界。
v4 带来了什么
1. TypeScript-first
请求响应、请求体和查询参数可以分别建模,常见调用保持直接:
import { createLuchRequest } from 'luch-request'
const http = createLuchRequest({
baseURL: 'https://api.example.com',
timeout: 10_000
})
interface User {
id: number
name: string
}
interface UserListParams {
page: number
keyword?: string
}
const response = await http.get<User[], UserListParams>('/users', {
params: {
page: 1
}
})
console.log(response.data)
JavaScript 项目仍然可以使用 v4;TypeScript 项目则能获得更完整的配置、响应、错误和公共导出类型。
2. 保留 uni 原生能力
v4 分别支持:
requestuploaddownload
三类操作共用实例配置、Interceptor 和错误契约,但在派发边界保留各自的原生参数,不使用 UPLOAD、DOWNLOAD 这样的伪 HTTP method。
3. 可以访问原生 Task
请求 Promise 提供 abort()、task 和 onTask(),单次请求配置也可以使用 onTask:
const request = http.get('/users', {
onTask(task) {
console.log('原生 Task:', task)
}
})
// 需要取消时
request.abort()
如果取消能力需要跨 service 层传递,可以使用 createCancelSource(),不必把请求 Promise 暴露给每一层调用方。
4. 明确的配置边界与 nativeOptions
v4 将配置分成不同职责:
- 常用 uni 请求参数可以直接配置;
- luch-request 自身行为放在
luchOptions; - 平台新增参数或插件尚未声明的参数通过
nativeOptions透传。
nativeOptions 是面向平台新能力的逃生窗口。当比较新的 uni API、某个平台参数或第三方插件参数还没有进入请求库类型时,使用方仍然可以显式透传,而不需要等待库发布新版本。
5. Interceptor 区分操作类型
v4 提供 request/response interceptor,支持同步和异步处理。Interceptor 上下文可以使用 LuchOperation 判断当前执行的是 request、upload 还是 download,避免依赖字符串或伪 method。
6. 统一错误与 JSON 解析策略
请求失败时统一抛出 LuchRequestError,并保留可用于判断的上下文,例如:
codeconfigresponsetaskcauserawcancelMode
网络失败、HTTP 状态码失败、取消和 JSON 解析失败可以分别判断。对于 JSON 解析失败,还可以通过 JSONParsingMode 选择抛出错误、保留文本等处理方式。
Alpha 阶段暂不包含什么
为了先稳定核心请求契约,首个版本不会内置以下能力:
- 自动重试
- 请求缓存
- 请求去重
- 并发控制
- Token 自动刷新
- WebSocket
uni_modules- uni-app x
这些能力并不是简单地全部塞进核心库。后续会根据真实使用场景,判断应该由 Interceptor、独立扩展还是核心能力提供。
v3 用户需要立即升级吗
不需要。
v3 稳定项目可以继续使用现有版本。v4 是 breaking version,适合以下开发者优先体验:
- 正在开发新项目;
- 希望获得更完整的 TypeScript 类型;
- 需要统一错误和取消行为;
- 需要访问原生 Task 或进度事件;
- 希望分别处理 request、upload 和 download;
- 愿意在自己的目标平台验证 Alpha 行为。
正式迁移前,请先阅读 v3 到 v4 的迁移文档,并在实际运行平台完成 smoke test。
希望大家重点帮助验证
自动化测试能够验证请求库逻辑,但不能代替所有真实设备、运行平台和 uni-app 版本。特别希望大家帮助验证:
- H5、App、微信小程序及其他小程序中的基础请求;
upload、download及进度事件;abort()是否触发平台原生中断;onTask()是否能取得预期的原生 Task;- 不同平台返回的错误结构是否存在差异;
nativeOptions是否能正确传递平台新增参数;- TypeScript 类型是否存在过严、过松或无法表达的场景;
- JavaScript 项目中的配置和错误行为是否足够清晰。
如何反馈
提交问题时,如果方便,请尽量提供以下信息:
运行平台:
HBuilderX / uni-app 版本:
luch-request 版本:
使用 TypeScript 还是 JavaScript:
最小复现代码:
预期结果:
实际结果:
错误对象或控制台日志:
如果问题与取消、上传下载或原生 Task 有关,也请说明实际运行平台,以及是否能取得对应 Task。
相关链接
- v4 文档:https://luch-request.quanzhan.co/
- GitHub:https://github.com/lei-mu/luch-request
- 问题反馈:https://github.com/lei-mu/luch-request/issues
- npm:https://www.npmjs.com/package/luch-request
- v3 稳定文档:https://v3.luch-request.quanzhan.co/
写在最后
v4 的核心设计和自动化测试已经完成,但 uni-app 请求库真正困难的部分始终是多平台运行差异。
希望正在使用 uni-app 的开发者帮助验证 request、upload、download、取消和原生 Task 等能力。无论是兼容性问题、类型设计建议,还是某个平台特有的行为差异,都欢迎在 GitHub Issue 或本帖中反馈。
感谢每一位愿意安装、运行和提供反馈的开发者。
收起阅读 »输入组件获焦时实现提示文本的动态效果
问题现象
在使用TextInput和TextArea组件进行输入时,如何实现提示文字的动态效果?
背景知识
组件的某些通用属性变化时,可以通过animation属性动画实现渐变过渡效果,提升用户体验。
TextInput是HarmonyOS提供的一种单行文本输入框组件。
TextArea是多行文本输入框组件,当输入的文本内容超过组件宽度时会自动换行显示。
attributeModifier接口支持动态属性设置,支持开发者在属性设置时使用if/else语法,且根据需要使用多态样式设置属性。
解决方案
在Stack组件中创建Text提示文本和TextInput输入框。
创建TextModifier类,实现attributeModifier接口,该类中的applyNormalAttribute方法可根据对应的boolean类型的值动态修改对应组件的样式属性。
当点击TextInput组件时,执行onFocus方法,若文本值为空,将对应的TextModifier类的属性装载至该组件上,从而实现animation动画效果。
完整示例参考如下:
@Entry
@Component
struct TextInputTestOne {
// 定义状态变量
message: string = 'Hello World';
textAreaValue: string = '';
textInputValue: string = '';
selectValue: string = '';
@State modifierOne: TextModifier = new TextModifier(50, 150, 20);
@State modifierTwo: TextModifier = new TextModifier(50, 0, 20);
opacityOne: number = 0.3;
opacityTwo: number = 0.3;
build() {
Column() {
Column() {
Stack() {
Text('请在这输入文字:')
.fontSize(this.modifierOne.fontSize)
.textAlign(TextAlign.Start)
.fontColor(Color.Black)
.margin({ left: this.modifierOne.marginLeft, bottom: this.modifierOne.marginBottom })
.attributeModifier(this.modifierOne)
.width('100%')
.opacity(this.opacityOne)
// 实现动画效果
.animation({
duration: 300,
curve: Curve.Friction,
iterations: 1,
playMode: PlayMode.Normal
});
TextArea({ text: $$this.textAreaValue })
// 聚焦回调时间
.onFocus(() => {
if (this.textAreaValue === '') {
this.modifierOne.isFocus = true;
this.opacityOne = 1;
}
})
// 失焦回调事件
.onBlur(() => {
if (this.textAreaValue === '') {
this.modifierOne.isFocus = false;
this.opacityOne = 0.3;
}
})
.height('80%');
};
}
.justifyContent(FlexAlign.Center)
.height('30%')
.margin({ left: 20, right: 20 });
Column() {
Stack() {
Text('请在这输入文字:')
.fontSize(this.modifierTwo.fontSize)
.fontColor(Color.Black)
.textAlign(TextAlign.Start)
.width('100%')
.margin({ left: this.modifierTwo.marginLeft, bottom: this.modifierTwo.marginBottom })
.attributeModifier(this.modifierTwo)
.opacity(this.opacityTwo)
// 实现动画效果
.animation({
duration: 300,
curve: Curve.Friction,
iterations: 1,
playMode: PlayMode.Normal
});
TextInput({ text: $$this.textInputValue })
// 聚焦回调时间
.onFocus(() => {
if (this.textInputValue === '') {
this.modifierTwo.isFocus = true;
this.opacityTwo = 1;
}
})
// 失焦回调事件
.onBlur(() => {
if (this.textInputValue === '') {
this.modifierTwo.isFocus = false;
this.opacityTwo = 0.3;
}
});
};
}
.justifyContent(FlexAlign.Center)
.height('30%')
.margin({ left: 20, right: 20 });
}
.height('100%')
.width('100%');
}
}
class TextModifier implements AttributeModifier<TextAttribute> {
isFocus: boolean | undefined = undefined;
marginLeft: number = 0;
marginBottom: number = 0;
fontSize: number = 0;
// 绑定属性
applyNormalAttribute(instance: TextAttribute): void {
if (this.isFocus) {
this.marginLeft -= 10;
this.marginBottom += 40;
this.fontSize -= 7;
instance
.fontSize(this.fontSize)
.margin({ left: this.marginLeft, bottom: this.marginBottom });
} else if (this.isFocus === false) {
this.marginLeft += 10;
this.marginBottom -= 40;
this.fontSize += 7;
instance
.fontSize(this.fontSize)
.margin({ left: this.marginLeft, bottom: this.marginBottom });
}
}
constructor(marginLeft: number, marginBottom: number, fontSize: number) {
this.marginLeft = marginLeft;
this.marginBottom = marginBottom;
this.fontSize = fontSize;
}
}
https://coub.com/view/4b7gqt
https://coub.com/view/4b7gqq
https://coub.com/view/4b7gql
https://coub.com/view/4b7gqi
https://coub.com/view/4b7gqg
https://coub.com/view/4b7gqe
https://coub.com/view/4b7gqb
https://coub.com/view/4b7gq9
https://coub.com/view/4b7gq6
https://coub.com/view/4b7gq3
https://coub.com/view/4b7gq1
https://coub.com/view/4b7gpz
https://coub.com/view/4b7gpw
https://coub.com/view/4b7gpe
https://coub.com/view/4b7gpa
https://coub.com/view/4b7gp8
https://coub.com/view/4b7gp7
https://coub.com/view/4b7gp6
https://coub.com/view/4b7gp5
https://coub.com/view/4b7gp1
https://coub.com/view/4b7gow
https://coub.com/view/4b7gou
https://coub.com/view/4b7goq
https://coub.com/view/4b7goj
https://coub.com/view/4b7goh
https://coub.com/view/4b7gog
https://coub.com/view/4b7go2
https://coub.com/view/4b7gnz
https://coub.com/view/4b7gnf
https://coub.com/view/4b7gne
https://coub.com/view/4b7gnd
https://coub.com/view/4b7gnb
https://coub.com/view/4b7gmd
https://coub.com/view/4b7gmb
https://coub.com/view/4b7gm6
https://coub.com/view/4b7gm1
https://coub.com/view/4b7glo
https://coub.com/view/4b7glk
https://coub.com/view/4b7glh
https://coub.com/view/4b7glg
https://coub.com/view/4b7glf
https://coub.com/view/4b7glc
https://coub.com/view/4b7glb
https://coub.com/view/4b7gl9
https://coub.com/view/4b7gl4
https://coub.com/view/4b7gl1
https://coub.com/view/4b7gkp
https://coub.com/view/4b7gkh
https://coub.com/view/4b7gk8
https://coub.com/view/4b7gk2
https://coub.com/view/4b7gju
https://coub.com/view/4b7gjn
https://coub.com/view/4b7gjl
https://coub.com/view/4b7gjj
https://coub.com/view/4b7gji
https://coub.com/view/4b7gjg
https://coub.com/view/4b7gjf
https://coub.com/view/4b7gje
https://coub.com/view/4b7gjb
https://coub.com/view/4b7gj7
https://coub.com/view/4b7gj5
https://coub.com/view/4b7gj2
https://coub.com/view/4b7giy
https://coub.com/view/4b7giw
https://coub.com/view/4b7giv
https://coub.com/view/4b7giu
https://coub.com/view/4b7gis
https://coub.com/view/4b7giq
https://coub.com/view/4b7gio
https://coub.com/view/4b7gil
https://coub.com/view/4b7gij
https://coub.com/view/4b7gie
https://coub.com/view/4b7gic
https://coub.com/view/4b7gia
https://coub.com/view/4b7gi7
https://coub.com/view/4b7gi2
https://coub.com/view/4b7ghx
https://coub.com/view/4b7ghi
https://coub.com/view/4b7ghh
https://coub.com/view/4b7ghd
https://coub.com/view/4b7ggo
https://coub.com/view/4b7ggn
https://coub.com/view/4b7ggl
https://coub.com/view/4b7ggg
https://coub.com/view/4b7gge
https://coub.com/view/4b7ggd
https://coub.com/view/4b7ggc
https://coub.com/view/4b7ggb
https://coub.com/view/4b7gga
https://coub.com/view/4b7gg7
https://coub.com/view/4b7gg1
https://coub.com/view/4b7gfw
https://coub.com/view/4b7gft
https://coub.com/view/4b7gfs
https://coub.com/view/4b7gfq
https://coub.com/view/4b7gfm
https://coub.com/view/4b77kh
https://coub.com/view/4b77kd
https://coub.com/view/4b77ka
https://coub.com/view/4b77k8
https://coub.com/view/4b77k4
https://coub.com/view/4b77k2
https://coub.com/view/4b77k0
https://coub.com/view/4b77jz
https://coub.com/view/4b77ju
https://coub.com/view/4b77jk
https://coub.com/view/4b77jf
https://coub.com/view/4b77ja
https://coub.com/view/4b77j1
https://coub.com/view/4b77iz
问题现象
在使用TextInput和TextArea组件进行输入时,如何实现提示文字的动态效果?
背景知识
组件的某些通用属性变化时,可以通过animation属性动画实现渐变过渡效果,提升用户体验。
TextInput是HarmonyOS提供的一种单行文本输入框组件。
TextArea是多行文本输入框组件,当输入的文本内容超过组件宽度时会自动换行显示。
attributeModifier接口支持动态属性设置,支持开发者在属性设置时使用if/else语法,且根据需要使用多态样式设置属性。
解决方案
在Stack组件中创建Text提示文本和TextInput输入框。
创建TextModifier类,实现attributeModifier接口,该类中的applyNormalAttribute方法可根据对应的boolean类型的值动态修改对应组件的样式属性。
当点击TextInput组件时,执行onFocus方法,若文本值为空,将对应的TextModifier类的属性装载至该组件上,从而实现animation动画效果。
完整示例参考如下:
@Entry
@Component
struct TextInputTestOne {
// 定义状态变量
message: string = 'Hello World';
textAreaValue: string = '';
textInputValue: string = '';
selectValue: string = '';
@State modifierOne: TextModifier = new TextModifier(50, 150, 20);
@State modifierTwo: TextModifier = new TextModifier(50, 0, 20);
opacityOne: number = 0.3;
opacityTwo: number = 0.3;
build() {
Column() {
Column() {
Stack() {
Text('请在这输入文字:')
.fontSize(this.modifierOne.fontSize)
.textAlign(TextAlign.Start)
.fontColor(Color.Black)
.margin({ left: this.modifierOne.marginLeft, bottom: this.modifierOne.marginBottom })
.attributeModifier(this.modifierOne)
.width('100%')
.opacity(this.opacityOne)
// 实现动画效果
.animation({
duration: 300,
curve: Curve.Friction,
iterations: 1,
playMode: PlayMode.Normal
});
TextArea({ text: $$this.textAreaValue })
// 聚焦回调时间
.onFocus(() => {
if (this.textAreaValue === '') {
this.modifierOne.isFocus = true;
this.opacityOne = 1;
}
})
// 失焦回调事件
.onBlur(() => {
if (this.textAreaValue === '') {
this.modifierOne.isFocus = false;
this.opacityOne = 0.3;
}
})
.height('80%');
};
}
.justifyContent(FlexAlign.Center)
.height('30%')
.margin({ left: 20, right: 20 });
Column() {
Stack() {
Text('请在这输入文字:')
.fontSize(this.modifierTwo.fontSize)
.fontColor(Color.Black)
.textAlign(TextAlign.Start)
.width('100%')
.margin({ left: this.modifierTwo.marginLeft, bottom: this.modifierTwo.marginBottom })
.attributeModifier(this.modifierTwo)
.opacity(this.opacityTwo)
// 实现动画效果
.animation({
duration: 300,
curve: Curve.Friction,
iterations: 1,
playMode: PlayMode.Normal
});
TextInput({ text: $$this.textInputValue })
// 聚焦回调时间
.onFocus(() => {
if (this.textInputValue === '') {
this.modifierTwo.isFocus = true;
this.opacityTwo = 1;
}
})
// 失焦回调事件
.onBlur(() => {
if (this.textInputValue === '') {
this.modifierTwo.isFocus = false;
this.opacityTwo = 0.3;
}
});
};
}
.justifyContent(FlexAlign.Center)
.height('30%')
.margin({ left: 20, right: 20 });
}
.height('100%')
.width('100%');
}
}
class TextModifier implements AttributeModifier<TextAttribute> {
isFocus: boolean | undefined = undefined;
marginLeft: number = 0;
marginBottom: number = 0;
fontSize: number = 0;
// 绑定属性
applyNormalAttribute(instance: TextAttribute): void {
if (this.isFocus) {
this.marginLeft -= 10;
this.marginBottom += 40;
this.fontSize -= 7;
instance
.fontSize(this.fontSize)
.margin({ left: this.marginLeft, bottom: this.marginBottom });
} else if (this.isFocus === false) {
this.marginLeft += 10;
this.marginBottom -= 40;
this.fontSize += 7;
instance
.fontSize(this.fontSize)
.margin({ left: this.marginLeft, bottom: this.marginBottom });
}
}
constructor(marginLeft: number, marginBottom: number, fontSize: number) {
this.marginLeft = marginLeft;
this.marginBottom = marginBottom;
this.fontSize = fontSize;
}
}
https://coub.com/view/4b7gqt
https://coub.com/view/4b7gqq
https://coub.com/view/4b7gql
https://coub.com/view/4b7gqi
https://coub.com/view/4b7gqg
https://coub.com/view/4b7gqe
https://coub.com/view/4b7gqb
https://coub.com/view/4b7gq9
https://coub.com/view/4b7gq6
https://coub.com/view/4b7gq3
https://coub.com/view/4b7gq1
https://coub.com/view/4b7gpz
https://coub.com/view/4b7gpw
https://coub.com/view/4b7gpe
https://coub.com/view/4b7gpa
https://coub.com/view/4b7gp8
https://coub.com/view/4b7gp7
https://coub.com/view/4b7gp6
https://coub.com/view/4b7gp5
https://coub.com/view/4b7gp1
https://coub.com/view/4b7gow
https://coub.com/view/4b7gou
https://coub.com/view/4b7goq
https://coub.com/view/4b7goj
https://coub.com/view/4b7goh
https://coub.com/view/4b7gog
https://coub.com/view/4b7go2
https://coub.com/view/4b7gnz
https://coub.com/view/4b7gnf
https://coub.com/view/4b7gne
https://coub.com/view/4b7gnd
https://coub.com/view/4b7gnb
https://coub.com/view/4b7gmd
https://coub.com/view/4b7gmb
https://coub.com/view/4b7gm6
https://coub.com/view/4b7gm1
https://coub.com/view/4b7glo
https://coub.com/view/4b7glk
https://coub.com/view/4b7glh
https://coub.com/view/4b7glg
https://coub.com/view/4b7glf
https://coub.com/view/4b7glc
https://coub.com/view/4b7glb
https://coub.com/view/4b7gl9
https://coub.com/view/4b7gl4
https://coub.com/view/4b7gl1
https://coub.com/view/4b7gkp
https://coub.com/view/4b7gkh
https://coub.com/view/4b7gk8
https://coub.com/view/4b7gk2
https://coub.com/view/4b7gju
https://coub.com/view/4b7gjn
https://coub.com/view/4b7gjl
https://coub.com/view/4b7gjj
https://coub.com/view/4b7gji
https://coub.com/view/4b7gjg
https://coub.com/view/4b7gjf
https://coub.com/view/4b7gje
https://coub.com/view/4b7gjb
https://coub.com/view/4b7gj7
https://coub.com/view/4b7gj5
https://coub.com/view/4b7gj2
https://coub.com/view/4b7giy
https://coub.com/view/4b7giw
https://coub.com/view/4b7giv
https://coub.com/view/4b7giu
https://coub.com/view/4b7gis
https://coub.com/view/4b7giq
https://coub.com/view/4b7gio
https://coub.com/view/4b7gil
https://coub.com/view/4b7gij
https://coub.com/view/4b7gie
https://coub.com/view/4b7gic
https://coub.com/view/4b7gia
https://coub.com/view/4b7gi7
https://coub.com/view/4b7gi2
https://coub.com/view/4b7ghx
https://coub.com/view/4b7ghi
https://coub.com/view/4b7ghh
https://coub.com/view/4b7ghd
https://coub.com/view/4b7ggo
https://coub.com/view/4b7ggn
https://coub.com/view/4b7ggl
https://coub.com/view/4b7ggg
https://coub.com/view/4b7gge
https://coub.com/view/4b7ggd
https://coub.com/view/4b7ggc
https://coub.com/view/4b7ggb
https://coub.com/view/4b7gga
https://coub.com/view/4b7gg7
https://coub.com/view/4b7gg1
https://coub.com/view/4b7gfw
https://coub.com/view/4b7gft
https://coub.com/view/4b7gfs
https://coub.com/view/4b7gfq
https://coub.com/view/4b7gfm
https://coub.com/view/4b77kh
https://coub.com/view/4b77kd
https://coub.com/view/4b77ka
https://coub.com/view/4b77k8
https://coub.com/view/4b77k4
https://coub.com/view/4b77k2
https://coub.com/view/4b77k0
https://coub.com/view/4b77jz
https://coub.com/view/4b77ju
https://coub.com/view/4b77jk
https://coub.com/view/4b77jf
https://coub.com/view/4b77ja
https://coub.com/view/4b77j1
https://coub.com/view/4b77iz
如何实现V1、V2版本之间父子组件的数据传递与共享
问题现象
场景一:如何实现V1版本组件向V2版本组件传递与共享数据?
场景二:如何实现V2版本组件向V1版本组件传递与共享数据?
背景知识
状态管理(V1):ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。
状态管理(V2):为了增强状态管理V1版本的部分能力,例如深度观察、属性级更新等,ArkUI推出状态管理V2供开发者使用。
在实现V1、V2版本之间父子组件的数据传递与共享的功能前,应先了解V1、V2版本混用的规则:
API19前,详见官方文档状态管理V1和V2混用指导(API version 19前):概述、限制条件。
API19后,详见官方文档状态管理V1和V2混用指导(API version 19及之后):限制条件。
UIUtils.enableV2Compatibility:API19新增的方法,用于实现V1版本的数据可以在V2版本中使用的能力。若想要将V2版本的数据传递到V1版本的子组件,可以通过将V1版本的数据声明在V2版本的组件中,然后传递给V1版本的子组件使用。
解决方案
根据混用规则总结可实现的方案如下:
传递方向
API
方案
实现方式
V1->V2
API 19以后
使用UIUtils.enableV2Compatibility
在V1组件向V2组件传递参数时,通过UIUtils.enableV2Compatibility方法把V1的数据转化为能被V2装饰器@Param装饰器接收的数据。
API 19以前
使用AppStorage/AppStorageV2
使用AppStorageV2.connect传递。
使用参数传递
方式一:在V1组件内直接声明V2装饰器装饰的实例,不使用V1装饰器初始化,该实例可直接传递给V2版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类。
方式三:在V1组件内通过V1版本装饰器声明的变量,在传递至V2版本的子组件时,用“{}”在外部构造为新的对象后,再传递给V2版本子组件。
V2->V1
API 19以后
使用UIUtils.enableV2Compatibility
在V2组件向V1组件传递参数时,通过UIUtils.enableV2Compatibility方法将V1版本@Observed装饰器装饰的类转化为能被@Local装饰器直接声明的数据。
API 19以前
使用AppStorage/AppStorageV2
使用AppStorageV2.connect传递。
使用参数传递
方式一:在V2组件内直接声明V1装饰器装饰的实例,不使用V2装饰器初始化,该实例可直接传递给V1版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类。
方式三:在V2组件内通过V2版本装饰器声明的变量,在传递至V1版本的子组件时,用“{}”在外部构造为新的对象后,再传递给V1版本子组件。
场景一:实现V1版本组件向V2版本组件传递与共享数据。
方案一:API19及以后,使用UIUtils.enableV2Compatibility方法,详情参考官网链接:V1中使用V2的自定义组件。
方案二:全局获取方式,通过AppStorage/AppStorageV2实现,适用于API19以前,API19以后建议使用方案一。实现思路如下:
由于AppStorage中的@StorageLink只能在V1组件中使用,所以AppStorage在V1向V2传递的场景下,V2版本的子组件不能自动同步更新,只能通过AppStorage.get手动获取。建议通过AppStorageV2实现V1版本组件向V2版本组件传递与共享数据的功能。
在使用AppStorageV2时,可以搭配@ObservedV2/@Trace实现深度观测。
由于AppStorageV2和@ObservedV2/@Trace是V2版本的装饰器,所以他们修饰的数据在V1子组件内初始化时,不能用V1的装饰器装饰。
完整示例代码如下:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class V1ToV2ExampleTwo {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
@Entry
@Component
struct V1ToV2Two {
exampleAppStorageV2: V1ToV2ExampleTwo =
AppStorageV2.connect<V1ToV2ExampleTwo>(V1ToV2ExampleTwo, () => new V1ToV2ExampleTwo('1'))!; // 不可直接使用V1装饰器装饰
build() {
Column({ space: 10 }) {
Text(V1父组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V1 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
Column() {
}
.height(50);
V1ToV2TwoChild();
}.width('100%');
}
}
@ComponentV2
struct V1ToV2TwoChild {
@Local exampleAppStorageV2: V1ToV2ExampleTwo =
AppStorageV2.connect<V1ToV2ExampleTwo>(V1ToV2ExampleTwo, () => new V1ToV2ExampleTwo('1'))!;
build() {
Column({ space: 10 }) {
Text(V2子组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V2 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
};
}
}
实现效果如下:
点击放大
方案三:传参方式,该方案一共三种传递形式,适用于API19以前,API19以后建议使用方案一。
该场景下由于@Observed/@ObjectLink装饰器需要搭配使用,但是V2版本的子组件不能使用@ObjectLink装饰器,所以该场景下,不建议使用@Observed装饰器装饰的类作为传递的参数。使用ObservedV2/@Trace装饰器装饰的类,作为传递的参数。实现思路如下:
方式一:在V1组件内直接声明V2装饰器装饰的实例,不使用V1装饰器初始化,该实例可直接传递给V2版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类,就可以实现在V1组件内用V1装饰器实例化该嵌套类。传递给V2版本的子组件时,将该嵌套对象内部的对象单独传递给V2版本的子组件即可。
方式三:在V1组件内通过V1版本装饰器声明的变量,在传递至V2版本的子组件时,用“{}”在外部构造为新的对象后,就可以允许传递给V2版本子组件。
注意
若方式三在V1组件内通过V1版本装饰器声明的变量是简单类型如string、number等,要实现在子组件中能修改父组件的变量,需要在父组件内实现一个修改该变量的函数,传递给子组件。子组件通过@Event接收,需要修改时直接调用该函数即可。
若方式三传递的是对象,由于@ObservedV2/@Trace装饰器装饰的类不能在V1组件内用V1版本的装饰器初始化,所以需要创建一个不带装饰器的类,然后才能用V1装饰器在V1组件内初始化。
完整示例代码如下:
@ObservedV2
class V1ToV2ExampleThreeV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V1ToV2ExampleThree {
text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V1ToV2ExampleThreeFather {
example: V1ToV2ExampleThreeV2 = new V1ToV2ExampleThreeV2('1');
}
interface V1ToV2ExampleThreeFace {
text: string;
}
interface V1ToV2ExampleThreeFatherFace {
example: V1ToV2ExampleThree;
}
@Entry
@Component
struct V1ToV2Three {
// 方式一,不使用V1装饰器在V1组件内初始化V2装饰器@ObservedV2/@Trace装饰的类
exampleOne: V1ToV2ExampleThreeV2 = new V1ToV2ExampleThreeV2('1');
// 方式二,将V2装饰器@ObservedV2/@Trace装饰的类嵌套进不带任何装饰器装饰的类里,再进行初始化
@State exampleTwo: V1ToV2ExampleThreeFather = new V1ToV2ExampleThreeFather();
// 方式三,传递简单类型
@State text: string = '1';
// 方式三,传递对象
@State exampleThreeChange: V1ToV2ExampleThree = new V1ToV2ExampleThree('1');
build() {
Column({ space: 10 }) {
Text(V1父组件,方式一,参数传递:${this.exampleOne.text});
Text(V1父组件,方式二,参数传递:${this.exampleTwo.example.text});
Text(V1父组件,方式三,简单类型参数传递:${this.text});
Text(V1父组件,方式三,对象参数传递:${this.exampleThreeChange.text});
Button('V1 中修改')
.onClick(() => {
// 方式一,数据在V1子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V1子组件中修改方式
this.exampleTwo.example.text += '1';
// 方式三,传递简单类型在V1子组件中修改方式
this.text += '1';
// 方式三,传递对象在V1子组件中修改方式
this.exampleThreeChange.text += '1';
});
Column() {
}
.height(20);
V1ToV2ThreeChild({
// 方式一,可直接传递
exampleOne: this.exampleOne,
// 方式二,需要传递的数据为属性
exampleTwo: this.exampleTwo.example,
// 方式三,将字符串作为对象的属性传递
exampleThree: {
text: this.text
},
changeText: () => {
this.text += '1';
},
// 方式三,将声明的对象作为属性传递
exampleThreeChange: {
example: this.exampleThreeChange
}
});
}.width('100%');
}
}
@ComponentV2
struct V1ToV2ThreeChild {
// 方式一接收的数据
@Param @Require exampleOne: V1ToV2ExampleThreeV2;
// 方式二接收的数据
@Param @Require exampleTwo: V1ToV2ExampleThreeV2;
// 方式三(传递简单类型)接收的数据
@Param @Require exampleThree: V1ToV2ExampleThreeFace;
// 父组件传递的是简单类型变量时必备,不然无法同步修改父组件字符串内容
@Event changeText: () => void = () => {
};
// 方式三(传递对象)接收的数据
@Param @Require exampleThreeChange: V1ToV2ExampleThreeFatherFace;
build() {
Column({ space: 10 }) {
Text(V2子组件,方式一,参数传递:${this.exampleOne.text});
Text(V2子组件,方式二,参数传递:${this.exampleTwo.text});
Text(V2子组件,方式三,简单类型参数传递:${this.exampleThree.text});
Text(V2子组件,方式三,对象参数传递:${this.exampleThreeChange.example.text});
Button('V2 中修改')
.onClick(() => {
// 方式一,数据在V2子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V2子组件中修改方式
this.exampleTwo.text += '1';
// 方式三,传递简单类型在V2子组件中修改方式
this.changeText();
// 方式三,传递对象在V2子组件中修改方式
this.exampleThreeChange.example.text += '1';
});
};
}
}
实现效果如下:
点击放大
场景二:实现V2版本组件向V1版本组件传递与共享数据。
方案一:API19之后,使用UIUtils.enableV2Compatibility方法,详情可参考官网链接:V2中使用V1的自定义组件。
方案二:全局获取方式,通过AppStorage/AppStorageV2实现,适用于API19以前,API19以后建议使用方案一。
该方案与场景一中方案二实现思路基本一致,额外补充如下:
V2版本的父组件不能通过AppStorage中的@StorageLink自动同步刷新,只能通过AppStorage.get手动获取,建议通过AppStorageV2实现。
完整示例代码如下:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class V2ToV1ExampleTwoV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
@Entry
@ComponentV2
struct V2ToV1Two {
@Local exampleAppStorageV2: V2ToV1ExampleTwoV2 =
AppStorageV2.connect<V2ToV1ExampleTwoV2>(V2ToV1ExampleTwoV2, () => new V2ToV1ExampleTwoV2('1'))!;
build() {
Column({ space: 10 }) {
Text(V2父组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V2 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
Column() {
}
.height(50);
V2ToV1TwoChild();
}.width('100%');
}
}
@Component
struct V2ToV1TwoChild {
exampleAppStorageV2: V2ToV1ExampleTwoV2 =
AppStorageV2.connect<V2ToV1ExampleTwoV2>(V2ToV1ExampleTwoV2, () => new V2ToV1ExampleTwoV2('1'))!; // 不可直接使用V1装饰器装饰
build() {
Column({ space: 10 }) {
Text(V1子组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V1 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
};
}
}
实现效果如下:
点击放大
方案三:传参方式,适用于API19以前,API19以后建议使用方案一。
该方案与场景一中方案三实现思路基本一致,额外补充如下:
该场景下V2组件作为父组件,V1组件作为子组件。@ObjectLink装饰器可以在V1子组件内使用,所以传递的参数既可以是@Observed装饰器装饰的类,也可以是@ObservedV2/@Trace装饰器装饰的类(@ObservedV2/@Trace装饰器的场景参考下文“方式三,对象参数传递”的场景)。
V2组件作为子组件时,只能通过@Param装饰器接收参数,但是当V1组件作为子组件时,可以通过@Prop、@ObjectLink、@Link三种装饰器接收参数。但是根据知识背景中的混用校验规则,@Link只能被V1状态变量初始化,详情见@Link变量的传递/访问规则说明,所以使用@Prop、@ObjectLink接收V2父组件传递来的参数。
在实现“方式三,对象参数传递”的场景时,由于@ObservedV2/@Trace装饰的类可以直接在V2组件内声明,所以将@ObservedV2/@Trace装饰的类实例化后作为属性传递即可。
完整示例代码如下:
@Observed
class V2ToV1ExampleThreeV1 {
text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V2ToV1ExampleThreeFather {
example: V2ToV1ExampleThreeV1 = new V2ToV1ExampleThreeV1('1');
}
@ObservedV2
class V2ToV1ExampleThreeV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
interface V2ToV1ExampleThreeFatherV2Face {
example: V2ToV1ExampleThreeV2;
}
interface V2ToV1ExampleThreeFace {
text: string;
}
@Entry
@ComponentV2
struct V2ToV1Three {
// 方式一,不使用V2装饰器在V2组件内初始化V1装饰器@Observed装饰的类
exampleOne: V2ToV1ExampleThreeV1 = new V2ToV1ExampleThreeV1('1');
// 方式二,将V1装饰器@Observed装饰的类嵌套进不带任何装饰器装饰的类里,再进行初始化
@Local exampleTwo: V2ToV1ExampleThreeFather = new V2ToV1ExampleThreeFather();
// 方式三,传递简单类型
@Local text: string = '1';
// 方式三,@ObservedV2装饰的类的实例化后传递
@Local exampleFour: V2ToV1ExampleThreeV2 = new V2ToV1ExampleThreeV2('1');
build() {
Column({ space: 10 }) {
Text(V2父组件,方式一,参数传递:${this.exampleOne.text}); // 不刷新
Text(V2父组件,方式二,参数传递:${this.exampleTwo.example.text}); // 不刷新
Text(V2父组件,方式三,简单类型参数传递:${this.text}); // 刷新
Text(V2父组件,方式三,对象参数传递:${this.exampleFour.text}); // 刷新
Button('V2 中修改')
.onClick(() => {
// 方式一,数据在V2父组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V2父组件中修改方式
this.exampleTwo.example.text += '1';
// 方式三,传递简单类型在V2父组件中修改方式
this.text += '1';
// 方式三,传递@ObservedV2装饰的对象在V2父组件中修改方式
this.exampleFour.text += '1';
});
Column() {
}
.height(20);
V2ToV1ThreeChild({
// 方式一,可直接传递
exampleOne: this.exampleOne,
// 方式二,传递的是封装后的属性
exampleTwo: this.exampleTwo.example,
// 方式三,将字符串作为对象的属性传递
exampleThree: {
text: this.text
},
changeText: () => {
this.text += '1';
},
// 方式三,将声明的@ObservedV2装饰的对象作为属性传递
exampleFour: {
example: this.exampleFour
}
});
}.width('100%');
}
}
@Component
struct V2ToV1ThreeChild {
// 方式一接收的数据
@Prop exampleOne: V2ToV1ExampleThreeV1;
// 方式二接受的数据
@ObjectLink exampleTwo: V2ToV1ExampleThreeV1;
// 方式三(传递简单类型)接收的数据
@Prop exampleThree: V2ToV1ExampleThreeFace;
// 父组件传递的是简单类型变量时必备,不然无法同步修改父组件字符串内容
changeText: () => void = () => {
};
// 方式三(传递对象)接收的数据
@ObjectLink exampleFour: V2ToV1ExampleThreeFatherV2Face;
build() {
Column({ space: 10 }) {
Text(V1子组件,方式一,参数传递:${this.exampleOne.text});
Text(V1子组件,方式二,参数传递:${this.exampleTwo.text});
Text(V1子组件,方式三,简单类型参数传递:${this.exampleThree.text});
Text(V1子组件,方式三,对象参数传递:${this.exampleFour.example.text});
Button('V1 中修改')
.onClick(() => {
// 方式一,数据在V1子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V1子组件中修改方式
this.exampleTwo.text += '1';
// 方式三,传递简单类型在V1子组件中修改方式
this.changeText();
// 方式三,传递对象在V1子组件中修改方式
this.exampleFour.example.text += '1';
});
};
}
}
实现效果如下:
点击放大
注意
@Observed/@ObjectLink可以实现父组件修改深层属性后,刷新子组件对应属性绑定的UI的功能,但是当子组件内修改深层属性,父组件并不会刷新对应属性绑定的UI。所以除了通过方式三以外的方式一、方式二不会刷新父组件绑定的对应属性的UI。
常见FAQ
Q:使用Navigation导航的应用在V1版本中用的是@Provide/@Consume装饰器传递的NavPathStack路由栈。在V1迁移V2过程中,Navigation主页被修改为V2版本,子页依旧是保持的V1版本。此时应该怎样传递NavPathStack路由栈给子页面?
A:由于@ComponentV2内不能使用V1版本的@Provide/@Consume装饰器,@Component内不能使用V2版本的@Provider/@Consumer装饰器,所以当父子组件分别属于V1、V2版本时,不能使用@Provider/@Consumer、@Provide/@Consume去实现组件见页面的共享与数据传递。可参考解决方案如下:
使用AppStorage存储或者获取数据:在Navigation主页面通过AppStorage.setOrCreate进行全局存储,在V1版本的NavDestination子页面内直接通过@StorageLink进行全局路由栈获取。
使用AppStorageV2存储或者获取数据:分别在父组件与子组件通过pathStack: NavPathStack = AppStorageV2.connect(NavPathStack, () => new NavPathStack())!;创建或获取全局路由栈。
如果页面中使用了NavDestination组件,可以通过onReady事件获取路由栈:.onReady((context: NavDestinationContext) => {this.pathInfos = context.pathStack}),从而避免状态管理版本冲突的问题。
通过自定义组件查询接口获取,也可以避免状态管理传递冲突的问题,参考queryNavigationInfo。
Q:在V1迁移V2的过程中,存在部分三方库是V1版本实现,还没适配V2版本,应该如何将V2版本的数据传递给V1版本三方库?
A:建议参考场景二中方案一,方案三实现。该场景下,方案二AppStorageV2.connect没办法写进三方库内。
总结
在API19之后,优先推荐使用官方提供的UIUtils.enableV2Compatibility方法实现V1、V2版本之间父子组件的数据传递与共享。
https://coub.com/view/4b707t
https://coub.com/view/4b707r
https://coub.com/view/4b707o
https://coub.com/view/4b707n
https://coub.com/view/4b707h
https://coub.com/view/4b707d
https://coub.com/view/4b7077
https://coub.com/view/4b7074
https://coub.com/view/4b706z
https://coub.com/view/4b706u
https://coub.com/view/4b706o
https://coub.com/view/4b706c
https://coub.com/view/4b7063
https://coub.com/view/4b7060
https://coub.com/view/4b705v
https://coub.com/view/4b705n
https://coub.com/view/4b705k
https://coub.com/view/4b701z
https://coub.com/view/4b6zjc
https://coub.com/view/4b6zja
https://coub.com/view/4b6zj9
https://coub.com/view/4b6zj6
https://coub.com/view/4b6zj4
https://coub.com/view/4b6zj3
https://coub.com/view/4b6zj2
https://coub.com/view/4b6zj1
https://coub.com/view/4b6ziz
https://coub.com/view/4b6zix
https://coub.com/view/4b6ziv
https://coub.com/view/4b6ziu
https://coub.com/view/4b6zit
https://coub.com/view/4b6zis
https://coub.com/view/4b6zio
https://coub.com/view/4b6zin
https://coub.com/view/4b6zim
https://coub.com/view/4b6zik
问题现象
场景一:如何实现V1版本组件向V2版本组件传递与共享数据?
场景二:如何实现V2版本组件向V1版本组件传递与共享数据?
背景知识
状态管理(V1):ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。
状态管理(V2):为了增强状态管理V1版本的部分能力,例如深度观察、属性级更新等,ArkUI推出状态管理V2供开发者使用。
在实现V1、V2版本之间父子组件的数据传递与共享的功能前,应先了解V1、V2版本混用的规则:
API19前,详见官方文档状态管理V1和V2混用指导(API version 19前):概述、限制条件。
API19后,详见官方文档状态管理V1和V2混用指导(API version 19及之后):限制条件。
UIUtils.enableV2Compatibility:API19新增的方法,用于实现V1版本的数据可以在V2版本中使用的能力。若想要将V2版本的数据传递到V1版本的子组件,可以通过将V1版本的数据声明在V2版本的组件中,然后传递给V1版本的子组件使用。
解决方案
根据混用规则总结可实现的方案如下:
传递方向
API
方案
实现方式
V1->V2
API 19以后
使用UIUtils.enableV2Compatibility
在V1组件向V2组件传递参数时,通过UIUtils.enableV2Compatibility方法把V1的数据转化为能被V2装饰器@Param装饰器接收的数据。
API 19以前
使用AppStorage/AppStorageV2
使用AppStorageV2.connect传递。
使用参数传递
方式一:在V1组件内直接声明V2装饰器装饰的实例,不使用V1装饰器初始化,该实例可直接传递给V2版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类。
方式三:在V1组件内通过V1版本装饰器声明的变量,在传递至V2版本的子组件时,用“{}”在外部构造为新的对象后,再传递给V2版本子组件。
V2->V1
API 19以后
使用UIUtils.enableV2Compatibility
在V2组件向V1组件传递参数时,通过UIUtils.enableV2Compatibility方法将V1版本@Observed装饰器装饰的类转化为能被@Local装饰器直接声明的数据。
API 19以前
使用AppStorage/AppStorageV2
使用AppStorageV2.connect传递。
使用参数传递
方式一:在V2组件内直接声明V1装饰器装饰的实例,不使用V2装饰器初始化,该实例可直接传递给V1版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类。
方式三:在V2组件内通过V2版本装饰器声明的变量,在传递至V1版本的子组件时,用“{}”在外部构造为新的对象后,再传递给V1版本子组件。
场景一:实现V1版本组件向V2版本组件传递与共享数据。
方案一:API19及以后,使用UIUtils.enableV2Compatibility方法,详情参考官网链接:V1中使用V2的自定义组件。
方案二:全局获取方式,通过AppStorage/AppStorageV2实现,适用于API19以前,API19以后建议使用方案一。实现思路如下:
由于AppStorage中的@StorageLink只能在V1组件中使用,所以AppStorage在V1向V2传递的场景下,V2版本的子组件不能自动同步更新,只能通过AppStorage.get手动获取。建议通过AppStorageV2实现V1版本组件向V2版本组件传递与共享数据的功能。
在使用AppStorageV2时,可以搭配@ObservedV2/@Trace实现深度观测。
由于AppStorageV2和@ObservedV2/@Trace是V2版本的装饰器,所以他们修饰的数据在V1子组件内初始化时,不能用V1的装饰器装饰。
完整示例代码如下:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class V1ToV2ExampleTwo {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
@Entry
@Component
struct V1ToV2Two {
exampleAppStorageV2: V1ToV2ExampleTwo =
AppStorageV2.connect<V1ToV2ExampleTwo>(V1ToV2ExampleTwo, () => new V1ToV2ExampleTwo('1'))!; // 不可直接使用V1装饰器装饰
build() {
Column({ space: 10 }) {
Text(V1父组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V1 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
Column() {
}
.height(50);
V1ToV2TwoChild();
}.width('100%');
}
}
@ComponentV2
struct V1ToV2TwoChild {
@Local exampleAppStorageV2: V1ToV2ExampleTwo =
AppStorageV2.connect<V1ToV2ExampleTwo>(V1ToV2ExampleTwo, () => new V1ToV2ExampleTwo('1'))!;
build() {
Column({ space: 10 }) {
Text(V2子组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V2 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
};
}
}
实现效果如下:
点击放大
方案三:传参方式,该方案一共三种传递形式,适用于API19以前,API19以后建议使用方案一。
该场景下由于@Observed/@ObjectLink装饰器需要搭配使用,但是V2版本的子组件不能使用@ObjectLink装饰器,所以该场景下,不建议使用@Observed装饰器装饰的类作为传递的参数。使用ObservedV2/@Trace装饰器装饰的类,作为传递的参数。实现思路如下:
方式一:在V1组件内直接声明V2装饰器装饰的实例,不使用V1装饰器初始化,该实例可直接传递给V2版本的子组件。
方式二:创建一个不带任何装饰器装饰的类,内部再嵌套一个V2装饰器装饰的类,就可以实现在V1组件内用V1装饰器实例化该嵌套类。传递给V2版本的子组件时,将该嵌套对象内部的对象单独传递给V2版本的子组件即可。
方式三:在V1组件内通过V1版本装饰器声明的变量,在传递至V2版本的子组件时,用“{}”在外部构造为新的对象后,就可以允许传递给V2版本子组件。
注意
若方式三在V1组件内通过V1版本装饰器声明的变量是简单类型如string、number等,要实现在子组件中能修改父组件的变量,需要在父组件内实现一个修改该变量的函数,传递给子组件。子组件通过@Event接收,需要修改时直接调用该函数即可。
若方式三传递的是对象,由于@ObservedV2/@Trace装饰器装饰的类不能在V1组件内用V1版本的装饰器初始化,所以需要创建一个不带装饰器的类,然后才能用V1装饰器在V1组件内初始化。
完整示例代码如下:
@ObservedV2
class V1ToV2ExampleThreeV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V1ToV2ExampleThree {
text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V1ToV2ExampleThreeFather {
example: V1ToV2ExampleThreeV2 = new V1ToV2ExampleThreeV2('1');
}
interface V1ToV2ExampleThreeFace {
text: string;
}
interface V1ToV2ExampleThreeFatherFace {
example: V1ToV2ExampleThree;
}
@Entry
@Component
struct V1ToV2Three {
// 方式一,不使用V1装饰器在V1组件内初始化V2装饰器@ObservedV2/@Trace装饰的类
exampleOne: V1ToV2ExampleThreeV2 = new V1ToV2ExampleThreeV2('1');
// 方式二,将V2装饰器@ObservedV2/@Trace装饰的类嵌套进不带任何装饰器装饰的类里,再进行初始化
@State exampleTwo: V1ToV2ExampleThreeFather = new V1ToV2ExampleThreeFather();
// 方式三,传递简单类型
@State text: string = '1';
// 方式三,传递对象
@State exampleThreeChange: V1ToV2ExampleThree = new V1ToV2ExampleThree('1');
build() {
Column({ space: 10 }) {
Text(V1父组件,方式一,参数传递:${this.exampleOne.text});
Text(V1父组件,方式二,参数传递:${this.exampleTwo.example.text});
Text(V1父组件,方式三,简单类型参数传递:${this.text});
Text(V1父组件,方式三,对象参数传递:${this.exampleThreeChange.text});
Button('V1 中修改')
.onClick(() => {
// 方式一,数据在V1子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V1子组件中修改方式
this.exampleTwo.example.text += '1';
// 方式三,传递简单类型在V1子组件中修改方式
this.text += '1';
// 方式三,传递对象在V1子组件中修改方式
this.exampleThreeChange.text += '1';
});
Column() {
}
.height(20);
V1ToV2ThreeChild({
// 方式一,可直接传递
exampleOne: this.exampleOne,
// 方式二,需要传递的数据为属性
exampleTwo: this.exampleTwo.example,
// 方式三,将字符串作为对象的属性传递
exampleThree: {
text: this.text
},
changeText: () => {
this.text += '1';
},
// 方式三,将声明的对象作为属性传递
exampleThreeChange: {
example: this.exampleThreeChange
}
});
}.width('100%');
}
}
@ComponentV2
struct V1ToV2ThreeChild {
// 方式一接收的数据
@Param @Require exampleOne: V1ToV2ExampleThreeV2;
// 方式二接收的数据
@Param @Require exampleTwo: V1ToV2ExampleThreeV2;
// 方式三(传递简单类型)接收的数据
@Param @Require exampleThree: V1ToV2ExampleThreeFace;
// 父组件传递的是简单类型变量时必备,不然无法同步修改父组件字符串内容
@Event changeText: () => void = () => {
};
// 方式三(传递对象)接收的数据
@Param @Require exampleThreeChange: V1ToV2ExampleThreeFatherFace;
build() {
Column({ space: 10 }) {
Text(V2子组件,方式一,参数传递:${this.exampleOne.text});
Text(V2子组件,方式二,参数传递:${this.exampleTwo.text});
Text(V2子组件,方式三,简单类型参数传递:${this.exampleThree.text});
Text(V2子组件,方式三,对象参数传递:${this.exampleThreeChange.example.text});
Button('V2 中修改')
.onClick(() => {
// 方式一,数据在V2子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V2子组件中修改方式
this.exampleTwo.text += '1';
// 方式三,传递简单类型在V2子组件中修改方式
this.changeText();
// 方式三,传递对象在V2子组件中修改方式
this.exampleThreeChange.example.text += '1';
});
};
}
}
实现效果如下:
点击放大
场景二:实现V2版本组件向V1版本组件传递与共享数据。
方案一:API19之后,使用UIUtils.enableV2Compatibility方法,详情可参考官网链接:V2中使用V1的自定义组件。
方案二:全局获取方式,通过AppStorage/AppStorageV2实现,适用于API19以前,API19以后建议使用方案一。
该方案与场景一中方案二实现思路基本一致,额外补充如下:
V2版本的父组件不能通过AppStorage中的@StorageLink自动同步刷新,只能通过AppStorage.get手动获取,建议通过AppStorageV2实现。
完整示例代码如下:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class V2ToV1ExampleTwoV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
@Entry
@ComponentV2
struct V2ToV1Two {
@Local exampleAppStorageV2: V2ToV1ExampleTwoV2 =
AppStorageV2.connect<V2ToV1ExampleTwoV2>(V2ToV1ExampleTwoV2, () => new V2ToV1ExampleTwoV2('1'))!;
build() {
Column({ space: 10 }) {
Text(V2父组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V2 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
Column() {
}
.height(50);
V2ToV1TwoChild();
}.width('100%');
}
}
@Component
struct V2ToV1TwoChild {
exampleAppStorageV2: V2ToV1ExampleTwoV2 =
AppStorageV2.connect<V2ToV1ExampleTwoV2>(V2ToV1ExampleTwoV2, () => new V2ToV1ExampleTwoV2('1'))!; // 不可直接使用V1装饰器装饰
build() {
Column({ space: 10 }) {
Text(V1子组件,AppStorageV2:${this.exampleAppStorageV2.text});
Button('V1 中修改')
.onClick(() => {
this.exampleAppStorageV2.text += '1';
});
};
}
}
实现效果如下:
点击放大
方案三:传参方式,适用于API19以前,API19以后建议使用方案一。
该方案与场景一中方案三实现思路基本一致,额外补充如下:
该场景下V2组件作为父组件,V1组件作为子组件。@ObjectLink装饰器可以在V1子组件内使用,所以传递的参数既可以是@Observed装饰器装饰的类,也可以是@ObservedV2/@Trace装饰器装饰的类(@ObservedV2/@Trace装饰器的场景参考下文“方式三,对象参数传递”的场景)。
V2组件作为子组件时,只能通过@Param装饰器接收参数,但是当V1组件作为子组件时,可以通过@Prop、@ObjectLink、@Link三种装饰器接收参数。但是根据知识背景中的混用校验规则,@Link只能被V1状态变量初始化,详情见@Link变量的传递/访问规则说明,所以使用@Prop、@ObjectLink接收V2父组件传递来的参数。
在实现“方式三,对象参数传递”的场景时,由于@ObservedV2/@Trace装饰的类可以直接在V2组件内声明,所以将@ObservedV2/@Trace装饰的类实例化后作为属性传递即可。
完整示例代码如下:
@Observed
class V2ToV1ExampleThreeV1 {
text: string = '1';
constructor(text: string) {
this.text = text;
}
}
class V2ToV1ExampleThreeFather {
example: V2ToV1ExampleThreeV1 = new V2ToV1ExampleThreeV1('1');
}
@ObservedV2
class V2ToV1ExampleThreeV2 {
@Trace text: string = '1';
constructor(text: string) {
this.text = text;
}
}
interface V2ToV1ExampleThreeFatherV2Face {
example: V2ToV1ExampleThreeV2;
}
interface V2ToV1ExampleThreeFace {
text: string;
}
@Entry
@ComponentV2
struct V2ToV1Three {
// 方式一,不使用V2装饰器在V2组件内初始化V1装饰器@Observed装饰的类
exampleOne: V2ToV1ExampleThreeV1 = new V2ToV1ExampleThreeV1('1');
// 方式二,将V1装饰器@Observed装饰的类嵌套进不带任何装饰器装饰的类里,再进行初始化
@Local exampleTwo: V2ToV1ExampleThreeFather = new V2ToV1ExampleThreeFather();
// 方式三,传递简单类型
@Local text: string = '1';
// 方式三,@ObservedV2装饰的类的实例化后传递
@Local exampleFour: V2ToV1ExampleThreeV2 = new V2ToV1ExampleThreeV2('1');
build() {
Column({ space: 10 }) {
Text(V2父组件,方式一,参数传递:${this.exampleOne.text}); // 不刷新
Text(V2父组件,方式二,参数传递:${this.exampleTwo.example.text}); // 不刷新
Text(V2父组件,方式三,简单类型参数传递:${this.text}); // 刷新
Text(V2父组件,方式三,对象参数传递:${this.exampleFour.text}); // 刷新
Button('V2 中修改')
.onClick(() => {
// 方式一,数据在V2父组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V2父组件中修改方式
this.exampleTwo.example.text += '1';
// 方式三,传递简单类型在V2父组件中修改方式
this.text += '1';
// 方式三,传递@ObservedV2装饰的对象在V2父组件中修改方式
this.exampleFour.text += '1';
});
Column() {
}
.height(20);
V2ToV1ThreeChild({
// 方式一,可直接传递
exampleOne: this.exampleOne,
// 方式二,传递的是封装后的属性
exampleTwo: this.exampleTwo.example,
// 方式三,将字符串作为对象的属性传递
exampleThree: {
text: this.text
},
changeText: () => {
this.text += '1';
},
// 方式三,将声明的@ObservedV2装饰的对象作为属性传递
exampleFour: {
example: this.exampleFour
}
});
}.width('100%');
}
}
@Component
struct V2ToV1ThreeChild {
// 方式一接收的数据
@Prop exampleOne: V2ToV1ExampleThreeV1;
// 方式二接受的数据
@ObjectLink exampleTwo: V2ToV1ExampleThreeV1;
// 方式三(传递简单类型)接收的数据
@Prop exampleThree: V2ToV1ExampleThreeFace;
// 父组件传递的是简单类型变量时必备,不然无法同步修改父组件字符串内容
changeText: () => void = () => {
};
// 方式三(传递对象)接收的数据
@ObjectLink exampleFour: V2ToV1ExampleThreeFatherV2Face;
build() {
Column({ space: 10 }) {
Text(V1子组件,方式一,参数传递:${this.exampleOne.text});
Text(V1子组件,方式二,参数传递:${this.exampleTwo.text});
Text(V1子组件,方式三,简单类型参数传递:${this.exampleThree.text});
Text(V1子组件,方式三,对象参数传递:${this.exampleFour.example.text});
Button('V1 中修改')
.onClick(() => {
// 方式一,数据在V1子组件中修改方式
this.exampleOne.text += '1';
// 方式二,数据在V1子组件中修改方式
this.exampleTwo.text += '1';
// 方式三,传递简单类型在V1子组件中修改方式
this.changeText();
// 方式三,传递对象在V1子组件中修改方式
this.exampleFour.example.text += '1';
});
};
}
}
实现效果如下:
点击放大
注意
@Observed/@ObjectLink可以实现父组件修改深层属性后,刷新子组件对应属性绑定的UI的功能,但是当子组件内修改深层属性,父组件并不会刷新对应属性绑定的UI。所以除了通过方式三以外的方式一、方式二不会刷新父组件绑定的对应属性的UI。
常见FAQ
Q:使用Navigation导航的应用在V1版本中用的是@Provide/@Consume装饰器传递的NavPathStack路由栈。在V1迁移V2过程中,Navigation主页被修改为V2版本,子页依旧是保持的V1版本。此时应该怎样传递NavPathStack路由栈给子页面?
A:由于@ComponentV2内不能使用V1版本的@Provide/@Consume装饰器,@Component内不能使用V2版本的@Provider/@Consumer装饰器,所以当父子组件分别属于V1、V2版本时,不能使用@Provider/@Consumer、@Provide/@Consume去实现组件见页面的共享与数据传递。可参考解决方案如下:
使用AppStorage存储或者获取数据:在Navigation主页面通过AppStorage.setOrCreate进行全局存储,在V1版本的NavDestination子页面内直接通过@StorageLink进行全局路由栈获取。
使用AppStorageV2存储或者获取数据:分别在父组件与子组件通过pathStack: NavPathStack = AppStorageV2.connect(NavPathStack, () => new NavPathStack())!;创建或获取全局路由栈。
如果页面中使用了NavDestination组件,可以通过onReady事件获取路由栈:.onReady((context: NavDestinationContext) => {this.pathInfos = context.pathStack}),从而避免状态管理版本冲突的问题。
通过自定义组件查询接口获取,也可以避免状态管理传递冲突的问题,参考queryNavigationInfo。
Q:在V1迁移V2的过程中,存在部分三方库是V1版本实现,还没适配V2版本,应该如何将V2版本的数据传递给V1版本三方库?
A:建议参考场景二中方案一,方案三实现。该场景下,方案二AppStorageV2.connect没办法写进三方库内。
总结
在API19之后,优先推荐使用官方提供的UIUtils.enableV2Compatibility方法实现V1、V2版本之间父子组件的数据传递与共享。
https://coub.com/view/4b707t
https://coub.com/view/4b707r
https://coub.com/view/4b707o
https://coub.com/view/4b707n
https://coub.com/view/4b707h
https://coub.com/view/4b707d
https://coub.com/view/4b7077
https://coub.com/view/4b7074
https://coub.com/view/4b706z
https://coub.com/view/4b706u
https://coub.com/view/4b706o
https://coub.com/view/4b706c
https://coub.com/view/4b7063
https://coub.com/view/4b7060
https://coub.com/view/4b705v
https://coub.com/view/4b705n
https://coub.com/view/4b705k
https://coub.com/view/4b701z
https://coub.com/view/4b6zjc
https://coub.com/view/4b6zja
https://coub.com/view/4b6zj9
https://coub.com/view/4b6zj6
https://coub.com/view/4b6zj4
https://coub.com/view/4b6zj3
https://coub.com/view/4b6zj2
https://coub.com/view/4b6zj1
https://coub.com/view/4b6ziz
https://coub.com/view/4b6zix
https://coub.com/view/4b6ziv
https://coub.com/view/4b6ziu
https://coub.com/view/4b6zit
https://coub.com/view/4b6zis
https://coub.com/view/4b6zio
https://coub.com/view/4b6zin
https://coub.com/view/4b6zim
https://coub.com/view/4b6zik
推荐一款 护航跑刀电竞下单系统,仓库开源 前后端
仓库地址
https://gitee.com/zhangshangshidai/dailianhuhang
电竞护航下单系统 | UniApp + ThinkPHP6
2026年最火的游戏模式,三角洲等游戏的护航陪玩模式,面向游戏工作室、代练工作室、电竞护航工作室打造的订单管理交易系统,一套完整前后端开源方案,快速搭建电竞护航、游戏代练线上下单平台。
🛠️ 技术栈
前端:UniApp,支持编译 H5、微信小程序、App 多端
后端:ThinkPHP6 RESTful API
数据库:MySQL,支持 Redis 缓存拓展
✨ 核心功能
后台管理端
游戏分类管理,自定义游戏列表
护航商品发布,支持多规格价格、多属性配置、多图片上传
订单统一管理,接单操作,录入打手信息,订单完结处理
用户管理、资金配置、支付渠道配置、退款审核
用户客户端
用户注册登录,个人中心
浏览护航商品,在线下单
支付方式:微信支付、账户余额支付
完整订单流程:待接单、进行中、已完成、退款申请
订单详情查询,提交退款申请
余额明细、消费记录查看
🎯 项目优势
专为电竞护航、游戏代练工作室业务场景定制,贴合行业流程
UniApp 多端适配,小程序 / H5/App 任选上线
商品支持多规格多价位,满足不同段位、时长护航套餐需求
订单闭环完整:下单→支付→后台接单→登记打手→完成 / 退款
双支付模式,余额体系便于工作室老客户复购结算
代码分层清晰,易于二次开发、自定义功能迭代
📌 适用主体
电竞护航工作室、游戏代练工作室、游戏陪玩工作室、线上游戏服务交易团队
🚀 部署简述
后端部署 PHP 环境,导入数据库,配置数据库与支付参数
HBuilderX 打开 UniApp 前端项目,修改后端接口地址
编译打包对应端(小程序 / H5/App)即可投入使用
仓库地址
https://gitee.com/zhangshangshidai/dailianhuhang
电竞护航下单系统 | UniApp + ThinkPHP6
2026年最火的游戏模式,三角洲等游戏的护航陪玩模式,面向游戏工作室、代练工作室、电竞护航工作室打造的订单管理交易系统,一套完整前后端开源方案,快速搭建电竞护航、游戏代练线上下单平台。
🛠️ 技术栈
前端:UniApp,支持编译 H5、微信小程序、App 多端
后端:ThinkPHP6 RESTful API
数据库:MySQL,支持 Redis 缓存拓展
✨ 核心功能
后台管理端
游戏分类管理,自定义游戏列表
护航商品发布,支持多规格价格、多属性配置、多图片上传
订单统一管理,接单操作,录入打手信息,订单完结处理
用户管理、资金配置、支付渠道配置、退款审核
用户客户端
用户注册登录,个人中心
浏览护航商品,在线下单
支付方式:微信支付、账户余额支付
完整订单流程:待接单、进行中、已完成、退款申请
订单详情查询,提交退款申请
余额明细、消费记录查看
🎯 项目优势
专为电竞护航、游戏代练工作室业务场景定制,贴合行业流程
UniApp 多端适配,小程序 / H5/App 任选上线
商品支持多规格多价位,满足不同段位、时长护航套餐需求
订单闭环完整:下单→支付→后台接单→登记打手→完成 / 退款
双支付模式,余额体系便于工作室老客户复购结算
代码分层清晰,易于二次开发、自定义功能迭代
📌 适用主体
电竞护航工作室、游戏代练工作室、游戏陪玩工作室、线上游戏服务交易团队
🚀 部署简述
后端部署 PHP 环境,导入数据库,配置数据库与支付参数
HBuilderX 打开 UniApp 前端项目,修改后端接口地址
编译打包对应端(小程序 / H5/App)即可投入使用
如何解决Repeat渲染数据重复显示问题
问题现象
使用Repeat实现列表功能,加载新数据会出现重复显示问题,一般有下面几种问题场景:
常见问题场景
问题描述
场景一
滑动加载新数据,在aboutToAppear方法里处理数据逻辑,导致数据重复。
场景二
Repeat与@Builder混用场景下,传参错误,导致数据重复。
场景一:滑动加载新数据,在aboutToAppear方法里处理数据逻辑,问题如图所示:
点击放大
场景二:进行Repeat与@Builder混用场景下,传参使用错误,导致数据重复,问题如图所示:
点击放大
背景知识
Repeat根据容器组件的有效加载范围(屏幕可视区域+预加载区域)加载子组件。当容器滑动/数组改变时,Repeat会根据父容器组件的布局过程重新计算有效加载范围,并管理列表子组件节点的创建与销毁。Repeat通过组件节点更新/复用从而优化性能表现。
节点更新复用能力说明:当滚动容器组件滑动/数组改变时,Repeat将失效的子组件节点(离开容器组件的显示区域和预加载区域)加入空闲节点缓存池中,即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时,对缓存池里的组件节点进行复用。
aboutToAppear:aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build()函数中生效。
问题定位
场景一:
根据问题代码分析,数据更新是在子组件的aboutToAppear方法里完成的,aboutToAppear在组件被创建时触发,之后不会再触发。因为Repeat提供了节点复用的能力,后续加载的是之前复用的节点,不会走aboutToAppear方法,导致数据没有更新。
问题代码如下:
@ComponentV2
struct LowCodeTitleView{
@Param ids: string = ''
@Local changes:string = ''
aboutToAppear(): void {
this.changes = this.ids // 数据更新是在子组件的aboutToAppear方法里完成的
}
build() {
Column(){
Text(this.changes+"标题内容")
}
}
}
@Entry
@ComponentV2
struct Index {
@Local dataArr: Array<string> = [];
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(data_${i}); // 为数组添加一些数据
}
}
build() {
RelativeContainer() {
List({ space: 3 }) {
Repeat<string>(this.dataArr)
.each((ri: RepeatItem<string>) => {
ListItem() {
LowCodeTitleView({
ids:ri.item
})
}
})
.key((item: string, index: number): string => "__test_lowcode--"+index)
.virtualScroll({ totalCount: this.dataArr.length })
}.cachedCount(1)
}
.height('100%')
.width('100%')
}
}
场景二:
根据问题代码分析:
首页展示正常,说明节点创建操作正常。
当滚动容器组件滑动/数组改变时,Repeat将失效的子组件节点(离开有效加载范围)加入空闲节点缓存池中,即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时,对缓存池里的组件节点进行复用。下滑后发现节点与历史出现的节点重复,说明节点复用时异常,推测传参的方式或类型不符合要求。
Repeat与Builder混用章节中描述:当Repeat与@Builder混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递RepeatItem.item或RepeatItem.index,将会出现UI渲染异常。
基于以上可确认问题原因在于组件imageItemView的传参有误。
问题代码如下:
List({ space: 10 }) {
Repeat<ItemDataV2>(this.imageList)
.each((ri: RepeatItem<ItemDataV2>) => {
ListItem() {
this.imageItemView(ri.item);
};
})
.key((item: ItemDataV2) => item.id.toString())
.templateId((item: ItemDataV2) => {
return item.type;
})
.virtualScroll({ totalCount: this.imageList.length });
}
分析结论
场景一:Repeat提供了节点复用的能力,节点复用的时候没有办法重走aboutToAppear,导致复用组件的数据没有及时刷新。需要把要更新的数据放在数据源里,通过数据源变化来触发Repeat刷新。
场景二:UI渲染异常根源在于Repeat与@Builder混用时传参错误,必须将RepeatItem类型整体进行传参而不是只传递RepeatItem.item或RepeatItem.index。
修改建议
场景一:
方案一:由于复用时aboutToAppear方法不会执行,无法在aboutToAppear里更新数据源,因此可以考虑通过Repeat直接监听数据源变化触发刷新,示例代码如下:
class Title {
id: number;
title: string;
constructor(id: number, title: string) {
this.id = id;
this.title = title;
}
}
@ComponentV2
struct LowCodeTitleView {
@Param title: Title = new Title(0, '标题内容'); // 数据从父组件传递给子组件,不走aboutToAppear
build() {
Column() {
Text(this.title.id + this.title.title);
}.margin({ left: 16, top: 10, bottom: 10 });
}
}
@Entry
@ComponentV2
struct RepeatLoadDataDemo {
@Local dataArr: Array<Title> = [];
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(new Title(i, '标题内容')); // 为数组添加一些数据
}
}
build() {
RelativeContainer() {
List({ space: 3 }) {
Repeat<Title>(this.dataArr) // 把更新的数据放在数据源里
.each((ri: RepeatItem<Title>) => {
ListItem() {
LowCodeTitleView({
title: ri.item
});
};
})
.key((_item: Title, index: number): string => '__test_lowcode--' + index)
.virtualScroll({ totalCount: this.dataArr.length });
}.cachedCount(1).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.height('100%')
.width('100%');
}
}
方案二:可以使用LazyForEach结合@Reusable实现列表组件的组件复用,在节点复用的时候使用aboutToReuse生命周期触发节点触发更新数据,参考链接示例代码:列表滚动配合LazyForEach使用。
在API 18后,Repeat提供了关闭自身复用的能力,详细参考:VirtualScrollOptions;配合@ReusableV2装饰的组件使用,复用的时候也可以触发组件的aboutToReuse生命周期函数。
场景二:
上述问题代码中this.imageItemView(ri.item)传参存在问题,必须将RepeatItem类型整体进行传参,应该改为this.imageItemView(ri)。
示例代码如下:
@ObservedV2
export class ItemDataV2 {
@Trace
id: number;
@Trace
type: string = '';
@Trace
title: ResourceStr;
@Trace
img: Resource;
constructor(id: number, title: ResourceStr, img: Resource, type?: string) {
this.id = id;
this.title = title;
this.img = img;
if (type) {
this.type = type;
}
}
}
@Entry
@ComponentV2
struct RepeatDemo {
@Local imageList: Array<ItemDataV2> = this.getFirstPageData();
// 模拟的第一页的数据
getFirstPageData(): Array<ItemDataV2> {
let imageList: Array<ItemDataV2> = [];
imageList.push(...getItemData(2, 10));
return imageList;
}
build() {
Column() {
Button('addItem').onClick(() => { // 点击按钮添加一页的数据
this.addTestData();
});
List({ space: 10 }) {
Repeat<ItemDataV2>(this.imageList)
.each((ri: RepeatItem<ItemDataV2>) => {
ListItem() {
this.imageItemView(ri);
};
})
.key((item: ItemDataV2) => item.id.toString())
.templateId((item: ItemDataV2) => {
return item.type;
})
.virtualScroll({ totalCount: this.imageList.length });
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.cachedCount(6)
.width('100%')
.padding({
top: 15,
right: 15,
left: 15,
bottom: 12
});
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}
// 添加一页的数据
addTestData(): void {
let count = this.imageList.length;
let moreArr: ItemDataV2[] = getItemData(count, 10);
this.imageList.push(...moreArr);
}
@Builder
imageItemView(ri: RepeatItem<ItemDataV2>) {
Stack() {
Image(ri.item.img)
.objectFit(ImageFit.Cover)
.aspectRatio(3)
.borderRadius(12);
Text(ri.item.title)
.padding(15)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black);
}.alignContent(Alignment.TopStart)
.width('100%');
}
}
let swiperImg: Array<Resource> = [$r('app.media.backImage')]; // 背景图
// 创建模拟的数据
function getItemData(start: number, count: number): ItemDataV2[] {
let arr: ItemDataV2[] = [];
for (let i = 0; i < count; i++) {
let imageIndex = i % swiperImg.length;
arr.push(new ItemDataV2(i + start, (i + start).toString(), swiperImg[imageIndex]));
}
return arr;
}
https://pastebin.com/BMW3u7yk
https://pastebin.com/zr8N97Ne
https://pastebin.com/Gs298WgD
https://pastebin.com/FnxZDBcW
https://pastebin.com/i8rpfLEP
https://pastebin.com/ez7BZPE3
https://pastebin.com/U0KwnFSL
https://pastebin.com/R4cMh2SL
https://pastebin.com/676zm0WG
https://pastebin.com/H9pKi7w7
https://pastebin.com/dbtzJw0x
https://pastebin.com/QLWvWbqU
https://pastebin.com/yGPiJTZD
https://pastebin.com/7VminAc3
https://pastebin.com/rsScjjFE
https://pastebin.com/ez8zq4VT
https://pastebin.com/1efA7vcf
https://pastebin.com/1HpF960z
https://pastebin.com/DLn96YHA
https://pastebin.com/8dFFy65M
https://pastebin.com/TKEbGuxn
https://pastebin.com/FVgewR7x
https://pastebin.com/XTKjPrmz
https://pastebin.com/CjtL2CbZ
https://pastebin.com/8JsJ7bMj
https://pastebin.com/a9RikA98
https://pastebin.com/DBn55k14
https://pastebin.com/fNpm9vYB
https://pastebin.com/72ZfQ149
https://pastebin.com/i7N5Avqf
https://pastebin.com/GUXF7KDm
https://pastebin.com/Ku19TS4j
https://pastebin.com/sqttnjrA
https://pastebin.com/9BVvdDdC
https://pastebin.com/CHYZ1Gvb
https://pastebin.com/hrbjEy3W
https://pastebin.com/Eejnahjn
https://pastebin.com/BJjGW58r
https://pastebin.com/TA5zPz46
https://pastebin.com/aWz5V4yR
https://pastebin.com/gLqvHV7C
https://pastebin.com/zrFtuaUg
https://pastebin.com/225KFvYC
https://pastebin.com/hYe0TumF
https://pastebin.com/KE2Ux8wJ
https://pastebin.com/TMdN3eBf
https://pastebin.com/JACkCBXg
https://pastebin.com/uQEujZg0
https://pastebin.com/aqgRAuwQ
https://pastebin.com/jpxU5vmQ
https://pastebin.com/Zhmfn6xS
https://pastebin.com/gLz2BJup
https://pastebin.com/qwWaQzjV
https://pastebin.com/dsc3dh7Q
https://pastebin.com/MEeAfNrK
https://pastebin.com/s13exYvP
https://pastebin.com/BGw364Z8
https://pastebin.com/cJKMevm3
https://pastebin.com/6DDggPWC
https://pastebin.com/UpDpAga4
https://pastebin.com/Lw1s28nY
https://pastebin.com/tLM0z9LE
https://pastebin.com/bVVHFj6b
https://pastebin.com/FunGXsh7
https://pastebin.com/ChnzMQ8K
https://pastebin.com/TLQiKJqr
https://pastebin.com/3jiBF2qM
https://pastebin.com/bXUHz9bE
https://pastebin.com/L3MVPMQq
https://pastebin.com/WYWQGEQV
https://pastebin.com/qB8KP2UN
https://pastebin.com/sf84cdKm
https://pastebin.com/1hmh7csp
https://pastebin.com/ZdW6ZsCP
https://pastebin.com/83GH1cLG
https://pastebin.com/dVp5TAsp
https://pastebin.com/D9MPcZSw
https://pastebin.com/dyUNcKUb
https://pastebin.com/GVGM2jKa
https://pastebin.com/ttCQv2jU
https://pastebin.com/U2hAp61K
https://pastebin.com/uX8yxjv3
https://pastebin.com/SE3Yq25k
https://pastebin.com/NkvV232B
https://pastebin.com/SGPc5xik
https://pastebin.com/fBmMd2cN
https://pastebin.com/9wsVURDP
https://pastebin.com/y4TmwfYR
https://pastebin.com/JcTuRYrc
https://pastebin.com/V8RPz7mM
https://pastebin.com/YqASCN8G
https://pastebin.com/6JMnz3jC
https://pastebin.com/JKN9bkBR
https://pastebin.com/YkX3cz9S
https://pastebin.com/L71qyZBH
https://pastebin.com/af14FyAF
https://pastebin.com/rYKWY5hk
https://pastebin.com/bcHNKC0q
https://pastebin.com/avy1Qyra
https://pastebin.com/v08NsSpW
https://pastebin.com/bxPJwdh2
https://pastebin.com/vV3Lxu9E
https://pastebin.com/PMwyJDrr
https://pastebin.com/GNceZj3i
https://pastebin.com/QAgS2DBq
https://pastebin.com/xU0LsnLd
https://pastebin.com/QaNHe5E3
https://pastebin.com/qxtNYsxX
https://pastebin.com/ApPBs6pZ
https://pastebin.com/W2N66ehs
https://pastebin.com/7QVU487N
https://pastebin.com/NK2wvLaE
https://pastebin.com/LxeFEpZ5
https://pastebin.com/0RnYrav7
https://pastebin.com/EYw7h4gC
https://pastebin.com/7yqhn05R
https://pastebin.com/Pkhb5WGT
https://pastebin.com/dcF8cERK
https://pastebin.com/WSitbd4k
https://pastebin.com/VJkwygDS
https://pastebin.com/AWqu7HfY
https://pastebin.com/dpSAXydY
https://pastebin.com/QRC4ygGj
https://pastebin.com/2E991jhR
https://pastebin.com/349smxri
https://pastebin.com/d8LJ0Q6C
https://pastebin.com/KYmcfhUq
https://pastebin.com/NYF6CT4C
https://pastebin.com/Fa6XQ06R
https://pastebin.com/DRe4gE72
https://pastebin.com/NvzHJugB
https://pastebin.com/bpYrBJ9Q
https://pastebin.com/AnC8XMWr
https://pastebin.com/1c2sjpwh
https://pastebin.com/yhXGhB8E
https://pastebin.com/itRM2ZMd
https://pastebin.com/aR9HpTRH
https://pastebin.com/m19RYG7j
https://pastebin.com/xjpU9ksH
https://pastebin.com/xiN4JaXB
https://pastebin.com/1gU4EArR
https://pastebin.com/6qhtwYk1
https://pastebin.com/H64fri79
https://pastebin.com/xsRKbG1m
https://pastebin.com/UipZaKhK
https://pastebin.com/RAbT0kVq
https://pastebin.com/0i00ADZz
https://pastebin.com/iurUhSyk
https://pastebin.com/AJ90NhRk
https://pastebin.com/rsyZZcM5
https://pastebin.com/fST61sHE
https://pastebin.com/tnxRKNzD
https://pastebin.com/dk9KKKDm
https://pastebin.com/gqwbsWku
https://pastebin.com/uxSXWcdC
https://pastebin.com/T59UHk0f
https://pastebin.com/058HRADt
https://pastebin.com/Qa8MpNXB
https://pastebin.com/W5KVa6Bu
https://pastebin.com/gJUwCVjy
https://pastebin.com/86avgWTq
https://pastebin.com/w8JAu1sb
https://pastebin.com/LbUD3yhy
https://pastebin.com/RAWTZN9P
https://pastebin.com/ivG5UTZ1
https://pastebin.com/vU366iR6
https://pastebin.com/XCA86ssH
https://pastebin.com/YgKSxnVv
https://pastebin.com/B6suLGgy
https://pastebin.com/vxZ0nFqR
https://pastebin.com/DZ0nxixL
https://pastebin.com/PqdR7bxv
https://pastebin.com/RmZa7vqb
https://pastebin.com/EhUSwQ4z
https://pastebin.com/9pHd3GTL
https://pastebin.com/VfF8FGVe
https://pastebin.com/T66RxLCX
https://pastebin.com/7fCyXFKW
https://pastebin.com/vcrXHYSx
https://pastebin.com/bj4mJD6y
https://pastebin.com/5rgDxsgD
https://pastebin.com/T0pMab3R
https://pastebin.com/G5LQvwML
https://pastebin.com/tvyGHmzU
https://pastebin.com/ax9M09Mi
https://pastebin.com/8ZduUtUd
https://pastebin.com/9490Ajrb
https://pastebin.com/2VLuQDwG
https://pastebin.com/tt4yG2tw
https://pastebin.com/zJmwjhrr
https://pastebin.com/dbde4zgM
https://pastebin.com/QFkQaF3K
https://pastebin.com/V3S6HFfw
https://pastebin.com/zuip5PZw
https://pastebin.com/rfSqfzE4
https://pastebin.com/LRWRUJcA
https://pastebin.com/1Dz9d3J6
https://pastebin.com/QNRn2bjK
问题现象
使用Repeat实现列表功能,加载新数据会出现重复显示问题,一般有下面几种问题场景:
常见问题场景
问题描述
场景一
滑动加载新数据,在aboutToAppear方法里处理数据逻辑,导致数据重复。
场景二
Repeat与@Builder混用场景下,传参错误,导致数据重复。
场景一:滑动加载新数据,在aboutToAppear方法里处理数据逻辑,问题如图所示:
点击放大
场景二:进行Repeat与@Builder混用场景下,传参使用错误,导致数据重复,问题如图所示:
点击放大
背景知识
Repeat根据容器组件的有效加载范围(屏幕可视区域+预加载区域)加载子组件。当容器滑动/数组改变时,Repeat会根据父容器组件的布局过程重新计算有效加载范围,并管理列表子组件节点的创建与销毁。Repeat通过组件节点更新/复用从而优化性能表现。
节点更新复用能力说明:当滚动容器组件滑动/数组改变时,Repeat将失效的子组件节点(离开容器组件的显示区域和预加载区域)加入空闲节点缓存池中,即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时,对缓存池里的组件节点进行复用。
aboutToAppear:aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build()函数中生效。
问题定位
场景一:
根据问题代码分析,数据更新是在子组件的aboutToAppear方法里完成的,aboutToAppear在组件被创建时触发,之后不会再触发。因为Repeat提供了节点复用的能力,后续加载的是之前复用的节点,不会走aboutToAppear方法,导致数据没有更新。
问题代码如下:
@ComponentV2
struct LowCodeTitleView{
@Param ids: string = ''
@Local changes:string = ''
aboutToAppear(): void {
this.changes = this.ids // 数据更新是在子组件的aboutToAppear方法里完成的
}
build() {
Column(){
Text(this.changes+"标题内容")
}
}
}
@Entry
@ComponentV2
struct Index {
@Local dataArr: Array<string> = [];
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(data_${i}); // 为数组添加一些数据
}
}
build() {
RelativeContainer() {
List({ space: 3 }) {
Repeat<string>(this.dataArr)
.each((ri: RepeatItem<string>) => {
ListItem() {
LowCodeTitleView({
ids:ri.item
})
}
})
.key((item: string, index: number): string => "__test_lowcode--"+index)
.virtualScroll({ totalCount: this.dataArr.length })
}.cachedCount(1)
}
.height('100%')
.width('100%')
}
}
场景二:
根据问题代码分析:
首页展示正常,说明节点创建操作正常。
当滚动容器组件滑动/数组改变时,Repeat将失效的子组件节点(离开有效加载范围)加入空闲节点缓存池中,即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时,对缓存池里的组件节点进行复用。下滑后发现节点与历史出现的节点重复,说明节点复用时异常,推测传参的方式或类型不符合要求。
Repeat与Builder混用章节中描述:当Repeat与@Builder混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递RepeatItem.item或RepeatItem.index,将会出现UI渲染异常。
基于以上可确认问题原因在于组件imageItemView的传参有误。
问题代码如下:
List({ space: 10 }) {
Repeat<ItemDataV2>(this.imageList)
.each((ri: RepeatItem<ItemDataV2>) => {
ListItem() {
this.imageItemView(ri.item);
};
})
.key((item: ItemDataV2) => item.id.toString())
.templateId((item: ItemDataV2) => {
return item.type;
})
.virtualScroll({ totalCount: this.imageList.length });
}
分析结论
场景一:Repeat提供了节点复用的能力,节点复用的时候没有办法重走aboutToAppear,导致复用组件的数据没有及时刷新。需要把要更新的数据放在数据源里,通过数据源变化来触发Repeat刷新。
场景二:UI渲染异常根源在于Repeat与@Builder混用时传参错误,必须将RepeatItem类型整体进行传参而不是只传递RepeatItem.item或RepeatItem.index。
修改建议
场景一:
方案一:由于复用时aboutToAppear方法不会执行,无法在aboutToAppear里更新数据源,因此可以考虑通过Repeat直接监听数据源变化触发刷新,示例代码如下:
class Title {
id: number;
title: string;
constructor(id: number, title: string) {
this.id = id;
this.title = title;
}
}
@ComponentV2
struct LowCodeTitleView {
@Param title: Title = new Title(0, '标题内容'); // 数据从父组件传递给子组件,不走aboutToAppear
build() {
Column() {
Text(this.title.id + this.title.title);
}.margin({ left: 16, top: 10, bottom: 10 });
}
}
@Entry
@ComponentV2
struct RepeatLoadDataDemo {
@Local dataArr: Array<Title> = [];
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(new Title(i, '标题内容')); // 为数组添加一些数据
}
}
build() {
RelativeContainer() {
List({ space: 3 }) {
Repeat<Title>(this.dataArr) // 把更新的数据放在数据源里
.each((ri: RepeatItem<Title>) => {
ListItem() {
LowCodeTitleView({
title: ri.item
});
};
})
.key((_item: Title, index: number): string => '__test_lowcode--' + index)
.virtualScroll({ totalCount: this.dataArr.length });
}.cachedCount(1).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.height('100%')
.width('100%');
}
}
方案二:可以使用LazyForEach结合@Reusable实现列表组件的组件复用,在节点复用的时候使用aboutToReuse生命周期触发节点触发更新数据,参考链接示例代码:列表滚动配合LazyForEach使用。
在API 18后,Repeat提供了关闭自身复用的能力,详细参考:VirtualScrollOptions;配合@ReusableV2装饰的组件使用,复用的时候也可以触发组件的aboutToReuse生命周期函数。
场景二:
上述问题代码中this.imageItemView(ri.item)传参存在问题,必须将RepeatItem类型整体进行传参,应该改为this.imageItemView(ri)。
示例代码如下:
@ObservedV2
export class ItemDataV2 {
@Trace
id: number;
@Trace
type: string = '';
@Trace
title: ResourceStr;
@Trace
img: Resource;
constructor(id: number, title: ResourceStr, img: Resource, type?: string) {
this.id = id;
this.title = title;
this.img = img;
if (type) {
this.type = type;
}
}
}
@Entry
@ComponentV2
struct RepeatDemo {
@Local imageList: Array<ItemDataV2> = this.getFirstPageData();
// 模拟的第一页的数据
getFirstPageData(): Array<ItemDataV2> {
let imageList: Array<ItemDataV2> = [];
imageList.push(...getItemData(2, 10));
return imageList;
}
build() {
Column() {
Button('addItem').onClick(() => { // 点击按钮添加一页的数据
this.addTestData();
});
List({ space: 10 }) {
Repeat<ItemDataV2>(this.imageList)
.each((ri: RepeatItem<ItemDataV2>) => {
ListItem() {
this.imageItemView(ri);
};
})
.key((item: ItemDataV2) => item.id.toString())
.templateId((item: ItemDataV2) => {
return item.type;
})
.virtualScroll({ totalCount: this.imageList.length });
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.cachedCount(6)
.width('100%')
.padding({
top: 15,
right: 15,
left: 15,
bottom: 12
});
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}
// 添加一页的数据
addTestData(): void {
let count = this.imageList.length;
let moreArr: ItemDataV2[] = getItemData(count, 10);
this.imageList.push(...moreArr);
}
@Builder
imageItemView(ri: RepeatItem<ItemDataV2>) {
Stack() {
Image(ri.item.img)
.objectFit(ImageFit.Cover)
.aspectRatio(3)
.borderRadius(12);
Text(ri.item.title)
.padding(15)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black);
}.alignContent(Alignment.TopStart)
.width('100%');
}
}
let swiperImg: Array<Resource> = [$r('app.media.backImage')]; // 背景图
// 创建模拟的数据
function getItemData(start: number, count: number): ItemDataV2[] {
let arr: ItemDataV2[] = [];
for (let i = 0; i < count; i++) {
let imageIndex = i % swiperImg.length;
arr.push(new ItemDataV2(i + start, (i + start).toString(), swiperImg[imageIndex]));
}
return arr;
}
https://pastebin.com/BMW3u7yk
https://pastebin.com/zr8N97Ne
https://pastebin.com/Gs298WgD
https://pastebin.com/FnxZDBcW
https://pastebin.com/i8rpfLEP
https://pastebin.com/ez7BZPE3
https://pastebin.com/U0KwnFSL
https://pastebin.com/R4cMh2SL
https://pastebin.com/676zm0WG
https://pastebin.com/H9pKi7w7
https://pastebin.com/dbtzJw0x
https://pastebin.com/QLWvWbqU
https://pastebin.com/yGPiJTZD
https://pastebin.com/7VminAc3
https://pastebin.com/rsScjjFE
https://pastebin.com/ez8zq4VT
https://pastebin.com/1efA7vcf
https://pastebin.com/1HpF960z
https://pastebin.com/DLn96YHA
https://pastebin.com/8dFFy65M
https://pastebin.com/TKEbGuxn
https://pastebin.com/FVgewR7x
https://pastebin.com/XTKjPrmz
https://pastebin.com/CjtL2CbZ
https://pastebin.com/8JsJ7bMj
https://pastebin.com/a9RikA98
https://pastebin.com/DBn55k14
https://pastebin.com/fNpm9vYB
https://pastebin.com/72ZfQ149
https://pastebin.com/i7N5Avqf
https://pastebin.com/GUXF7KDm
https://pastebin.com/Ku19TS4j
https://pastebin.com/sqttnjrA
https://pastebin.com/9BVvdDdC
https://pastebin.com/CHYZ1Gvb
https://pastebin.com/hrbjEy3W
https://pastebin.com/Eejnahjn
https://pastebin.com/BJjGW58r
https://pastebin.com/TA5zPz46
https://pastebin.com/aWz5V4yR
https://pastebin.com/gLqvHV7C
https://pastebin.com/zrFtuaUg
https://pastebin.com/225KFvYC
https://pastebin.com/hYe0TumF
https://pastebin.com/KE2Ux8wJ
https://pastebin.com/TMdN3eBf
https://pastebin.com/JACkCBXg
https://pastebin.com/uQEujZg0
https://pastebin.com/aqgRAuwQ
https://pastebin.com/jpxU5vmQ
https://pastebin.com/Zhmfn6xS
https://pastebin.com/gLz2BJup
https://pastebin.com/qwWaQzjV
https://pastebin.com/dsc3dh7Q
https://pastebin.com/MEeAfNrK
https://pastebin.com/s13exYvP
https://pastebin.com/BGw364Z8
https://pastebin.com/cJKMevm3
https://pastebin.com/6DDggPWC
https://pastebin.com/UpDpAga4
https://pastebin.com/Lw1s28nY
https://pastebin.com/tLM0z9LE
https://pastebin.com/bVVHFj6b
https://pastebin.com/FunGXsh7
https://pastebin.com/ChnzMQ8K
https://pastebin.com/TLQiKJqr
https://pastebin.com/3jiBF2qM
https://pastebin.com/bXUHz9bE
https://pastebin.com/L3MVPMQq
https://pastebin.com/WYWQGEQV
https://pastebin.com/qB8KP2UN
https://pastebin.com/sf84cdKm
https://pastebin.com/1hmh7csp
https://pastebin.com/ZdW6ZsCP
https://pastebin.com/83GH1cLG
https://pastebin.com/dVp5TAsp
https://pastebin.com/D9MPcZSw
https://pastebin.com/dyUNcKUb
https://pastebin.com/GVGM2jKa
https://pastebin.com/ttCQv2jU
https://pastebin.com/U2hAp61K
https://pastebin.com/uX8yxjv3
https://pastebin.com/SE3Yq25k
https://pastebin.com/NkvV232B
https://pastebin.com/SGPc5xik
https://pastebin.com/fBmMd2cN
https://pastebin.com/9wsVURDP
https://pastebin.com/y4TmwfYR
https://pastebin.com/JcTuRYrc
https://pastebin.com/V8RPz7mM
https://pastebin.com/YqASCN8G
https://pastebin.com/6JMnz3jC
https://pastebin.com/JKN9bkBR
https://pastebin.com/YkX3cz9S
https://pastebin.com/L71qyZBH
https://pastebin.com/af14FyAF
https://pastebin.com/rYKWY5hk
https://pastebin.com/bcHNKC0q
https://pastebin.com/avy1Qyra
https://pastebin.com/v08NsSpW
https://pastebin.com/bxPJwdh2
https://pastebin.com/vV3Lxu9E
https://pastebin.com/PMwyJDrr
https://pastebin.com/GNceZj3i
https://pastebin.com/QAgS2DBq
https://pastebin.com/xU0LsnLd
https://pastebin.com/QaNHe5E3
https://pastebin.com/qxtNYsxX
https://pastebin.com/ApPBs6pZ
https://pastebin.com/W2N66ehs
https://pastebin.com/7QVU487N
https://pastebin.com/NK2wvLaE
https://pastebin.com/LxeFEpZ5
https://pastebin.com/0RnYrav7
https://pastebin.com/EYw7h4gC
https://pastebin.com/7yqhn05R
https://pastebin.com/Pkhb5WGT
https://pastebin.com/dcF8cERK
https://pastebin.com/WSitbd4k
https://pastebin.com/VJkwygDS
https://pastebin.com/AWqu7HfY
https://pastebin.com/dpSAXydY
https://pastebin.com/QRC4ygGj
https://pastebin.com/2E991jhR
https://pastebin.com/349smxri
https://pastebin.com/d8LJ0Q6C
https://pastebin.com/KYmcfhUq
https://pastebin.com/NYF6CT4C
https://pastebin.com/Fa6XQ06R
https://pastebin.com/DRe4gE72
https://pastebin.com/NvzHJugB
https://pastebin.com/bpYrBJ9Q
https://pastebin.com/AnC8XMWr
https://pastebin.com/1c2sjpwh
https://pastebin.com/yhXGhB8E
https://pastebin.com/itRM2ZMd
https://pastebin.com/aR9HpTRH
https://pastebin.com/m19RYG7j
https://pastebin.com/xjpU9ksH
https://pastebin.com/xiN4JaXB
https://pastebin.com/1gU4EArR
https://pastebin.com/6qhtwYk1
https://pastebin.com/H64fri79
https://pastebin.com/xsRKbG1m
https://pastebin.com/UipZaKhK
https://pastebin.com/RAbT0kVq
https://pastebin.com/0i00ADZz
https://pastebin.com/iurUhSyk
https://pastebin.com/AJ90NhRk
https://pastebin.com/rsyZZcM5
https://pastebin.com/fST61sHE
https://pastebin.com/tnxRKNzD
https://pastebin.com/dk9KKKDm
https://pastebin.com/gqwbsWku
https://pastebin.com/uxSXWcdC
https://pastebin.com/T59UHk0f
https://pastebin.com/058HRADt
https://pastebin.com/Qa8MpNXB
https://pastebin.com/W5KVa6Bu
https://pastebin.com/gJUwCVjy
https://pastebin.com/86avgWTq
https://pastebin.com/w8JAu1sb
https://pastebin.com/LbUD3yhy
https://pastebin.com/RAWTZN9P
https://pastebin.com/ivG5UTZ1
https://pastebin.com/vU366iR6
https://pastebin.com/XCA86ssH
https://pastebin.com/YgKSxnVv
https://pastebin.com/B6suLGgy
https://pastebin.com/vxZ0nFqR
https://pastebin.com/DZ0nxixL
https://pastebin.com/PqdR7bxv
https://pastebin.com/RmZa7vqb
https://pastebin.com/EhUSwQ4z
https://pastebin.com/9pHd3GTL
https://pastebin.com/VfF8FGVe
https://pastebin.com/T66RxLCX
https://pastebin.com/7fCyXFKW
https://pastebin.com/vcrXHYSx
https://pastebin.com/bj4mJD6y
https://pastebin.com/5rgDxsgD
https://pastebin.com/T0pMab3R
https://pastebin.com/G5LQvwML
https://pastebin.com/tvyGHmzU
https://pastebin.com/ax9M09Mi
https://pastebin.com/8ZduUtUd
https://pastebin.com/9490Ajrb
https://pastebin.com/2VLuQDwG
https://pastebin.com/tt4yG2tw
https://pastebin.com/zJmwjhrr
https://pastebin.com/dbde4zgM
https://pastebin.com/QFkQaF3K
https://pastebin.com/V3S6HFfw
https://pastebin.com/zuip5PZw
https://pastebin.com/rfSqfzE4
https://pastebin.com/LRWRUJcA
https://pastebin.com/1Dz9d3J6
https://pastebin.com/QNRn2bjK




