HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

打包

打包

三点打包。提示还有一分钟开始打包,然后打到五点。 打包两小时。这是不让下班了

三点打包。提示还有一分钟开始打包,然后打到五点。 打包两小时。这是不让下班了

如何实现背景以圆弧形式向外扩展的翻页动画效果

问题现象
如何实现一种翻页动画效果,其中背景以圆弧形式逐步扩大,同时文字向左下方放大并移动?

背景知识
Stack:堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。
createAnimator:定义Animator类,可以设置动画播放时长、插值曲线等参数。
解决方案
可通过帧动画实现页面转场效果,具体步骤如下:

定义两个Page组件,分别用于展示不同的页面内容。
class PageContent {
content: string;
style: TextStyle;

constructor(content: string, style: TextStyle = {}) {
this.content = content;
this.style = style;
}
}
配置页面切换时的动画。
this.animator = this.getUIContext().createAnimator({
duration: 500,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: 1,
begin: 0, //动画插值起点
end: 500 //动画插值终点
});
this.animator.onFrame = progress => {
this.pageController[0].onFrame?.(progress / 500, this.currentIndex === 1);
this.pageController[1].onFrame?.(progress / 500, this.currentIndex === 0);
};
this.animator.onFinish = () => {
let lastIndex = this.currentIndex;
this.currentIndex = 1 - this.currentIndex;
this.zIndexList[lastIndex] = 1;
this.zIndexList[this.currentIndex] = 0;
this.isAnimating = false;
};
使用Stack布局将两个页面组件堆叠显示。
Stack({ alignContent: Alignment.TopStart }) {
Page({
content: page1Content,
bgColor: '#000',
scaleValue: 1,
controller: this.pageController[0],
isShow: true
})
.zIndex(this.zIndexList[0])
.onClick(() => {
this.handleClick();
});
Page({
content: page2Content,
bgColor: '#fff',
scaleValue: 0.01,
controller: this.pageController[1]
})
.zIndex(this.zIndexList[1])
.onClick(() => {
this.handleClick();
});
}
.width('100%')
.height('100%')
.id('root')
.clip(true);
在用户点击右上角圆形按钮时,触发动画切换。
handleClick() {
if (this.isAnimating) {
return;
}
this.isAnimating = true;
this.animator?.play();
}
完整示例代码如下:

import { AnimatorResult } from '@kit.ArkUI';

class PageContent {
content: string;
style: TextStyle;

constructor(content: string, style: TextStyle = {}) {
this.content = content;
this.style = style;
}
}

const page1Content: PageContent[] = [
new PageContent('Hello everyone, I`am very happy to tell you hello world ',
{ fontSize: 30, fontWeight: 700, fontColor: Color.White }),
new PageContent('The text component is used to display a piece of textual information.Support universal attributes and universal text attributes.',
{ fontColor: Color.White }),
new PageContent('This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content. This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content.',
{ fontColor: Color.White }),
];

const page2Content: PageContent[] = [
new PageContent('Hello everyone, Please say hello! ', { fontSize: 30 }),
new PageContent('Nice to meet you.', { fontSize: 30 }),
new PageContent('I`m fine thank you and you.', { fontSize: 30 }),
new PageContent('No problem.', { fontSize: 30 }),
];

interface PageInfo {
width: number,
height: number,
centerX: number,
centerY: number
}

class PageController {
push?: (onFinish: () => void) => void;
pop?: (onFinish: () => void) => void;
onFrame?: (progress: number, isPush: boolean) => void;
}

@Entry
@Component
export struct TransitionPage {
@Provide info: PageInfo = {
width: 0,
height: 0,
centerX: 0,
centerY: 0
};
pageController: PageController[] = [];
@State zIndexList: number[] = [];
private isAnimating: boolean = false;
private animator?: AnimatorResult;
private currentIndex: number = 0;

aboutToAppear(): void {
let observer = this.getUIContext().getUIInspector().createComponentObserver('root');
observer.on('layout', () => {
let node = this.getUIContext().getAttachedFrameNodeById('root');
if (node) {
this.info = {
width: this.getUIContext().px2vp(node.getMeasuredSize().width),
height: this.getUIContext().px2vp(node.getMeasuredSize().height),
centerY: 50,
centerX: this.getUIContext().px2vp(node.getMeasuredSize().width) - 40
};
}
});
for (let i = 0; i < 2; i++) {
this.pageController.push(new PageController());
this.zIndexList.push(i);
}
// 页面切换时的动画
this.animator = this.getUIContext().createAnimator({
duration: 500,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: 1,
begin: 0, //动画插值起点
end: 500 //动画插值终点
});
this.animator.onFrame = progress => {
this.pageController[0].onFrame?.(progress / 500, this.currentIndex === 1);
this.pageController[1].onFrame?.(progress / 500, this.currentIndex === 0);
};
this.animator.onFinish = () => {
let lastIndex = this.currentIndex;
this.currentIndex = 1 - this.currentIndex;
this.zIndexList[lastIndex] = 1;
this.zIndexList[this.currentIndex] = 0;
this.isAnimating = false;
};
}

aboutToDisappear(): void {
this.animator?.finish();
this.animator = undefined;
}

push(index: number) {
let promise: Promise<void>[] = [];
promise.push(new Promise(resolve => {
this.pageController[index].push?.(() => {
this.zIndexList[index] = 0;
resolve();
});
}));
let lastIndex = 1 - index;
promise.push(new Promise(resolve => {
this.pageController[lastIndex].pop?.(() => {
this.zIndexList[lastIndex] = 1;
resolve();
});
}));
Promise.all(promise).then(() => this.isAnimating = false);
}
// 动画切换事件
handleClick() {
if (this.isAnimating) {
return;
}
this.isAnimating = true;
this.animator?.play();
}

build() {
// 两个页面组件堆叠显示
Stack({ alignContent: Alignment.TopStart }) {
Page({
content: page1Content,
bgColor: '#000',
scaleValue: 1,
controller: this.pageController[0],
isShow: true
})
.zIndex(this.zIndexList[0])
.onClick(() => {
this.handleClick();
});
Page({
content: page2Content,
bgColor: '#fff',
scaleValue: 0.01,
controller: this.pageController[1]
})
.zIndex(this.zIndexList[1])
.onClick(() => {
this.handleClick();
});
}
.width('100%')
.height('100%')
.id('root')
.clip(true);
}

onDidBuild(): void {
if (!this.animator) {
return;
}
}
}

@Component
struct Page {
@Require content: PageContent[] = [];
@Consume @Watch('onInit') info: PageInfo;
@State scaleValue: number = 0.01;
@State r: number = 0;
@State isShow: boolean = false;
bgColor: string = '#fff';
controller?: PageController;

onInit() {
this.r = Math.sqrt(this.info.centerX this.info.centerX +
(this.info.height - this.info.centerY)
(this.info.height - this.info.centerY));
}

aboutToAppear(): void {
if (this.controller) {
this.controller.push = (onFinish) => {
this.onPush(onFinish);
};
this.controller.pop = (onFinish) => {
this.onPop(onFinish);
};
this.controller.onFrame = (progress, isPush) => {
if (isPush) {
this.isShow = true;
this.scaleValue = 0.01 + (1 - 0.01) progress;
} else {
this.scaleValue = 1 + (4 - 1)
progress;
if (progress >= 1) {
this.isShow = false;
this.scaleValue = 0.01;
}
}
};
}
}

onPush(onFinish: () => void) {
this.isShow = true;
this.getUIContext().animateTo({ duration: 500, onFinish: onFinish }, () => {
this.scaleValue = 1;
});
}

onPop(onFinish: () => void) {
this.getUIContext().animateTo({
duration: 500,
onFinish: () => {
this.scaleValue = 0.01;
this.isShow = false;
onFinish();
}
}, () => {
this.scaleValue = 4;
});
}

build() {
Column() {
if (this.isShow) {
Column({ space: 20 }) {
ForEach(this.content, (item: PageContent) => {
Text(item.content)
.fontSize(item.style.fontSize)
.fontColor(item.style.fontColor)
.fontWeight(item.style.fontWeight)
.fontStyle(item.style.fontStyle)
.fontFamily(item.style.fontFamily);
});
}
.position({ x: this.r - this.info.centerX, y: this.r - this.info.centerY })
.padding({ left: 16, right:16 })
.width(this.info.width)
.height(this.info.height)
.justifyContent(FlexAlign.SpaceEvenly);
}
}
.width(this.r 2)
.height(this.r
2)
.constraintSize({ minWidth: this.r * 2 })
.scale({ x: this.scaleValue, y: this.scaleValue })
.offset({ x: this.info.centerX - this.r, y: this.info.centerY - this.r })
.borderRadius(this.r)
.clip(true)
.backgroundColor(this.bgColor);
}
}
https://coub.com/view/4b7yjg
https://coub.com/view/4b7yjf
https://coub.com/view/4b7yjd
https://coub.com/view/4b7yiw
https://coub.com/view/4b7yis
https://coub.com/view/4b7yip
https://coub.com/view/4b7yik
https://coub.com/view/4b7yij
https://coub.com/view/4b7yig
https://coub.com/view/4b7yie
https://coub.com/view/4b7yic
https://coub.com/view/4b7yia
https://coub.com/view/4b7yi7
https://coub.com/view/4b7yi5
https://coub.com/view/4b7yi3
https://coub.com/view/4b7yhy
https://coub.com/view/4b7yht
https://coub.com/view/4b7yhr
https://coub.com/view/4b7yhp
https://coub.com/view/4b7yhh
https://coub.com/view/4b7yhg
https://coub.com/view/4b7yhf
https://coub.com/view/4b7yhb
https://coub.com/view/4b7yh9
https://coub.com/view/4b7yh6
https://coub.com/view/4b7yh4
https://coub.com/view/4b7ygz
https://coub.com/view/4b7ygx
https://coub.com/view/4b7ygu
https://coub.com/view/4b7ygr
https://coub.com/view/4b7ygn
https://coub.com/view/4b7ygk
https://coub.com/view/4b7ygh
https://coub.com/view/4b7ygf
https://coub.com/view/4b7ygd
https://coub.com/view/4b7ygb
https://coub.com/view/4b7yg8
https://coub.com/view/4b7yg5
https://coub.com/view/4b7yg2
https://coub.com/view/4b7yfr
https://coub.com/view/4b7yfo
https://coub.com/view/4b7yfk
https://coub.com/view/4b7yfh
https://coub.com/view/4b7yfe
https://coub.com/view/4b7yfc
https://coub.com/view/4b7yfa
https://coub.com/view/4b7yf7
https://coub.com/view/4b7yf5
https://coub.com/view/4b7yf1
https://coub.com/view/4b7yez
https://coub.com/view/4b7yet
https://coub.com/view/4b7yer
https://coub.com/view/4b7yep
https://coub.com/view/4b7yeo
https://coub.com/view/4b7ub4
https://coub.com/view/4b7ub3
https://coub.com/view/4b7ub2
https://coub.com/view/4b7ub0
https://coub.com/view/4b7uay
https://coub.com/view/4b7uax
https://coub.com/view/4b7uav
https://coub.com/view/4b7uap
https://coub.com/view/4b7uao
https://coub.com/view/4b7ual
https://coub.com/view/4b7uad
https://coub.com/view/4b7uaa
https://coub.com/view/4b7ua7
https://coub.com/view/4b7ua5
https://coub.com/view/4b7ua2
https://coub.com/view/4b7ua1
https://coub.com/view/4b7u9z
https://coub.com/view/4b7u9y
https://coub.com/view/4b7u9x
https://coub.com/view/4b7u9p
https://coub.com/view/4b7u9k
https://coub.com/view/4b7u9j
https://coub.com/view/4b7u9i
https://coub.com/view/4b7u9g
https://coub.com/view/4b7u9d
https://coub.com/view/4b7u9a
https://coub.com/view/4b7u98
https://coub.com/view/4b7u96
https://coub.com/view/4b7u94
https://coub.com/view/4b7u92
https://coub.com/view/4b7u91
https://coub.com/view/4b7u8z
https://coub.com/view/4b7u8t
https://coub.com/view/4b7u8q
https://coub.com/view/4b7u8p
https://coub.com/view/4b7u8n
https://coub.com/view/4b7u8j
https://coub.com/view/4b7u8i
https://coub.com/view/4b7u78
https://coub.com/view/4b7u76
https://coub.com/view/4b7u72
https://coub.com/view/4b7u6v
https://coub.com/view/4b7u6t
https://coub.com/view/4b7u6r
https://coub.com/view/4b7u6o
https://coub.com/view/4b7u6m
https://coub.com/view/4b7u6l
https://coub.com/view/4b7u6j
https://coub.com/view/4b7u6h
https://coub.com/view/4b7u6f
https://coub.com/view/4b7u6d
https://coub.com/view/4b7u6a
https://coub.com/view/4b7u67
https://coub.com/view/4b7u65
https://coub.com/view/4b7u60
https://coub.com/view/4b7u5t
https://coub.com/view/4b7u5p
https://coub.com/view/4b7u5i
https://coub.com/view/4b7u4t
https://coub.com/view/4b7u4p
https://coub.com/view/4b7u4l
https://coub.com/view/4b7u46
https://coub.com/view/4b7u3v
https://coub.com/view/4b7u3u
https://coub.com/view/4b7u3t
https://coub.com/view/4b7u3r
https://coub.com/view/4b7u3o
https://coub.com/view/4b7u3l
https://coub.com/view/4b7u3k
https://coub.com/view/4b7u3h
https://coub.com/view/4b7u3a
https://coub.com/view/4b7u38
https://coub.com/view/4b7u35
https://coub.com/view/4b7u34
https://coub.com/view/4b7u33
https://coub.com/view/4b7u30
https://coub.com/view/4b7u2z
https://coub.com/view/4b7u2w

继续阅读 »

问题现象
如何实现一种翻页动画效果,其中背景以圆弧形式逐步扩大,同时文字向左下方放大并移动?

背景知识
Stack:堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。
createAnimator:定义Animator类,可以设置动画播放时长、插值曲线等参数。
解决方案
可通过帧动画实现页面转场效果,具体步骤如下:

定义两个Page组件,分别用于展示不同的页面内容。
class PageContent {
content: string;
style: TextStyle;

constructor(content: string, style: TextStyle = {}) {
this.content = content;
this.style = style;
}
}
配置页面切换时的动画。
this.animator = this.getUIContext().createAnimator({
duration: 500,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: 1,
begin: 0, //动画插值起点
end: 500 //动画插值终点
});
this.animator.onFrame = progress => {
this.pageController[0].onFrame?.(progress / 500, this.currentIndex === 1);
this.pageController[1].onFrame?.(progress / 500, this.currentIndex === 0);
};
this.animator.onFinish = () => {
let lastIndex = this.currentIndex;
this.currentIndex = 1 - this.currentIndex;
this.zIndexList[lastIndex] = 1;
this.zIndexList[this.currentIndex] = 0;
this.isAnimating = false;
};
使用Stack布局将两个页面组件堆叠显示。
Stack({ alignContent: Alignment.TopStart }) {
Page({
content: page1Content,
bgColor: '#000',
scaleValue: 1,
controller: this.pageController[0],
isShow: true
})
.zIndex(this.zIndexList[0])
.onClick(() => {
this.handleClick();
});
Page({
content: page2Content,
bgColor: '#fff',
scaleValue: 0.01,
controller: this.pageController[1]
})
.zIndex(this.zIndexList[1])
.onClick(() => {
this.handleClick();
});
}
.width('100%')
.height('100%')
.id('root')
.clip(true);
在用户点击右上角圆形按钮时,触发动画切换。
handleClick() {
if (this.isAnimating) {
return;
}
this.isAnimating = true;
this.animator?.play();
}
完整示例代码如下:

import { AnimatorResult } from '@kit.ArkUI';

class PageContent {
content: string;
style: TextStyle;

constructor(content: string, style: TextStyle = {}) {
this.content = content;
this.style = style;
}
}

const page1Content: PageContent[] = [
new PageContent('Hello everyone, I`am very happy to tell you hello world ',
{ fontSize: 30, fontWeight: 700, fontColor: Color.White }),
new PageContent('The text component is used to display a piece of textual information.Support universal attributes and universal text attributes.',
{ fontColor: Color.White }),
new PageContent('This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content. This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content.',
{ fontColor: Color.White }),
];

const page2Content: PageContent[] = [
new PageContent('Hello everyone, Please say hello! ', { fontSize: 30 }),
new PageContent('Nice to meet you.', { fontSize: 30 }),
new PageContent('I`m fine thank you and you.', { fontSize: 30 }),
new PageContent('No problem.', { fontSize: 30 }),
];

interface PageInfo {
width: number,
height: number,
centerX: number,
centerY: number
}

class PageController {
push?: (onFinish: () => void) => void;
pop?: (onFinish: () => void) => void;
onFrame?: (progress: number, isPush: boolean) => void;
}

@Entry
@Component
export struct TransitionPage {
@Provide info: PageInfo = {
width: 0,
height: 0,
centerX: 0,
centerY: 0
};
pageController: PageController[] = [];
@State zIndexList: number[] = [];
private isAnimating: boolean = false;
private animator?: AnimatorResult;
private currentIndex: number = 0;

aboutToAppear(): void {
let observer = this.getUIContext().getUIInspector().createComponentObserver('root');
observer.on('layout', () => {
let node = this.getUIContext().getAttachedFrameNodeById('root');
if (node) {
this.info = {
width: this.getUIContext().px2vp(node.getMeasuredSize().width),
height: this.getUIContext().px2vp(node.getMeasuredSize().height),
centerY: 50,
centerX: this.getUIContext().px2vp(node.getMeasuredSize().width) - 40
};
}
});
for (let i = 0; i < 2; i++) {
this.pageController.push(new PageController());
this.zIndexList.push(i);
}
// 页面切换时的动画
this.animator = this.getUIContext().createAnimator({
duration: 500,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: 1,
begin: 0, //动画插值起点
end: 500 //动画插值终点
});
this.animator.onFrame = progress => {
this.pageController[0].onFrame?.(progress / 500, this.currentIndex === 1);
this.pageController[1].onFrame?.(progress / 500, this.currentIndex === 0);
};
this.animator.onFinish = () => {
let lastIndex = this.currentIndex;
this.currentIndex = 1 - this.currentIndex;
this.zIndexList[lastIndex] = 1;
this.zIndexList[this.currentIndex] = 0;
this.isAnimating = false;
};
}

aboutToDisappear(): void {
this.animator?.finish();
this.animator = undefined;
}

push(index: number) {
let promise: Promise<void>[] = [];
promise.push(new Promise(resolve => {
this.pageController[index].push?.(() => {
this.zIndexList[index] = 0;
resolve();
});
}));
let lastIndex = 1 - index;
promise.push(new Promise(resolve => {
this.pageController[lastIndex].pop?.(() => {
this.zIndexList[lastIndex] = 1;
resolve();
});
}));
Promise.all(promise).then(() => this.isAnimating = false);
}
// 动画切换事件
handleClick() {
if (this.isAnimating) {
return;
}
this.isAnimating = true;
this.animator?.play();
}

build() {
// 两个页面组件堆叠显示
Stack({ alignContent: Alignment.TopStart }) {
Page({
content: page1Content,
bgColor: '#000',
scaleValue: 1,
controller: this.pageController[0],
isShow: true
})
.zIndex(this.zIndexList[0])
.onClick(() => {
this.handleClick();
});
Page({
content: page2Content,
bgColor: '#fff',
scaleValue: 0.01,
controller: this.pageController[1]
})
.zIndex(this.zIndexList[1])
.onClick(() => {
this.handleClick();
});
}
.width('100%')
.height('100%')
.id('root')
.clip(true);
}

onDidBuild(): void {
if (!this.animator) {
return;
}
}
}

@Component
struct Page {
@Require content: PageContent[] = [];
@Consume @Watch('onInit') info: PageInfo;
@State scaleValue: number = 0.01;
@State r: number = 0;
@State isShow: boolean = false;
bgColor: string = '#fff';
controller?: PageController;

onInit() {
this.r = Math.sqrt(this.info.centerX this.info.centerX +
(this.info.height - this.info.centerY)
(this.info.height - this.info.centerY));
}

aboutToAppear(): void {
if (this.controller) {
this.controller.push = (onFinish) => {
this.onPush(onFinish);
};
this.controller.pop = (onFinish) => {
this.onPop(onFinish);
};
this.controller.onFrame = (progress, isPush) => {
if (isPush) {
this.isShow = true;
this.scaleValue = 0.01 + (1 - 0.01) progress;
} else {
this.scaleValue = 1 + (4 - 1)
progress;
if (progress >= 1) {
this.isShow = false;
this.scaleValue = 0.01;
}
}
};
}
}

onPush(onFinish: () => void) {
this.isShow = true;
this.getUIContext().animateTo({ duration: 500, onFinish: onFinish }, () => {
this.scaleValue = 1;
});
}

onPop(onFinish: () => void) {
this.getUIContext().animateTo({
duration: 500,
onFinish: () => {
this.scaleValue = 0.01;
this.isShow = false;
onFinish();
}
}, () => {
this.scaleValue = 4;
});
}

build() {
Column() {
if (this.isShow) {
Column({ space: 20 }) {
ForEach(this.content, (item: PageContent) => {
Text(item.content)
.fontSize(item.style.fontSize)
.fontColor(item.style.fontColor)
.fontWeight(item.style.fontWeight)
.fontStyle(item.style.fontStyle)
.fontFamily(item.style.fontFamily);
});
}
.position({ x: this.r - this.info.centerX, y: this.r - this.info.centerY })
.padding({ left: 16, right:16 })
.width(this.info.width)
.height(this.info.height)
.justifyContent(FlexAlign.SpaceEvenly);
}
}
.width(this.r 2)
.height(this.r
2)
.constraintSize({ minWidth: this.r * 2 })
.scale({ x: this.scaleValue, y: this.scaleValue })
.offset({ x: this.info.centerX - this.r, y: this.info.centerY - this.r })
.borderRadius(this.r)
.clip(true)
.backgroundColor(this.bgColor);
}
}
https://coub.com/view/4b7yjg
https://coub.com/view/4b7yjf
https://coub.com/view/4b7yjd
https://coub.com/view/4b7yiw
https://coub.com/view/4b7yis
https://coub.com/view/4b7yip
https://coub.com/view/4b7yik
https://coub.com/view/4b7yij
https://coub.com/view/4b7yig
https://coub.com/view/4b7yie
https://coub.com/view/4b7yic
https://coub.com/view/4b7yia
https://coub.com/view/4b7yi7
https://coub.com/view/4b7yi5
https://coub.com/view/4b7yi3
https://coub.com/view/4b7yhy
https://coub.com/view/4b7yht
https://coub.com/view/4b7yhr
https://coub.com/view/4b7yhp
https://coub.com/view/4b7yhh
https://coub.com/view/4b7yhg
https://coub.com/view/4b7yhf
https://coub.com/view/4b7yhb
https://coub.com/view/4b7yh9
https://coub.com/view/4b7yh6
https://coub.com/view/4b7yh4
https://coub.com/view/4b7ygz
https://coub.com/view/4b7ygx
https://coub.com/view/4b7ygu
https://coub.com/view/4b7ygr
https://coub.com/view/4b7ygn
https://coub.com/view/4b7ygk
https://coub.com/view/4b7ygh
https://coub.com/view/4b7ygf
https://coub.com/view/4b7ygd
https://coub.com/view/4b7ygb
https://coub.com/view/4b7yg8
https://coub.com/view/4b7yg5
https://coub.com/view/4b7yg2
https://coub.com/view/4b7yfr
https://coub.com/view/4b7yfo
https://coub.com/view/4b7yfk
https://coub.com/view/4b7yfh
https://coub.com/view/4b7yfe
https://coub.com/view/4b7yfc
https://coub.com/view/4b7yfa
https://coub.com/view/4b7yf7
https://coub.com/view/4b7yf5
https://coub.com/view/4b7yf1
https://coub.com/view/4b7yez
https://coub.com/view/4b7yet
https://coub.com/view/4b7yer
https://coub.com/view/4b7yep
https://coub.com/view/4b7yeo
https://coub.com/view/4b7ub4
https://coub.com/view/4b7ub3
https://coub.com/view/4b7ub2
https://coub.com/view/4b7ub0
https://coub.com/view/4b7uay
https://coub.com/view/4b7uax
https://coub.com/view/4b7uav
https://coub.com/view/4b7uap
https://coub.com/view/4b7uao
https://coub.com/view/4b7ual
https://coub.com/view/4b7uad
https://coub.com/view/4b7uaa
https://coub.com/view/4b7ua7
https://coub.com/view/4b7ua5
https://coub.com/view/4b7ua2
https://coub.com/view/4b7ua1
https://coub.com/view/4b7u9z
https://coub.com/view/4b7u9y
https://coub.com/view/4b7u9x
https://coub.com/view/4b7u9p
https://coub.com/view/4b7u9k
https://coub.com/view/4b7u9j
https://coub.com/view/4b7u9i
https://coub.com/view/4b7u9g
https://coub.com/view/4b7u9d
https://coub.com/view/4b7u9a
https://coub.com/view/4b7u98
https://coub.com/view/4b7u96
https://coub.com/view/4b7u94
https://coub.com/view/4b7u92
https://coub.com/view/4b7u91
https://coub.com/view/4b7u8z
https://coub.com/view/4b7u8t
https://coub.com/view/4b7u8q
https://coub.com/view/4b7u8p
https://coub.com/view/4b7u8n
https://coub.com/view/4b7u8j
https://coub.com/view/4b7u8i
https://coub.com/view/4b7u78
https://coub.com/view/4b7u76
https://coub.com/view/4b7u72
https://coub.com/view/4b7u6v
https://coub.com/view/4b7u6t
https://coub.com/view/4b7u6r
https://coub.com/view/4b7u6o
https://coub.com/view/4b7u6m
https://coub.com/view/4b7u6l
https://coub.com/view/4b7u6j
https://coub.com/view/4b7u6h
https://coub.com/view/4b7u6f
https://coub.com/view/4b7u6d
https://coub.com/view/4b7u6a
https://coub.com/view/4b7u67
https://coub.com/view/4b7u65
https://coub.com/view/4b7u60
https://coub.com/view/4b7u5t
https://coub.com/view/4b7u5p
https://coub.com/view/4b7u5i
https://coub.com/view/4b7u4t
https://coub.com/view/4b7u4p
https://coub.com/view/4b7u4l
https://coub.com/view/4b7u46
https://coub.com/view/4b7u3v
https://coub.com/view/4b7u3u
https://coub.com/view/4b7u3t
https://coub.com/view/4b7u3r
https://coub.com/view/4b7u3o
https://coub.com/view/4b7u3l
https://coub.com/view/4b7u3k
https://coub.com/view/4b7u3h
https://coub.com/view/4b7u3a
https://coub.com/view/4b7u38
https://coub.com/view/4b7u35
https://coub.com/view/4b7u34
https://coub.com/view/4b7u33
https://coub.com/view/4b7u30
https://coub.com/view/4b7u2z
https://coub.com/view/4b7u2w

收起阅读 »

用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

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

【离线打包】最近开发了个安卓和苹果的离线打包工具:uni-pack,开源了!!!

Android离线打包 uniapp离线打包 App离线打包 iOS离线打包

uni-pack
支持 mac 和 windows 帮忙点一下star 谢谢
gitee:git clone https://gitee.com/mayunrhgsdfh/uni-pack.git

uni-pack
支持 mac 和 windows 帮忙点一下star 谢谢
gitee:git clone https://gitee.com/mayunrhgsdfh/uni-pack.git

java.lang.SecurityException: Shell does not have permission to access user 100 运行真机报错终于解决了

真机运行

报错原文

2026-08-06 15:22:08.448 [WARNING:] [PluginHost] Error: Command failed: E:\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe -s adb-AN2FVB1731016322-t2CsiT._adb-tls-connect._tcp shell pm list packages io.dcloud.HBuilder  
Exception occurred while executing 'list':  
java.lang.SecurityException: Shell does not have permission to access user 100  
com.android.server.am.ActivityManagerService.handleIncomingUser:14620 android.app.ActivityManager.handleIncomingUser:4753 com.android.server.pm.PackageManagerShellCommand.translateUserId:3853  
at com.android.server.am.UserController.handleIncomingUser(UserController.java:2745)  
at com.android.server.am.ActivityManagerService.handleIncomingUser(ActivityManagerService.java:14620)  
at android.app.ActivityManager.handleIncomingUser(ActivityManager.java:4753)  
at com.android.server.pm.PackageManagerShellCommand.translateUserId(PackageManagerShellCommand.java:3853)  
at com.android.server.pm.PackageManagerShellCommand.runListPackages(PackageManagerShellCommand.java:1060)  
at com.android.server.pm.PackageManagerShellCommand.runListPackages(PackageManagerShellCommand.java:956)  
at com.android.server.pm.PackageManagerShellCommand.runList(PackageManagerShellCommand.java:792)  
at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:230)  
at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)  
at android.os.ShellCommand.exec(ShellCommand.java:38)  
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onShellCommand(PackageManagerService.java:7086)  
at android.os.Binder.shellCommand(Binder.java:1087)  
at android.os.Binder.onTransact(Binder.java:904)  
at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4368)  
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onTransact(PackageManagerService.java:7062)  
at android.os.Binder.execTransactInternal(Binder.java:1371)  
at android.os.Binder.execTransact(Binder.java:1310)  
at genericNodeError (node:internal/errors:983:15)  
at wrappedFn (node:internal/errors:537:14)  
at ChildProcess.exithandler (node:child_process:417:12)  
at ChildProcess.emit (node:events:519:28)  
at maybeClose (node:internal/child_process:1101:16)  
at ChildProcess._handle.onexit (node:internal/child_process:304:5) {  
code: 255,  
killed: false,  
signal: null,  
cmd: 'E:\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe -s adb-AN2FVB1731016322-t2CsiT._adb-tls-connect._tcp shell pm list packages io.dcloud.HBuilder'  
}

解决方法

在 adb 安卓目录 E:\HBuilderX\plugins\launcher-tools\tools\adbs 输入 cmd 打开命令行 输入 adb devices

确保手机已经成功链接
然后输入 adb shell pm list users 查看系统底层真实用户

如果有 这个 UserInfo{100:平行空间:20000410} 说明你手机开启了平行空间 可以在手机设置里搜索 平行空间 直接销毁就行了

继续阅读 »

报错原文

2026-08-06 15:22:08.448 [WARNING:] [PluginHost] Error: Command failed: E:\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe -s adb-AN2FVB1731016322-t2CsiT._adb-tls-connect._tcp shell pm list packages io.dcloud.HBuilder  
Exception occurred while executing 'list':  
java.lang.SecurityException: Shell does not have permission to access user 100  
com.android.server.am.ActivityManagerService.handleIncomingUser:14620 android.app.ActivityManager.handleIncomingUser:4753 com.android.server.pm.PackageManagerShellCommand.translateUserId:3853  
at com.android.server.am.UserController.handleIncomingUser(UserController.java:2745)  
at com.android.server.am.ActivityManagerService.handleIncomingUser(ActivityManagerService.java:14620)  
at android.app.ActivityManager.handleIncomingUser(ActivityManager.java:4753)  
at com.android.server.pm.PackageManagerShellCommand.translateUserId(PackageManagerShellCommand.java:3853)  
at com.android.server.pm.PackageManagerShellCommand.runListPackages(PackageManagerShellCommand.java:1060)  
at com.android.server.pm.PackageManagerShellCommand.runListPackages(PackageManagerShellCommand.java:956)  
at com.android.server.pm.PackageManagerShellCommand.runList(PackageManagerShellCommand.java:792)  
at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:230)  
at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)  
at android.os.ShellCommand.exec(ShellCommand.java:38)  
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onShellCommand(PackageManagerService.java:7086)  
at android.os.Binder.shellCommand(Binder.java:1087)  
at android.os.Binder.onTransact(Binder.java:904)  
at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4368)  
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onTransact(PackageManagerService.java:7062)  
at android.os.Binder.execTransactInternal(Binder.java:1371)  
at android.os.Binder.execTransact(Binder.java:1310)  
at genericNodeError (node:internal/errors:983:15)  
at wrappedFn (node:internal/errors:537:14)  
at ChildProcess.exithandler (node:child_process:417:12)  
at ChildProcess.emit (node:events:519:28)  
at maybeClose (node:internal/child_process:1101:16)  
at ChildProcess._handle.onexit (node:internal/child_process:304:5) {  
code: 255,  
killed: false,  
signal: null,  
cmd: 'E:\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe -s adb-AN2FVB1731016322-t2CsiT._adb-tls-connect._tcp shell pm list packages io.dcloud.HBuilder'  
}

解决方法

在 adb 安卓目录 E:\HBuilderX\plugins\launcher-tools\tools\adbs 输入 cmd 打开命令行 输入 adb devices

确保手机已经成功链接
然后输入 adb shell pm list users 查看系统底层真实用户

如果有 这个 UserInfo{100:平行空间:20000410} 说明你手机开启了平行空间 可以在手机设置里搜索 平行空间 直接销毁就行了

收起阅读 »

如何实现全局可调用弹窗

问题现象
应用/元服务如何实现一个全局可调用的弹窗?

背景知识
windowStage为窗口管理器,管理各个基本窗口单元。
通过CustomDialogController类显示自定义弹窗,需要使用@CustomDialog定义弹窗内容。调用弹窗时,需要new CustomDialogController(),并将@CustomDialog作为CustomDialogControllerOptions中的builder参数传入。
openCustomDialog用于创建并弹出dialogContent对应的自定义弹窗。
解决方案
实现一个全局可调用弹窗,可通过如下三种方式实现:

方案一:实现全局可调用弹窗推荐使用:不依赖UI组件的全局自定义弹出框 (openCustomDialog),示例请参考完整示例。
方案二:可使用子窗口方式实现全局可调用弹窗,实现方式如下:
在EntryAbility.ets中通过AppStorage获取窗口管理器用于后续获取Window实例实现子窗口的创建与销毁,代码如下:
import { ConfigurationConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
onCreate(): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}

onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}

onWindowStageCreate(windowStage: window.WindowStage): void {
AppStorage.setOrCreate('windowStage', windowStage);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}

onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}

onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}

onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
};
SubWindow.ets页面封装打开弹窗createMySubWindowFun方法,通过createSubWindow实现弹窗的弹出,使用setUIContent设置SubPage页面为子窗口加载的具体页面内容,通过moveWindowTo、resize等方法设置子窗口样式。封装deleteMySubWindowFun方法通过destroyWindow实现弹窗的关闭。代码如下:
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 获取windowStage
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
let windowClass: window.Window | undefined = undefined;

export class mySubWindow {
createMySubWindowFun(subWindowName: string) {
// 创建子窗口
windowStage.createSubWindow(subWindowName, (err: BusinessError, data) => {
windowClass = data;
data.setUIContent('pages/SubPage', () => {
// 设置子窗口左上角坐标
data.moveWindowTo(150, 300);
// 设置子窗口大小
data.resize(1000, 1000);
// 展示子窗口
data.showWindow();
});
});
}

deleteMySubWindowFun() {
if (windowClass) {
windowClass.destroyWindow();
}
}
}
SubPage.ets页面用于展示子窗页面内容,代码如下:
// 子窗口显示页面
import { mySubWindow } from '../utils/SubWindow';

@Entry
@Component
struct SubPage {
mySubWindow: mySubWindow = new mySubWindow();

build() {
Column() {
Text('子窗口页面')
.fontSize(40)
.margin({ top: 30 });
Button('点击关闭')
.onClick(() => {
this.mySubWindow.deleteMySubWindowFun();
})
.margin({ top: 100 });
}
.width('100%')
.height('100%');
}
}
Index.ets页面使用方式通过引入SubWindow页面,通过调用封装的createMySubWindowFun方法实现弹窗的打开,代码如下:
import { mySubWindow } from '../utils/SubWindow';

@Entry
@Component
struct Index {
mySubWindow: mySubWindow = new mySubWindow();

build() {
RelativeContainer() {
Text('点击打开弹窗')
.id('HelloWorld')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})
.onClick(() => {
this.mySubWindow.createMySubWindowFun('mySubWindow');
});
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5');
}
}
运行效果图如下:

点击放大

方案三:通过自定义弹窗(CustomDialog)封装成自定义组件的方式:
封装弹窗控制方法Dialog.ets,使用@watch监听外部传入的变量变化,通过CustomDialogController控制弹窗的显示隐藏,代码如下:
import { CustomDialogView } from './CustomDialogView';

@Component
export struct Dialog {
// 监听外部传入的visible变量,visible值发生变化时触发onChange回调函数
@Watch('onChange') @Link visible: boolean;
onCancel?: () => void;
onConfirm?: () => void;
// 通过CustomDialogController的builder参数绑定弹窗组件CustomDialogView
private controller = new CustomDialogController({
builder: CustomDialogView({
visible: $visible,
onCancel: this.onCancel,
onConfirm: this.onConfirm,
}),
autoCancel: false,
alignment: DialogAlignment.Center,
});

// 当visible的值变化时触发回调
onChange(): void {
if (this.visible) {
this.controller.open();
} else {
this.controller.close();
}
}

// 二次封装的Dialog组件主要通过控制器控制弹窗,不需要任何界面
build() {
}
}
封装弹窗视图页面CustomDialogView.ets,作用于弹窗中作为展示页面,代码如下:
@CustomDialog
export struct CustomDialogView {
@Link visible: boolean;
controller: CustomDialogController;
// 弹窗交互事件参数,点击确认和取消按钮时的回调函数
onCancel?: () => void;
onConfirm?: () => void;

build() {
Column() {
Text('自定义弹窗')
.margin({ top: 20, bottom: 40 });
Button('点击关闭弹窗').onClick(() => {
// 重置弹窗显隐参数
this.visible = false;
// 关闭弹窗
this.controller.close();
});
}
.width('100%')
.height(200)
.backgroundColor(Color.White);
}
}
使用弹窗页面CustomDialog.ets,通过给引入的Dialog组件传入visible控制弹窗的弹出,代码如下:
import { Dialog } from './Dialog';

@Entry
@Component
export struct CustomDialog {
// 外部定义visible变量作为弹窗组件入参,控制弹窗显隐
@State visible: boolean = false;

build() {
Column({ space: 20 }) {
Button('点击打开弹窗')
// 点击修改visible变量后,visible的值可以被Dialog组件监听并响应
.onClick(() => this.visible = !this.visible)
.margin({ top: 40 });
// 通过双向绑定visible变量,实现外部控制弹窗
Dialog({
visible: $visible,
});
}
.width('100%');
}
}
运行效果图如下:

点击放大

总结
实现全局可调用弹窗可通过openCustomDialog、子窗口与自定义弹窗方式实现,openCustomDialog通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求,优先推荐使用。子窗口方式需要使用页面当作弹窗内容且需要获取窗口管理器,代码复杂性较高。自定义弹窗方式与调用UI界面耦合性较高也不建议使用。
https://coub.com/view/4b7ub4
https://coub.com/view/4b7ub3
https://coub.com/view/4b7ub2
https://coub.com/view/4b7ub0
https://coub.com/view/4b7uay
https://coub.com/view/4b7uax
https://coub.com/view/4b7uav
https://coub.com/view/4b7uap
https://coub.com/view/4b7uao
https://coub.com/view/4b7ual
https://coub.com/view/4b7uad
https://coub.com/view/4b7uaa
https://coub.com/view/4b7ua7
https://coub.com/view/4b7ua5
https://coub.com/view/4b7ua2
https://coub.com/view/4b7ua1
https://coub.com/view/4b7u9z
https://coub.com/view/4b7u9y
https://coub.com/view/4b7u9x
https://coub.com/view/4b7u9p
https://coub.com/view/4b7u9k
https://coub.com/view/4b7u9j
https://coub.com/view/4b7u9i
https://coub.com/view/4b7u9g
https://coub.com/view/4b7u9d
https://coub.com/view/4b7u9a
https://coub.com/view/4b7u98
https://coub.com/view/4b7u96
https://coub.com/view/4b7u94
https://coub.com/view/4b7u92
https://coub.com/view/4b7u91
https://coub.com/view/4b7u8z
https://coub.com/view/4b7u8t
https://coub.com/view/4b7u8q
https://coub.com/view/4b7u8p
https://coub.com/view/4b7u8n
https://coub.com/view/4b7u8j
https://coub.com/view/4b7u8i
https://coub.com/view/4b7u78
https://coub.com/view/4b7u76
https://coub.com/view/4b7u72
https://coub.com/view/4b7u6v
https://coub.com/view/4b7u6t
https://coub.com/view/4b7u6r
https://coub.com/view/4b7u6o
https://coub.com/view/4b7u6m
https://coub.com/view/4b7u6l
https://coub.com/view/4b7u6j
https://coub.com/view/4b7u6h
https://coub.com/view/4b7u6f
https://coub.com/view/4b7u6d
https://coub.com/view/4b7u6a
https://coub.com/view/4b7u67
https://coub.com/view/4b7u65
https://coub.com/view/4b7u60
https://coub.com/view/4b7u5t
https://coub.com/view/4b7u5p
https://coub.com/view/4b7u5i
https://coub.com/view/4b7u4t
https://coub.com/view/4b7u4p
https://coub.com/view/4b7u4l
https://coub.com/view/4b7u46
https://coub.com/view/4b7u3v
https://coub.com/view/4b7u3u
https://coub.com/view/4b7u3t
https://coub.com/view/4b7u3r
https://coub.com/view/4b7u3o
https://coub.com/view/4b7u3l
https://coub.com/view/4b7u3k
https://coub.com/view/4b7u3h
https://coub.com/view/4b7u3a
https://coub.com/view/4b7u38
https://coub.com/view/4b7u35
https://coub.com/view/4b7u34
https://coub.com/view/4b7u33
https://coub.com/view/4b7u30
https://coub.com/view/4b7u2z
https://coub.com/view/4b7u2w

https://coub.com/view/4b7pz5
https://coub.com/view/4b7pz2
https://coub.com/view/4b7pyu
https://coub.com/view/4b7pyr
https://coub.com/view/4b7pyp
https://coub.com/view/4b7pym
https://coub.com/view/4b7pyk
https://coub.com/view/4b7pya
https://coub.com/view/4b7py6
https://coub.com/view/4b7py5
https://coub.com/view/4b7py2
https://coub.com/view/4b7pxi
https://coub.com/view/4b7px8
https://coub.com/view/4b7pxd
https://coub.com/view/4b7px4
https://coub.com/view/4b7px2
https://coub.com/view/4b7pwy
https://coub.com/view/4b7pwt
https://coub.com/view/4b7pwi
https://coub.com/view/4b7pwb
https://coub.com/view/4b7pvo
https://coub.com/view/4b7pvk
https://coub.com/view/4b7pvf
https://coub.com/view/4b7pvd
https://coub.com/view/4b7pv9
https://coub.com/view/4b7pv7
https://coub.com/view/4b7puz
https://coub.com/view/4b7puu
https://coub.com/view/4b7pur
https://coub.com/view/4b7puj
https://coub.com/view/4b7puf
https://coub.com/view/4b7puc
https://coub.com/view/4b7pu5
https://coub.com/view/4b7pu3
https://coub.com/view/4b7pty
https://coub.com/view/4b7ptw
https://coub.com/view/4b7ptu
https://coub.com/view/4b7ptt
https://coub.com/view/4b7ptr
https://coub.com/view/4b7ptp
https://coub.com/view/4b7ptn
https://coub.com/view/4b7ptm
https://coub.com/view/4b7ptk
https://coub.com/view/4b7ptj
https://coub.com/view/4b7ptf
https://coub.com/view/4b7ptd
https://coub.com/view/4b7ptb
https://coub.com/view/4b7pt9
https://coub.com/view/4b7pt6
https://coub.com/view/4b7psx
https://coub.com/view/4b7pso
https://coub.com/view/4b7psl
https://coub.com/view/4b7psh
https://coub.com/view/4b7psc
https://coub.com/view/4b7ps7
https://coub.com/view/4b7ps2
https://coub.com/view/4b7prv
https://coub.com/view/4b7prm
https://coub.com/view/4b7prj
https://coub.com/view/4b7pri

https://coub.com/view/4b7lh7
https://coub.com/view/4b7lh5
https://coub.com/view/4b7lh4
https://coub.com/view/4b7lh1
https://coub.com/view/4b7lh0
https://coub.com/view/4b7lgx
https://coub.com/view/4b7lgu
https://coub.com/view/4b7lgr
https://coub.com/view/4b7lgo
https://coub.com/view/4b7lgj
https://coub.com/view/4b7lgh
https://coub.com/view/4b7lgd
https://coub.com/view/4b7lgb
https://coub.com/view/4b7lg6
https://coub.com/view/4b7lg2
https://coub.com/view/4b7lfw
https://coub.com/view/4b7lft
https://coub.com/view/4b7lfn
https://coub.com/view/4b7lfh
https://coub.com/view/4b7lfa
https://coub.com/view/4b7lek
https://coub.com/view/4b7leh
https://coub.com/view/4b7lef
https://coub.com/view/4b7lec
https://coub.com/view/4b7leb
https://coub.com/view/4b7le7
https://coub.com/view/4b7le1
https://coub.com/view/4b7ldz
https://coub.com/view/4b7ldx
https://coub.com/view/4b7ldw
https://coub.com/view/4b7ldt
https://coub.com/view/4b7ldq
https://coub.com/view/4b7ldn
https://coub.com/view/4b7lda
https://coub.com/view/4b7ld8
https://coub.com/view/4b7ld4
https://coub.com/view/4b7lcz
https://coub.com/view/4b7lcv
https://coub.com/view/4b7lcq
https://coub.com/view/4b7lco
https://coub.com/view/4b7lcm
https://coub.com/view/4b7lcj
https://coub.com/view/4b7lcd
https://coub.com/view/4b7lc9
https://coub.com/view/4b7lc0
https://coub.com/view/4b7lbx
https://coub.com/view/4b7lbw
https://coub.com/view/4b7lbv
https://coub.com/view/4b7lbu
https://coub.com/view/4b7lbt
https://coub.com/view/4b7lbc
https://coub.com/view/4b7lb5
https://coub.com/view/4b7lax
https://coub.com/view/4b7laj
https://coub.com/view/4b7laf
https://coub.com/view/4b7lae
https://coub.com/view/4b7lab
https://coub.com/view/4b7la7
https://coub.com/view/4b7l9u
https://coub.com/view/4b7l9s
https://coub.com/view/4b7l9q
https://coub.com/view/4b7l9n
https://coub.com/view/4b7l9k
https://coub.com/view/4b7l9j
https://coub.com/view/4b7l9i
https://coub.com/view/4b7l9h
https://coub.com/view/4b7l9g
https://coub.com/view/4b7l9f
https://coub.com/view/4b7l99
https://coub.com/view/4b7l95
https://coub.com/view/4b7l93
https://coub.com/view/4b7l90
https://coub.com/view/4b7l8u
https://coub.com/view/4b7l8r
https://coub.com/view/4b7l8q
https://coub.com/view/4b7l8p
https://coub.com/view/4b7l8o
https://coub.com/view/4b7l8m
https://coub.com/view/4b7l8k
https://coub.com/view/4b7l8g
https://coub.com/view/4b7l8f
https://coub.com/view/4b7l8e
https://coub.com/view/4b7l8b
https://coub.com/view/4b7l88
https://coub.com/view/4b7l86
https://coub.com/view/4b7l84
https://coub.com/view/4b7l81
https://coub.com/view/4b7l7z
https://coub.com/view/4b7l7x
https://coub.com/view/4b7l7w
https://coub.com/view/4b7l7v
https://coub.com/view/4b7l7u
https://coub.com/view/4b7l7s

继续阅读 »

问题现象
应用/元服务如何实现一个全局可调用的弹窗?

背景知识
windowStage为窗口管理器,管理各个基本窗口单元。
通过CustomDialogController类显示自定义弹窗,需要使用@CustomDialog定义弹窗内容。调用弹窗时,需要new CustomDialogController(),并将@CustomDialog作为CustomDialogControllerOptions中的builder参数传入。
openCustomDialog用于创建并弹出dialogContent对应的自定义弹窗。
解决方案
实现一个全局可调用弹窗,可通过如下三种方式实现:

方案一:实现全局可调用弹窗推荐使用:不依赖UI组件的全局自定义弹出框 (openCustomDialog),示例请参考完整示例。
方案二:可使用子窗口方式实现全局可调用弹窗,实现方式如下:
在EntryAbility.ets中通过AppStorage获取窗口管理器用于后续获取Window实例实现子窗口的创建与销毁,代码如下:
import { ConfigurationConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
onCreate(): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}

onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}

onWindowStageCreate(windowStage: window.WindowStage): void {
AppStorage.setOrCreate('windowStage', windowStage);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}

onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}

onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}

onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
};
SubWindow.ets页面封装打开弹窗createMySubWindowFun方法,通过createSubWindow实现弹窗的弹出,使用setUIContent设置SubPage页面为子窗口加载的具体页面内容,通过moveWindowTo、resize等方法设置子窗口样式。封装deleteMySubWindowFun方法通过destroyWindow实现弹窗的关闭。代码如下:
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 获取windowStage
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
let windowClass: window.Window | undefined = undefined;

export class mySubWindow {
createMySubWindowFun(subWindowName: string) {
// 创建子窗口
windowStage.createSubWindow(subWindowName, (err: BusinessError, data) => {
windowClass = data;
data.setUIContent('pages/SubPage', () => {
// 设置子窗口左上角坐标
data.moveWindowTo(150, 300);
// 设置子窗口大小
data.resize(1000, 1000);
// 展示子窗口
data.showWindow();
});
});
}

deleteMySubWindowFun() {
if (windowClass) {
windowClass.destroyWindow();
}
}
}
SubPage.ets页面用于展示子窗页面内容,代码如下:
// 子窗口显示页面
import { mySubWindow } from '../utils/SubWindow';

@Entry
@Component
struct SubPage {
mySubWindow: mySubWindow = new mySubWindow();

build() {
Column() {
Text('子窗口页面')
.fontSize(40)
.margin({ top: 30 });
Button('点击关闭')
.onClick(() => {
this.mySubWindow.deleteMySubWindowFun();
})
.margin({ top: 100 });
}
.width('100%')
.height('100%');
}
}
Index.ets页面使用方式通过引入SubWindow页面,通过调用封装的createMySubWindowFun方法实现弹窗的打开,代码如下:
import { mySubWindow } from '../utils/SubWindow';

@Entry
@Component
struct Index {
mySubWindow: mySubWindow = new mySubWindow();

build() {
RelativeContainer() {
Text('点击打开弹窗')
.id('HelloWorld')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})
.onClick(() => {
this.mySubWindow.createMySubWindowFun('mySubWindow');
});
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5');
}
}
运行效果图如下:

点击放大

方案三:通过自定义弹窗(CustomDialog)封装成自定义组件的方式:
封装弹窗控制方法Dialog.ets,使用@watch监听外部传入的变量变化,通过CustomDialogController控制弹窗的显示隐藏,代码如下:
import { CustomDialogView } from './CustomDialogView';

@Component
export struct Dialog {
// 监听外部传入的visible变量,visible值发生变化时触发onChange回调函数
@Watch('onChange') @Link visible: boolean;
onCancel?: () => void;
onConfirm?: () => void;
// 通过CustomDialogController的builder参数绑定弹窗组件CustomDialogView
private controller = new CustomDialogController({
builder: CustomDialogView({
visible: $visible,
onCancel: this.onCancel,
onConfirm: this.onConfirm,
}),
autoCancel: false,
alignment: DialogAlignment.Center,
});

// 当visible的值变化时触发回调
onChange(): void {
if (this.visible) {
this.controller.open();
} else {
this.controller.close();
}
}

// 二次封装的Dialog组件主要通过控制器控制弹窗,不需要任何界面
build() {
}
}
封装弹窗视图页面CustomDialogView.ets,作用于弹窗中作为展示页面,代码如下:
@CustomDialog
export struct CustomDialogView {
@Link visible: boolean;
controller: CustomDialogController;
// 弹窗交互事件参数,点击确认和取消按钮时的回调函数
onCancel?: () => void;
onConfirm?: () => void;

build() {
Column() {
Text('自定义弹窗')
.margin({ top: 20, bottom: 40 });
Button('点击关闭弹窗').onClick(() => {
// 重置弹窗显隐参数
this.visible = false;
// 关闭弹窗
this.controller.close();
});
}
.width('100%')
.height(200)
.backgroundColor(Color.White);
}
}
使用弹窗页面CustomDialog.ets,通过给引入的Dialog组件传入visible控制弹窗的弹出,代码如下:
import { Dialog } from './Dialog';

@Entry
@Component
export struct CustomDialog {
// 外部定义visible变量作为弹窗组件入参,控制弹窗显隐
@State visible: boolean = false;

build() {
Column({ space: 20 }) {
Button('点击打开弹窗')
// 点击修改visible变量后,visible的值可以被Dialog组件监听并响应
.onClick(() => this.visible = !this.visible)
.margin({ top: 40 });
// 通过双向绑定visible变量,实现外部控制弹窗
Dialog({
visible: $visible,
});
}
.width('100%');
}
}
运行效果图如下:

点击放大

总结
实现全局可调用弹窗可通过openCustomDialog、子窗口与自定义弹窗方式实现,openCustomDialog通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求,优先推荐使用。子窗口方式需要使用页面当作弹窗内容且需要获取窗口管理器,代码复杂性较高。自定义弹窗方式与调用UI界面耦合性较高也不建议使用。
https://coub.com/view/4b7ub4
https://coub.com/view/4b7ub3
https://coub.com/view/4b7ub2
https://coub.com/view/4b7ub0
https://coub.com/view/4b7uay
https://coub.com/view/4b7uax
https://coub.com/view/4b7uav
https://coub.com/view/4b7uap
https://coub.com/view/4b7uao
https://coub.com/view/4b7ual
https://coub.com/view/4b7uad
https://coub.com/view/4b7uaa
https://coub.com/view/4b7ua7
https://coub.com/view/4b7ua5
https://coub.com/view/4b7ua2
https://coub.com/view/4b7ua1
https://coub.com/view/4b7u9z
https://coub.com/view/4b7u9y
https://coub.com/view/4b7u9x
https://coub.com/view/4b7u9p
https://coub.com/view/4b7u9k
https://coub.com/view/4b7u9j
https://coub.com/view/4b7u9i
https://coub.com/view/4b7u9g
https://coub.com/view/4b7u9d
https://coub.com/view/4b7u9a
https://coub.com/view/4b7u98
https://coub.com/view/4b7u96
https://coub.com/view/4b7u94
https://coub.com/view/4b7u92
https://coub.com/view/4b7u91
https://coub.com/view/4b7u8z
https://coub.com/view/4b7u8t
https://coub.com/view/4b7u8q
https://coub.com/view/4b7u8p
https://coub.com/view/4b7u8n
https://coub.com/view/4b7u8j
https://coub.com/view/4b7u8i
https://coub.com/view/4b7u78
https://coub.com/view/4b7u76
https://coub.com/view/4b7u72
https://coub.com/view/4b7u6v
https://coub.com/view/4b7u6t
https://coub.com/view/4b7u6r
https://coub.com/view/4b7u6o
https://coub.com/view/4b7u6m
https://coub.com/view/4b7u6l
https://coub.com/view/4b7u6j
https://coub.com/view/4b7u6h
https://coub.com/view/4b7u6f
https://coub.com/view/4b7u6d
https://coub.com/view/4b7u6a
https://coub.com/view/4b7u67
https://coub.com/view/4b7u65
https://coub.com/view/4b7u60
https://coub.com/view/4b7u5t
https://coub.com/view/4b7u5p
https://coub.com/view/4b7u5i
https://coub.com/view/4b7u4t
https://coub.com/view/4b7u4p
https://coub.com/view/4b7u4l
https://coub.com/view/4b7u46
https://coub.com/view/4b7u3v
https://coub.com/view/4b7u3u
https://coub.com/view/4b7u3t
https://coub.com/view/4b7u3r
https://coub.com/view/4b7u3o
https://coub.com/view/4b7u3l
https://coub.com/view/4b7u3k
https://coub.com/view/4b7u3h
https://coub.com/view/4b7u3a
https://coub.com/view/4b7u38
https://coub.com/view/4b7u35
https://coub.com/view/4b7u34
https://coub.com/view/4b7u33
https://coub.com/view/4b7u30
https://coub.com/view/4b7u2z
https://coub.com/view/4b7u2w

https://coub.com/view/4b7pz5
https://coub.com/view/4b7pz2
https://coub.com/view/4b7pyu
https://coub.com/view/4b7pyr
https://coub.com/view/4b7pyp
https://coub.com/view/4b7pym
https://coub.com/view/4b7pyk
https://coub.com/view/4b7pya
https://coub.com/view/4b7py6
https://coub.com/view/4b7py5
https://coub.com/view/4b7py2
https://coub.com/view/4b7pxi
https://coub.com/view/4b7px8
https://coub.com/view/4b7pxd
https://coub.com/view/4b7px4
https://coub.com/view/4b7px2
https://coub.com/view/4b7pwy
https://coub.com/view/4b7pwt
https://coub.com/view/4b7pwi
https://coub.com/view/4b7pwb
https://coub.com/view/4b7pvo
https://coub.com/view/4b7pvk
https://coub.com/view/4b7pvf
https://coub.com/view/4b7pvd
https://coub.com/view/4b7pv9
https://coub.com/view/4b7pv7
https://coub.com/view/4b7puz
https://coub.com/view/4b7puu
https://coub.com/view/4b7pur
https://coub.com/view/4b7puj
https://coub.com/view/4b7puf
https://coub.com/view/4b7puc
https://coub.com/view/4b7pu5
https://coub.com/view/4b7pu3
https://coub.com/view/4b7pty
https://coub.com/view/4b7ptw
https://coub.com/view/4b7ptu
https://coub.com/view/4b7ptt
https://coub.com/view/4b7ptr
https://coub.com/view/4b7ptp
https://coub.com/view/4b7ptn
https://coub.com/view/4b7ptm
https://coub.com/view/4b7ptk
https://coub.com/view/4b7ptj
https://coub.com/view/4b7ptf
https://coub.com/view/4b7ptd
https://coub.com/view/4b7ptb
https://coub.com/view/4b7pt9
https://coub.com/view/4b7pt6
https://coub.com/view/4b7psx
https://coub.com/view/4b7pso
https://coub.com/view/4b7psl
https://coub.com/view/4b7psh
https://coub.com/view/4b7psc
https://coub.com/view/4b7ps7
https://coub.com/view/4b7ps2
https://coub.com/view/4b7prv
https://coub.com/view/4b7prm
https://coub.com/view/4b7prj
https://coub.com/view/4b7pri

https://coub.com/view/4b7lh7
https://coub.com/view/4b7lh5
https://coub.com/view/4b7lh4
https://coub.com/view/4b7lh1
https://coub.com/view/4b7lh0
https://coub.com/view/4b7lgx
https://coub.com/view/4b7lgu
https://coub.com/view/4b7lgr
https://coub.com/view/4b7lgo
https://coub.com/view/4b7lgj
https://coub.com/view/4b7lgh
https://coub.com/view/4b7lgd
https://coub.com/view/4b7lgb
https://coub.com/view/4b7lg6
https://coub.com/view/4b7lg2
https://coub.com/view/4b7lfw
https://coub.com/view/4b7lft
https://coub.com/view/4b7lfn
https://coub.com/view/4b7lfh
https://coub.com/view/4b7lfa
https://coub.com/view/4b7lek
https://coub.com/view/4b7leh
https://coub.com/view/4b7lef
https://coub.com/view/4b7lec
https://coub.com/view/4b7leb
https://coub.com/view/4b7le7
https://coub.com/view/4b7le1
https://coub.com/view/4b7ldz
https://coub.com/view/4b7ldx
https://coub.com/view/4b7ldw
https://coub.com/view/4b7ldt
https://coub.com/view/4b7ldq
https://coub.com/view/4b7ldn
https://coub.com/view/4b7lda
https://coub.com/view/4b7ld8
https://coub.com/view/4b7ld4
https://coub.com/view/4b7lcz
https://coub.com/view/4b7lcv
https://coub.com/view/4b7lcq
https://coub.com/view/4b7lco
https://coub.com/view/4b7lcm
https://coub.com/view/4b7lcj
https://coub.com/view/4b7lcd
https://coub.com/view/4b7lc9
https://coub.com/view/4b7lc0
https://coub.com/view/4b7lbx
https://coub.com/view/4b7lbw
https://coub.com/view/4b7lbv
https://coub.com/view/4b7lbu
https://coub.com/view/4b7lbt
https://coub.com/view/4b7lbc
https://coub.com/view/4b7lb5
https://coub.com/view/4b7lax
https://coub.com/view/4b7laj
https://coub.com/view/4b7laf
https://coub.com/view/4b7lae
https://coub.com/view/4b7lab
https://coub.com/view/4b7la7
https://coub.com/view/4b7l9u
https://coub.com/view/4b7l9s
https://coub.com/view/4b7l9q
https://coub.com/view/4b7l9n
https://coub.com/view/4b7l9k
https://coub.com/view/4b7l9j
https://coub.com/view/4b7l9i
https://coub.com/view/4b7l9h
https://coub.com/view/4b7l9g
https://coub.com/view/4b7l9f
https://coub.com/view/4b7l99
https://coub.com/view/4b7l95
https://coub.com/view/4b7l93
https://coub.com/view/4b7l90
https://coub.com/view/4b7l8u
https://coub.com/view/4b7l8r
https://coub.com/view/4b7l8q
https://coub.com/view/4b7l8p
https://coub.com/view/4b7l8o
https://coub.com/view/4b7l8m
https://coub.com/view/4b7l8k
https://coub.com/view/4b7l8g
https://coub.com/view/4b7l8f
https://coub.com/view/4b7l8e
https://coub.com/view/4b7l8b
https://coub.com/view/4b7l88
https://coub.com/view/4b7l86
https://coub.com/view/4b7l84
https://coub.com/view/4b7l81
https://coub.com/view/4b7l7z
https://coub.com/view/4b7l7x
https://coub.com/view/4b7l7w
https://coub.com/view/4b7l7v
https://coub.com/view/4b7l7u
https://coub.com/view/4b7l7s

收起阅读 »

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 请求库

request

大家好,我是 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 平台;
  • requestuploadFiledownloadFile 拥有不同的原生参数和 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 分别支持:

  • request
  • upload
  • download

三类操作共用实例配置、Interceptor 和错误契约,但在派发边界保留各自的原生参数,不使用 UPLOADDOWNLOAD 这样的伪 HTTP method。

3. 可以访问原生 Task

请求 Promise 提供 abort()taskonTask(),单次请求配置也可以使用 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 判断当前执行的是 requestupload 还是 download,避免依赖字符串或伪 method。

6. 统一错误与 JSON 解析策略

请求失败时统一抛出 LuchRequestError,并保留可用于判断的上下文,例如:

  • code
  • config
  • response
  • task
  • cause
  • raw
  • cancelMode

网络失败、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 版本。特别希望大家帮助验证:

  1. H5、App、微信小程序及其他小程序中的基础请求;
  2. uploaddownload 及进度事件;
  3. abort() 是否触发平台原生中断;
  4. onTask() 是否能取得预期的原生 Task;
  5. 不同平台返回的错误结构是否存在差异;
  6. nativeOptions 是否能正确传递平台新增参数;
  7. TypeScript 类型是否存在过严、过松或无法表达的场景;
  8. JavaScript 项目中的配置和错误行为是否足够清晰。

如何反馈

提交问题时,如果方便,请尽量提供以下信息:

运行平台:  
HBuilderX / uni-app 版本:  
luch-request 版本:  
使用 TypeScript 还是 JavaScript:  
最小复现代码:  
预期结果:  
实际结果:  
错误对象或控制台日志:

如果问题与取消、上传下载或原生 Task 有关,也请说明实际运行平台,以及是否能取得对应 Task。

相关链接

写在最后

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 平台;
  • requestuploadFiledownloadFile 拥有不同的原生参数和 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 分别支持:

  • request
  • upload
  • download

三类操作共用实例配置、Interceptor 和错误契约,但在派发边界保留各自的原生参数,不使用 UPLOADDOWNLOAD 这样的伪 HTTP method。

3. 可以访问原生 Task

请求 Promise 提供 abort()taskonTask(),单次请求配置也可以使用 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 判断当前执行的是 requestupload 还是 download,避免依赖字符串或伪 method。

6. 统一错误与 JSON 解析策略

请求失败时统一抛出 LuchRequestError,并保留可用于判断的上下文,例如:

  • code
  • config
  • response
  • task
  • cause
  • raw
  • cancelMode

网络失败、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 版本。特别希望大家帮助验证:

  1. H5、App、微信小程序及其他小程序中的基础请求;
  2. uploaddownload 及进度事件;
  3. abort() 是否触发平台原生中断;
  4. onTask() 是否能取得预期的原生 Task;
  5. 不同平台返回的错误结构是否存在差异;
  6. nativeOptions 是否能正确传递平台新增参数;
  7. TypeScript 类型是否存在过严、过松或无法表达的场景;
  8. JavaScript 项目中的配置和错误行为是否足够清晰。

如何反馈

提交问题时,如果方便,请尽量提供以下信息:

运行平台:  
HBuilderX / uni-app 版本:  
luch-request 版本:  
使用 TypeScript 还是 JavaScript:  
最小复现代码:  
预期结果:  
实际结果:  
错误对象或控制台日志:

如果问题与取消、上传下载或原生 Task 有关,也请说明实际运行平台,以及是否能取得对应 Task。

相关链接

写在最后

v4 的核心设计和自动化测试已经完成,但 uni-app 请求库真正困难的部分始终是多平台运行差异。

希望正在使用 uni-app 的开发者帮助验证 request、upload、download、取消和原生 Task 等能力。无论是兼容性问题、类型设计建议,还是某个平台特有的行为差异,都欢迎在 GitHub Issue 或本帖中反馈。

感谢每一位愿意安装、运行和提供反馈的开发者。

收起阅读 »

uni-app / uni-app x / 微信小程序 / 抖音小程序 / App 外包开发|全栈接单|二开修Bug / 后台 / 后端 / 前端

确认bug 外包接单 外包

uni-app / uni-app x / 微信小程序 / 抖音小程序 / App 外包开发|全栈接单|二开修Bug / 后台 / 后端 / 前端

20年开发经验,需要的咨询

uni-app / uni-app x / 微信小程序 / 抖音小程序 / App 外包开发|全栈接单|二开修Bug / 后台 / 后端 / 前端

20年开发经验,需要的咨询

打造高效移动应用,Android 与 iOS 性能优化策略与工具选择

iOS

'''之前一直做 iOS 开发,性能优化就是 Instruments 一套流程走下来,从 CPU 到内存到能耗都有对应的模板。后来接手一个 Flutter 跨平台项目,才发现 Android 和 iOS 的性能优化策略差别不小,用的工具也不一样,一套思路套两个平台行不通。这里把两个平台的优化策略和工具选择放在一起对比,给跨平台团队做个参考,重点是找出哪些策略是共通的、哪些要分开做。

启动速度优化

两个平台都很看重启动速度,但优化点不同。iOS 的冷启动受动态库加载和首屏渲染影响,优化方向是精简启动逻辑、延迟初始化、减少主线程首帧前的工作量,把不着急的初始化移到首帧之后。Android 启动受 Application 初始化影响更大,大量 SDK 在 Application 里同步初始化会明显拖慢启动,优化方向是 SDK 异步初始化、按需加载、能懒加载就懒加载。两个平台都可以通过拆分启动任务、错峰执行来改善,这个思路是共通的。

内存优化

内存问题的表现形式不同。iOS 常见的是循环引用导致的内存泄漏和图片缓存堆积,排查用 Instruments Allocations 做深度分析,或者用 KeyMob 的内存面板实时看曲线——退出页面时扫一眼曲线有没有回落,就能判断释放是否正常。Android 常见的是 Bitmap 占用过高和内存泄漏(Activity 泄漏居多),排查用 Android Studio 的 Memory Profiler,看 GC 后的内存水位。优化方向类似:及时释放资源、合理设置缓存上限、避免持有不必要的大对象,这些策略两个平台通用。

流畅度与帧率

流畅度优化的核心都是保证帧率稳定,但排查路径不同。iOS 端重点排查主线程耗时操作和离屏渲染(圆角、阴影、mask),用 Instruments Time Profiler 深挖,或者 KeyMob 实时监控 FPS 曲线,操作页面时看哪里掉帧。Android 端重点排查主线程阻塞(比如在主线程做网络请求或大文件 IO)和过度绘制,用 Android Studio 的 CPU Profiler 和 GPU Profiler,开启开发者选项里的"显示布局边界"可以直观看到过度绘制区域。Flutter 项目两个平台共用渲染层,优化思路一致,主要关注 build 和 layout 阶段的耗时,这部分优化一次两个平台同时受益。

能耗优化

能耗优化两个平台方向接近:减少后台定位频率、合批网络请求、控制唤醒。iOS 用 Instruments Energy Log 或 KeyMob 的能耗记录查看硬件调用情况,Android 用 Battery Historian 分析耗电明细。跨平台项目在共享代码层做合批和降频处理,两个平台同时受益。

工具怎么选

跨平台团队建议每个平台备一套监控工具:iOS 用 Instruments 做深度分析,日常开发用 KeyMob 实时看 CPU、内存、FPS 曲线,改完代码随手扫一眼;Android 用 Android Studio Profiler 做深度分析,日常用 PerfDog 或厂商工具快速验证。工具不一定要最全,能形成"日常快检 + 深度定位"的组合就行,重点是养成每次发版前跑一轮性能对比的习惯。'''

继续阅读 »

'''之前一直做 iOS 开发,性能优化就是 Instruments 一套流程走下来,从 CPU 到内存到能耗都有对应的模板。后来接手一个 Flutter 跨平台项目,才发现 Android 和 iOS 的性能优化策略差别不小,用的工具也不一样,一套思路套两个平台行不通。这里把两个平台的优化策略和工具选择放在一起对比,给跨平台团队做个参考,重点是找出哪些策略是共通的、哪些要分开做。

启动速度优化

两个平台都很看重启动速度,但优化点不同。iOS 的冷启动受动态库加载和首屏渲染影响,优化方向是精简启动逻辑、延迟初始化、减少主线程首帧前的工作量,把不着急的初始化移到首帧之后。Android 启动受 Application 初始化影响更大,大量 SDK 在 Application 里同步初始化会明显拖慢启动,优化方向是 SDK 异步初始化、按需加载、能懒加载就懒加载。两个平台都可以通过拆分启动任务、错峰执行来改善,这个思路是共通的。

内存优化

内存问题的表现形式不同。iOS 常见的是循环引用导致的内存泄漏和图片缓存堆积,排查用 Instruments Allocations 做深度分析,或者用 KeyMob 的内存面板实时看曲线——退出页面时扫一眼曲线有没有回落,就能判断释放是否正常。Android 常见的是 Bitmap 占用过高和内存泄漏(Activity 泄漏居多),排查用 Android Studio 的 Memory Profiler,看 GC 后的内存水位。优化方向类似:及时释放资源、合理设置缓存上限、避免持有不必要的大对象,这些策略两个平台通用。

流畅度与帧率

流畅度优化的核心都是保证帧率稳定,但排查路径不同。iOS 端重点排查主线程耗时操作和离屏渲染(圆角、阴影、mask),用 Instruments Time Profiler 深挖,或者 KeyMob 实时监控 FPS 曲线,操作页面时看哪里掉帧。Android 端重点排查主线程阻塞(比如在主线程做网络请求或大文件 IO)和过度绘制,用 Android Studio 的 CPU Profiler 和 GPU Profiler,开启开发者选项里的"显示布局边界"可以直观看到过度绘制区域。Flutter 项目两个平台共用渲染层,优化思路一致,主要关注 build 和 layout 阶段的耗时,这部分优化一次两个平台同时受益。

能耗优化

能耗优化两个平台方向接近:减少后台定位频率、合批网络请求、控制唤醒。iOS 用 Instruments Energy Log 或 KeyMob 的能耗记录查看硬件调用情况,Android 用 Battery Historian 分析耗电明细。跨平台项目在共享代码层做合批和降频处理,两个平台同时受益。

工具怎么选

跨平台团队建议每个平台备一套监控工具:iOS 用 Instruments 做深度分析,日常开发用 KeyMob 实时看 CPU、内存、FPS 曲线,改完代码随手扫一眼;Android 用 Android Studio Profiler 做深度分析,日常用 PerfDog 或厂商工具快速验证。工具不一定要最全,能形成"日常快检 + 深度定位"的组合就行,重点是养成每次发版前跑一轮性能对比的习惯。'''

收起阅读 »

Navigation在分栏模式下,如何给右侧空白部分设置默认页面

问题现象
Navigation在Split模式下,未点击左侧导航栏时,右侧子页显示区显示为空白。当未进行页面路由推送时,如何给右侧的空白部分设置默认展示页面?

背景知识
Navigation组件的分栏模式由mode属性控制,包括单栏(Stack)、分栏(Split)和自适应(Auto)三个属性。该属性默认为Auto模式,在该模式下会自动监听屏幕属性,当为折叠屏或平板时,默认分栏显示,在折叠状态或普通手机时可为单栏显示。
splitPlaceholder:在API20下,Navigation双栏模式支持设置右侧页面显示默认占位页,占位页仅作为UI展示页,不可获焦和响应事件。
解决方案
方案一:push方法推送默认页面。
编写默认页面,并重写onBackPressed返回,当在该页面返回时直接退出软件。
Navigation主页执行aboutToAppear,将默认页面加入页面栈进行默认显示。
import { window } from '@kit.ArkUI';

@Entry
@Component
struct NavigationExample {
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
private arr: number[] = [1, 2, 3];

aboutToAppear(): void {
this.pageInfos.pushPath({ name: 'DefaultPage' });
}

@Builder
PageMap(name: string) {
if (name === 'NavDestinationTitle1') {
pageOneTmp();
} else if (name === 'NavDestinationTitle2') {
pageTwoTmp();
} else if (name === 'NavDestinationTitle3') {
pageThreeTmp();
} else if (name === 'DefaultPage') {
DefaultPage();
}
}

build() {
Column() {
Navigation(this.pageInfos) {
TextInput({ placeholder: 'search...' })
.width('90%')
.height(40)
.backgroundColor('#FFFFFF');
List({ space: 12 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text('Page' + item)
.width('100%')
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
.onClick(() => {
this.pageInfos.pushPath({ name: 'NavDestinationTitle' + item });
});
};
}, (item: number) => item.toString());
}
.width('90%')
.margin({ top: 12 });
}
.title('主标题')
.mode(NavigationMode.Split)
.navDestination(this.PageMap);
}
.height('100%')
.width('100%')
.background('#F1F3F5');
}
}

// PageOne页面
@Component
export struct pageOneTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent1');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle1');
}
}

// PageTwo页面
@Component
export struct pageTwoTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent2');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle2');
}
}

// PageThree页面
@Component
export struct pageThreeTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent3');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle3');
}
}
@Component
export struct DefaultPage {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('DefaultPage');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('DefaultPage')
.onBackPressed(() => {
// 返回true表示自定义返回,能避免返回空白页面。返回false则表示系统默认返回,会返回空白页面。
try {
// 需要导入window
window.getLastWindow(this.getUIContext().getHostContext(), (err, win) => {
const errCode: number = err.code;
if (errCode) {
console.error(Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message});
return;
}
win.minimize((err) => {
const errCode: number = err.code;
if (errCode) {
console.error(Failed to minimize the window. Cause code: ${err.code}, message: ${err.message});
return;
}
console.info('Succeeded in minimizing the window.');
});
});
} catch (exception) {
console.error(Failed to obtain the top window. Cause code: ${exception.code}, message: ${exception.message});
}
return true;
});
}
}
实现效果如下:

点击放大

方案二:采用API20中的splitPlaceholder属性。
详情参考:示例14。

常见FAQ
Q:API20以下,当采用Auto模式时,如何判断是否需要推送默认显示页面?

A:可以通过以下方式在aboutToAppear中判断是否需要推送默认页面:

通过设备信息@ohos.deviceInfo接口获取设备的deviceType类型。
通过屏幕属性@ohos.display的isFoldable方法返回当前设备是否可折叠的结果。
总结
实现默认展示页面的方式为,创建一个默认页面,在创建主页时自动推送该默认页面即可,其他关联实现方式参照常见FAQ实现即可。
https://coub.com/view/4b7lh7
https://coub.com/view/4b7lh5
https://coub.com/view/4b7lh4
https://coub.com/view/4b7lh1
https://coub.com/view/4b7lh0
https://coub.com/view/4b7lgx
https://coub.com/view/4b7lgu
https://coub.com/view/4b7lgr
https://coub.com/view/4b7lgo
https://coub.com/view/4b7lgj
https://coub.com/view/4b7lgh
https://coub.com/view/4b7lgd
https://coub.com/view/4b7lgb
https://coub.com/view/4b7lg6
https://coub.com/view/4b7lg2
https://coub.com/view/4b7lfw
https://coub.com/view/4b7lft
https://coub.com/view/4b7lfn
https://coub.com/view/4b7lfh
https://coub.com/view/4b7lfa
https://coub.com/view/4b7lek
https://coub.com/view/4b7leh
https://coub.com/view/4b7lef
https://coub.com/view/4b7lec
https://coub.com/view/4b7leb
https://coub.com/view/4b7le7
https://coub.com/view/4b7le1
https://coub.com/view/4b7ldz
https://coub.com/view/4b7ldx
https://coub.com/view/4b7ldw
https://coub.com/view/4b7ldt
https://coub.com/view/4b7ldq
https://coub.com/view/4b7ldn
https://coub.com/view/4b7lda
https://coub.com/view/4b7ld8
https://coub.com/view/4b7ld4
https://coub.com/view/4b7lcz
https://coub.com/view/4b7lcv
https://coub.com/view/4b7lcq
https://coub.com/view/4b7lco
https://coub.com/view/4b7lcm
https://coub.com/view/4b7lcj
https://coub.com/view/4b7lcd
https://coub.com/view/4b7lc9
https://coub.com/view/4b7lc0
https://coub.com/view/4b7lbx
https://coub.com/view/4b7lbw
https://coub.com/view/4b7lbv
https://coub.com/view/4b7lbu
https://coub.com/view/4b7lbt
https://coub.com/view/4b7lbc
https://coub.com/view/4b7lb5
https://coub.com/view/4b7lax
https://coub.com/view/4b7laj
https://coub.com/view/4b7laf
https://coub.com/view/4b7lae
https://coub.com/view/4b7lab
https://coub.com/view/4b7la7
https://coub.com/view/4b7l9u
https://coub.com/view/4b7l9s
https://coub.com/view/4b7l9q
https://coub.com/view/4b7l9n
https://coub.com/view/4b7l9k
https://coub.com/view/4b7l9j
https://coub.com/view/4b7l9i
https://coub.com/view/4b7l9h
https://coub.com/view/4b7l9g
https://coub.com/view/4b7l9f
https://coub.com/view/4b7l99
https://coub.com/view/4b7l95
https://coub.com/view/4b7l93
https://coub.com/view/4b7l90
https://coub.com/view/4b7l8u
https://coub.com/view/4b7l8r
https://coub.com/view/4b7l8q
https://coub.com/view/4b7l8p
https://coub.com/view/4b7l8o
https://coub.com/view/4b7l8m
https://coub.com/view/4b7l8k
https://coub.com/view/4b7l8g
https://coub.com/view/4b7l8f
https://coub.com/view/4b7l8e
https://coub.com/view/4b7l8b
https://coub.com/view/4b7l88
https://coub.com/view/4b7l86
https://coub.com/view/4b7l84
https://coub.com/view/4b7l81
https://coub.com/view/4b7l7z
https://coub.com/view/4b7l7x
https://coub.com/view/4b7l7w
https://coub.com/view/4b7l7v
https://coub.com/view/4b7l7u
https://coub.com/view/4b7l7s

继续阅读 »

问题现象
Navigation在Split模式下,未点击左侧导航栏时,右侧子页显示区显示为空白。当未进行页面路由推送时,如何给右侧的空白部分设置默认展示页面?

背景知识
Navigation组件的分栏模式由mode属性控制,包括单栏(Stack)、分栏(Split)和自适应(Auto)三个属性。该属性默认为Auto模式,在该模式下会自动监听屏幕属性,当为折叠屏或平板时,默认分栏显示,在折叠状态或普通手机时可为单栏显示。
splitPlaceholder:在API20下,Navigation双栏模式支持设置右侧页面显示默认占位页,占位页仅作为UI展示页,不可获焦和响应事件。
解决方案
方案一:push方法推送默认页面。
编写默认页面,并重写onBackPressed返回,当在该页面返回时直接退出软件。
Navigation主页执行aboutToAppear,将默认页面加入页面栈进行默认显示。
import { window } from '@kit.ArkUI';

@Entry
@Component
struct NavigationExample {
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
private arr: number[] = [1, 2, 3];

aboutToAppear(): void {
this.pageInfos.pushPath({ name: 'DefaultPage' });
}

@Builder
PageMap(name: string) {
if (name === 'NavDestinationTitle1') {
pageOneTmp();
} else if (name === 'NavDestinationTitle2') {
pageTwoTmp();
} else if (name === 'NavDestinationTitle3') {
pageThreeTmp();
} else if (name === 'DefaultPage') {
DefaultPage();
}
}

build() {
Column() {
Navigation(this.pageInfos) {
TextInput({ placeholder: 'search...' })
.width('90%')
.height(40)
.backgroundColor('#FFFFFF');
List({ space: 12 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text('Page' + item)
.width('100%')
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
.onClick(() => {
this.pageInfos.pushPath({ name: 'NavDestinationTitle' + item });
});
};
}, (item: number) => item.toString());
}
.width('90%')
.margin({ top: 12 });
}
.title('主标题')
.mode(NavigationMode.Split)
.navDestination(this.PageMap);
}
.height('100%')
.width('100%')
.background('#F1F3F5');
}
}

// PageOne页面
@Component
export struct pageOneTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent1');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle1');
}
}

// PageTwo页面
@Component
export struct pageTwoTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent2');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle2');
}
}

// PageThree页面
@Component
export struct pageThreeTmp {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('NavDestinationContent3');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('NavDestinationTitle3');
}
}
@Component
export struct DefaultPage {
@Consume('pageInfos') pageInfos: NavPathStack;

build() {
NavDestination() {
Column() {
Text('DefaultPage');
}.width('100%').height('100%').justifyContent(FlexAlign.Center);
}.title('DefaultPage')
.onBackPressed(() => {
// 返回true表示自定义返回,能避免返回空白页面。返回false则表示系统默认返回,会返回空白页面。
try {
// 需要导入window
window.getLastWindow(this.getUIContext().getHostContext(), (err, win) => {
const errCode: number = err.code;
if (errCode) {
console.error(Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message});
return;
}
win.minimize((err) => {
const errCode: number = err.code;
if (errCode) {
console.error(Failed to minimize the window. Cause code: ${err.code}, message: ${err.message});
return;
}
console.info('Succeeded in minimizing the window.');
});
});
} catch (exception) {
console.error(Failed to obtain the top window. Cause code: ${exception.code}, message: ${exception.message});
}
return true;
});
}
}
实现效果如下:

点击放大

方案二:采用API20中的splitPlaceholder属性。
详情参考:示例14。

常见FAQ
Q:API20以下,当采用Auto模式时,如何判断是否需要推送默认显示页面?

A:可以通过以下方式在aboutToAppear中判断是否需要推送默认页面:

通过设备信息@ohos.deviceInfo接口获取设备的deviceType类型。
通过屏幕属性@ohos.display的isFoldable方法返回当前设备是否可折叠的结果。
总结
实现默认展示页面的方式为,创建一个默认页面,在创建主页时自动推送该默认页面即可,其他关联实现方式参照常见FAQ实现即可。
https://coub.com/view/4b7lh7
https://coub.com/view/4b7lh5
https://coub.com/view/4b7lh4
https://coub.com/view/4b7lh1
https://coub.com/view/4b7lh0
https://coub.com/view/4b7lgx
https://coub.com/view/4b7lgu
https://coub.com/view/4b7lgr
https://coub.com/view/4b7lgo
https://coub.com/view/4b7lgj
https://coub.com/view/4b7lgh
https://coub.com/view/4b7lgd
https://coub.com/view/4b7lgb
https://coub.com/view/4b7lg6
https://coub.com/view/4b7lg2
https://coub.com/view/4b7lfw
https://coub.com/view/4b7lft
https://coub.com/view/4b7lfn
https://coub.com/view/4b7lfh
https://coub.com/view/4b7lfa
https://coub.com/view/4b7lek
https://coub.com/view/4b7leh
https://coub.com/view/4b7lef
https://coub.com/view/4b7lec
https://coub.com/view/4b7leb
https://coub.com/view/4b7le7
https://coub.com/view/4b7le1
https://coub.com/view/4b7ldz
https://coub.com/view/4b7ldx
https://coub.com/view/4b7ldw
https://coub.com/view/4b7ldt
https://coub.com/view/4b7ldq
https://coub.com/view/4b7ldn
https://coub.com/view/4b7lda
https://coub.com/view/4b7ld8
https://coub.com/view/4b7ld4
https://coub.com/view/4b7lcz
https://coub.com/view/4b7lcv
https://coub.com/view/4b7lcq
https://coub.com/view/4b7lco
https://coub.com/view/4b7lcm
https://coub.com/view/4b7lcj
https://coub.com/view/4b7lcd
https://coub.com/view/4b7lc9
https://coub.com/view/4b7lc0
https://coub.com/view/4b7lbx
https://coub.com/view/4b7lbw
https://coub.com/view/4b7lbv
https://coub.com/view/4b7lbu
https://coub.com/view/4b7lbt
https://coub.com/view/4b7lbc
https://coub.com/view/4b7lb5
https://coub.com/view/4b7lax
https://coub.com/view/4b7laj
https://coub.com/view/4b7laf
https://coub.com/view/4b7lae
https://coub.com/view/4b7lab
https://coub.com/view/4b7la7
https://coub.com/view/4b7l9u
https://coub.com/view/4b7l9s
https://coub.com/view/4b7l9q
https://coub.com/view/4b7l9n
https://coub.com/view/4b7l9k
https://coub.com/view/4b7l9j
https://coub.com/view/4b7l9i
https://coub.com/view/4b7l9h
https://coub.com/view/4b7l9g
https://coub.com/view/4b7l9f
https://coub.com/view/4b7l99
https://coub.com/view/4b7l95
https://coub.com/view/4b7l93
https://coub.com/view/4b7l90
https://coub.com/view/4b7l8u
https://coub.com/view/4b7l8r
https://coub.com/view/4b7l8q
https://coub.com/view/4b7l8p
https://coub.com/view/4b7l8o
https://coub.com/view/4b7l8m
https://coub.com/view/4b7l8k
https://coub.com/view/4b7l8g
https://coub.com/view/4b7l8f
https://coub.com/view/4b7l8e
https://coub.com/view/4b7l8b
https://coub.com/view/4b7l88
https://coub.com/view/4b7l86
https://coub.com/view/4b7l84
https://coub.com/view/4b7l81
https://coub.com/view/4b7l7z
https://coub.com/view/4b7l7x
https://coub.com/view/4b7l7w
https://coub.com/view/4b7l7v
https://coub.com/view/4b7l7u
https://coub.com/view/4b7l7s

收起阅读 »