HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

如何解决拖拽功能和长按功能的冲突问题

问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?

效果预览
点击放大

背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';

@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;

aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}

itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}

// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}

// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}

// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}

// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}

// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}

// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}

// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}

// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}

// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}

// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}

build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

继续阅读 »

问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?

效果预览
点击放大

背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';

@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;

aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}

itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}

// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}

// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}

// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}

// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}

// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}

// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}

// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}

// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}

// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}

// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}

build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

收起阅读 »

Tab组件内Navigation跳转,TabBar导航栏隐藏失败如何解决

tabbar

问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。

点击放大点击放大

主页面有“首页”、“我的”两个Tab页面,部分代码如下:

@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))

  TabContent() {  
    Text('首页')  
  }  
  .tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))  
}  
.barPosition(BarPosition.End)  

}
}

@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}

build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}

@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();

build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大

背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。

将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。

@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}

build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))

    TabContent() {  
      Text('首页').fontColor('40fp');  
    }  
    .tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))  
  }  
  .barPosition(BarPosition.End);  
}  
.hideTitleBar(true)  
.navDestination(this.PagesMap)  

}
}

@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;

build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}

@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();

build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg

继续阅读 »

问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。

点击放大点击放大

主页面有“首页”、“我的”两个Tab页面,部分代码如下:

@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))

  TabContent() {  
    Text('首页')  
  }  
  .tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))  
}  
.barPosition(BarPosition.End)  

}
}

@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}

build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}

@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();

build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大

背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。

将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。

@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}

build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))

    TabContent() {  
      Text('首页').fontColor('40fp');  
    }  
    .tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))  
  }  
  .barPosition(BarPosition.End);  
}  
.hideTitleBar(true)  
.navDestination(this.PagesMap)  

}
}

@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;

build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}

@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();

build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg

收起阅读 »

Android离线打包,plus.runtime.install导致应用闪退

logcat日志如下:

Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run

参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674

在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:

dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}

继续阅读 »

logcat日志如下:

Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run

参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674

在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:

dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}

收起阅读 »

uni-starter模板代码更正

创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->

            <!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->  
            <uni-list-item  v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon  
             clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >  
            </uni-list-item>  
        </uni-list>  
    </view>  

这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。

继续阅读 »

创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->

            <!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->  
            <uni-list-item  v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon  
             clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >  
            </uni-list-item>  
        </uni-list>  
    </view>  

这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。

收起阅读 »

组件实现随手拖动松手后贴边效果

问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。

背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:

通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:

import { curves, display } from '@kit.ArkUI';

@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();

// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}

build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}

@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;

build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')

.onAreaChange((oldValue: Area, newValue: Area) => {  
  // 记录父组件的宽高  
  if (oldValue.width !== newValue.width) {  
    this.containerWidth = newValue.width as number;  
  }  

  if (oldValue.height !== newValue.height) {  
    this.containerHeight = newValue.height as number;  
  }  
})  

}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

继续阅读 »

问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。

背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:

通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:

import { curves, display } from '@kit.ArkUI';

@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();

// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}

build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}

@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;

build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')

.onAreaChange((oldValue: Area, newValue: Area) => {  
  // 记录父组件的宽高  
  if (oldValue.width !== newValue.width) {  
    this.containerWidth = newValue.width as number;  
  }  

  if (oldValue.height !== newValue.height) {  
    this.containerHeight = newValue.height as number;  
  }  
})  

}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

收起阅读 »

无Mac获取P12证书、描述文件、上传IPA

Appstore上传 ipa


1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
​​​​
2、然后使用邮箱注册并登陆

3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据

4、进入上传IPA功能,选择IPA文件上传即可

继续阅读 »


1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
​​​​
2、然后使用邮箱注册并登陆

3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据

4、进入上传IPA功能,选择IPA文件上传即可

收起阅读 »

在线教育知识付费直播源码免费下载|UniApp 多端可用

uniapp

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示

演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 FastAdmin MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2) TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法) Sass 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp 全局主题样式,小程序端内置构建适配补丁(scripts/
uView(本地 components/uview-ui 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 webpack @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.jsenableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包   public/app/user/order/course 五个分包,约 100  页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue js.vue css.css 三文件拆分(<script src="./js.vue"> @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
继续阅读 »

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示

演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 FastAdmin MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2) TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法) Sass 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp 全局主题样式,小程序端内置构建适配补丁(scripts/
uView(本地 components/uview-ui 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 webpack @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.jsenableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包   public/app/user/order/course 五个分包,约 100  页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue js.vue css.css 三文件拆分(<script src="./js.vue"> @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
收起阅读 »

等保测评wgt打包有高危漏洞!

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

App Store Connect API 密钥是什么,为什么推荐用它登录?

iOS

'''# App Store Connect API 密钥是什么,为什么推荐用它登录?

App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。

和账号密码登录相比的三个优势

对比项 账号密码登录 API 密钥登录
是否需要密码 需要 不需要
是否触发双重验证 会(首次) 不会
是否长期有效、可多端共用 依赖登录会话 是,生成一次可在任意设备导入使用

怎么获取

在「开心上架(AppUploader)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .json 文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。

密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。

什么场景必须用 API 密钥

「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''

继续阅读 »

'''# App Store Connect API 密钥是什么,为什么推荐用它登录?

App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。

和账号密码登录相比的三个优势

对比项 账号密码登录 API 密钥登录
是否需要密码 需要 不需要
是否触发双重验证 会(首次) 不会
是否长期有效、可多端共用 依赖登录会话 是,生成一次可在任意设备导入使用

怎么获取

在「开心上架(AppUploader)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .json 文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。

密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。

什么场景必须用 API 密钥

「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''

收起阅读 »

在线教育知识付费直播源码免费下载|UniApp 多端可用

付费 直播

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示

演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 + FastAdmin + MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2)+ TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 + Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 + Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法)+ Sass 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp 全局主题样式,小程序端内置构建适配补丁(scripts/
uView(本地 components/uview-ui 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio + 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 + webpack @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.jsenableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200+ 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue + js.vue + css.css 三文件拆分(<script src="./js.vue"> + @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
继续阅读 »

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2)+ TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示

演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 + FastAdmin + MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2)+ TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 + Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 + Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法)+ Sass 页面按 xxx.vue + js.vue + css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp 全局主题样式,小程序端内置构建适配补丁(scripts/
uView(本地 components/uview-ui 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45+ 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio + 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 + webpack @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.jsenableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200+ 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包 + public/app/user/order/course 五个分包,约 100+ 页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue + js.vue + css.css 三文件拆分(<script src="./js.vue"> + @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
收起阅读 »

组件截图(ComponentSnapshot)返回错误码100001,可能原因为截图尺寸过大,文档说明其与具体硬件限制有关,如何查看具体限制是多少

硬件限制因平台而异,可以通过如下命令进行查看:

hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。

如需实现离屏组件的长截图,可参考以下实现:

// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;

if (scrollYOffsets.length >= 2) {  
  let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];  
  if (listHeight - realScrollHeight > 0) {  
    let cropRegion: image.Region = {  
      x: 0,  
      y: context.vp2px(listHeight - realScrollHeight) || 0,  
      size: {  
        height: context.vp2px(realScrollHeight) || 0,  
        width: context.vp2px(listWidth) || 0  
      }  
    };  
    await pixelMap.crop(cropRegion);  
  }  
}  

let area: image.PositionArea = {  
  pixels: buffer,  
  offset: 0,  
  stride: stride,  
  region: {  
    size: {  
      width: 0,  
      height: 0  
    },  
    x: 0,  
    y: 0  
  }  
}  

try {  
  let imgInfo = pixelMap.getImageInfoSync();  
  area.region.size.width = imgInfo.size.width;  
  area.region.size.height = imgInfo.size.height;  
  pixelMap.readPixelsSync(area);  
} catch (err) {  
  let error = err as BusinessError;  
  console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);  
}  
return area;  

}

// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;

for (let i = 0; i < areaArray.length; i++) {  
  let readArea = areaArray[i];  
  let area: image.PositionArea = {  
    pixels: readArea.pixels,  
    offset: 0,  
    stride: readArea.stride,  
    region: {  
      size: {  
        width: readArea.region.size.width,  
        height: readArea.region.size.height  
      },  
      x: 0,  
      y: imgPosition  
    }  
  }  
  imgPosition += readArea.region.size.height;  
  try {  
    longPixelMap.writePixelsSync(area);  
  } catch (err) {  
    let error = err as BusinessError;  
    console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);  
  }  
}  
return longPixelMap;  

}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';

export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];

pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}

getAllData(): string[] {
return this._data;
}

totalCount(): number {
return this._data.length;
}

getData(index: number): string {
return this._data[index];
}

registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}

@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';

aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}

async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}

async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}

async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}

afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}

build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)

    // Use a mask to cover the screenshot area  
    Column()  
      .width('100%').height('100%').backgroundColor(Color.White)  
    // Long screenshot  
    Scroll() {  
      Image(this.mergedImage)  
    }  
  }  
  .width('100%')  
  .layoutWeight(1)  
}  

}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home

继续阅读 »

硬件限制因平台而异,可以通过如下命令进行查看:

hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。

如需实现离屏组件的长截图,可参考以下实现:

// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;

if (scrollYOffsets.length >= 2) {  
  let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];  
  if (listHeight - realScrollHeight > 0) {  
    let cropRegion: image.Region = {  
      x: 0,  
      y: context.vp2px(listHeight - realScrollHeight) || 0,  
      size: {  
        height: context.vp2px(realScrollHeight) || 0,  
        width: context.vp2px(listWidth) || 0  
      }  
    };  
    await pixelMap.crop(cropRegion);  
  }  
}  

let area: image.PositionArea = {  
  pixels: buffer,  
  offset: 0,  
  stride: stride,  
  region: {  
    size: {  
      width: 0,  
      height: 0  
    },  
    x: 0,  
    y: 0  
  }  
}  

try {  
  let imgInfo = pixelMap.getImageInfoSync();  
  area.region.size.width = imgInfo.size.width;  
  area.region.size.height = imgInfo.size.height;  
  pixelMap.readPixelsSync(area);  
} catch (err) {  
  let error = err as BusinessError;  
  console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);  
}  
return area;  

}

// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;

for (let i = 0; i < areaArray.length; i++) {  
  let readArea = areaArray[i];  
  let area: image.PositionArea = {  
    pixels: readArea.pixels,  
    offset: 0,  
    stride: readArea.stride,  
    region: {  
      size: {  
        width: readArea.region.size.width,  
        height: readArea.region.size.height  
      },  
      x: 0,  
      y: imgPosition  
    }  
  }  
  imgPosition += readArea.region.size.height;  
  try {  
    longPixelMap.writePixelsSync(area);  
  } catch (err) {  
    let error = err as BusinessError;  
    console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);  
  }  
}  
return longPixelMap;  

}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';

export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];

pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}

getAllData(): string[] {
return this._data;
}

totalCount(): number {
return this._data.length;
}

getData(index: number): string {
return this._data[index];
}

registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}

@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';

aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}

async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}

async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}

async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}

afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}

build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)

    // Use a mask to cover the screenshot area  
    Column()  
      .width('100%').height('100%').backgroundColor(Color.White)  
    // Long screenshot  
    Scroll() {  
      Image(this.mergedImage)  
    }  
  }  
  .width('100%')  
  .layoutWeight(1)  
}  

}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home

收起阅读 »

这20块独立打包费,算是喂狗了。

App打包

排除20分钟,使用独立打包,还是等了接近20分钟。。

排除20分钟,使用独立打包,还是等了接近20分钟。。