如何实现不同分组间元素拖拽切换效果
问题现象
如下图所示,需求是现在有A、B两个组,A、B两组中的元素可以拖动,并且A组中的元素可以拖动到B组,B组的元素同样可以拖动到A组,请问如何实现这种多组之间相互拖拽的效果?
点击放大
背景知识
使用Grid组件构建网格元素布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
onItemDragStart和onItemDrop事件分别在开始拖拽网格元素时触发和停止拖拽时触发,通过事件的组合完成交换数组位置的逻辑。
解决方案
使用Grid布局构建界面。其中,columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;columnsGap可设置列与列的间距;rowsGap可设置行与行的间距。
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});
if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});
if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation为true,即Grid拖拽元素时支持动画。
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
定义GridItem被拖拽时元素的样式,元素被拖拽时展示浮动内容。
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
判断当前移动的是不是标题,如果是标题禁止拖动。
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
定义拖拽过程中的数组交换逻辑。
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop中完成交换数组位置的逻辑。
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(eventX: ${event.x});
this.moveToIndex(itemIndex, insertIndex);
});
完整示例如下:
@Entry
@Component
struct GridDemo {
@State numbers: string[] = [];
@State numbers2: string[] = [];
@State isShowGroup1: boolean = true;
@State isShowGroup2: boolean = true;
@State text: string = 'drag';
scroller: Scroller = new Scroller();
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push('组' + i);
this.numbers2.push('组' + i);
}
}
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
build() {
Column({ space: 5 }) {
Column() {
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});
if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});
if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(`eventX: ${event.x}`);
this.moveToIndex(itemIndex, insertIndex);
});
};
}.width('100%').margin({ top: 5 });
}
}
https://pastebin.com/ALZ1heJ8
https://pastebin.com/NUA8cAMq
https://pastebin.com/DU7BW7SP
https://pastebin.com/Sh2K4277
https://pastebin.com/MKNCS5xM
https://pastebin.com/jBqWTCsQ
https://pastebin.com/TNhaEKd7
https://pastebin.com/pAcyuLWk
https://pastebin.com/BccwcGVK
https://pastebin.com/ETXeKqiW
https://pastebin.com/DRxMx41K
https://pastebin.com/L15SkLc9
https://pastebin.com/Pyz2h7xc
https://pastebin.com/LWtxbvUn
https://pastebin.com/S7wbsPSF
https://pastebin.com/XmPzcDSQ
https://pastebin.com/zJ2kJ7ZA
https://pastebin.com/wknFnVct
https://pastebin.com/Jx9edSj2
https://pastebin.com/s7w3uv0a
https://pastebin.com/JHwj1NaL
https://pastebin.com/RzdjWfNv
https://pastebin.com/9G3P7TeA
https://pastebin.com/sG5wxN1u
https://pastebin.com/hJ2c716v
https://pastebin.com/4exdhVkn
https://pastebin.com/hUVhcvMP
https://pastebin.com/A4LnBDtM
https://pastebin.com/L6FwB3N9
https://pastebin.com/pt0KRF9P
https://pastebin.com/93GGDNJv
https://pastebin.com/DHdskaAf
https://pastebin.com/1QH5dm9R
https://pastebin.com/P30Qqeif
https://pastebin.com/ngQjFN3W
https://pastebin.com/BRAv0z5d
https://pastebin.com/fdYZAvMX
https://pastebin.com/ciXgk5F6
https://pastebin.com/RjC4wjc9
https://pastebin.com/kEaUKCTq
https://pastebin.com/C3JuxCrQ
https://pastebin.com/y0KUAU4u
https://pastebin.com/nW5AFBcM
https://pastebin.com/yR9yyNZ5
https://pastebin.com/zvH6TX45
https://pastebin.com/RsL54ULk
https://pastebin.com/TRTYXt4k
https://pastebin.com/zCGW0vif
https://pastebin.com/6drbFd0j
https://pastebin.com/D7urfsq6
https://pastebin.com/119esj24
https://pastebin.com/ndHUJCr4
https://pastebin.com/ciSWy0ut
https://pastebin.com/iKf50her
https://pastebin.com/RkE5yyn8
https://pastebin.com/jj99Wa7u
https://pastebin.com/iwJ5us6j
https://pastebin.com/hSuvYeqx
https://pastebin.com/6YQNbNZ6
https://pastebin.com/SsUF39yN
https://pastebin.com/xLFVFWSd
https://pastebin.com/8C1YCkb7
https://pastebin.com/PBRiQwEE
https://pastebin.com/CZw00CFD
https://pastebin.com/MhC3vvEH
https://pastebin.com/NGucrr8R
https://pastebin.com/5inLc8eS
https://pastebin.com/rnNKvRei
https://pastebin.com/LDXYYnhG
https://pastebin.com/5YpXtQeT
https://pastebin.com/mb5JFUs5
https://pastebin.com/HDzxgFHY
https://pastebin.com/Ssx5zf6i
https://pastebin.com/qMCRpKuL
https://pastebin.com/YMyidNzi
https://pastebin.com/vSwjFPu0
https://pastebin.com/yFPJgYtk
https://pastebin.com/5FLbpDXJ
https://pastebin.com/vRj3ezdr
https://pastebin.com/mYUwfwNk
https://pastebin.com/dTx5DQSb
https://pastebin.com/e7jXBCMc
https://pastebin.com/dxgE2bcC
https://pastebin.com/STW6QhxS
https://pastebin.com/bZhrAr9R
https://pastebin.com/nfYTP7Au
https://pastebin.com/aWunSSVB
https://pastebin.com/zh4pAmyd
https://pastebin.com/3pqzBPyc
https://pastebin.com/ddwcDZHC
https://pastebin.com/2ZqNLikt
https://pastebin.com/56U4rWjG
https://pastebin.com/w24SbjAy
https://pastebin.com/by3Jn4Pq
https://pastebin.com/9UkiAZMK
https://pastebin.com/MxhA05RB
https://pastebin.com/4Jshmpfb
https://pastebin.com/L5sHYH9N
https://pastebin.com/0bJWTV7h
https://pastebin.com/AwM6iDiL
https://pastebin.com/Lsr4Cxkg
https://pastebin.com/LAfLC3a3
https://pastebin.com/f7wTnKEC
https://pastebin.com/HSEbHCyh
https://pastebin.com/uv6Hqjs4
https://pastebin.com/i2V8QUcy
https://pastebin.com/ZAPYfBNk
https://pastebin.com/waMXDnj3
https://pastebin.com/GMUKBZUs
https://pastebin.com/pezF3Vbk
https://pastebin.com/n7DYUQ5v
https://pastebin.com/cyT65uSE
https://pastebin.com/sx2iJqcn
https://pastebin.com/ZPqkWKaR
https://pastebin.com/HEyb77U9
https://pastebin.com/4FZufveK
https://pastebin.com/uExv5h76
https://pastebin.com/Ac7CHJc7
https://pastebin.com/M4s8ZR79
https://pastebin.com/nys7xPvA
https://pastebin.com/PehqarxQ
https://pastebin.com/RbdWT272
https://pastebin.com/UfFk8r7t
https://pastebin.com/y1XYanaB
https://pastebin.com/VmifaeiF
https://pastebin.com/2dDAckiN
https://pastebin.com/FAfELYhc
https://pastebin.com/QCFiEVnQ
https://pastebin.com/QbgF4PzX
https://pastebin.com/X4yVdKxp
https://pastebin.com/cuqhnADr
https://pastebin.com/8iUfUnL8
https://pastebin.com/xhFdDU3m
https://pastebin.com/5Yt1eLAQ
https://pastebin.com/XNDHeeM8
https://pastebin.com/kJXfXZ8R
https://pastebin.com/hxwe46wW
https://pastebin.com/B4mB17YB
https://pastebin.com/GQbTpcbY
https://pastebin.com/DpYauvxs
https://pastebin.com/1gsWBrLW
https://pastebin.com/g9WsSEzm
https://pastebin.com/hzqj45RD
https://pastebin.com/vWKXzBJ9
https://pastebin.com/GTajTfJB
https://pastebin.com/d52vdzg9
https://pastebin.com/w1xQpURf
https://pastebin.com/GVzvf2jD
https://pastebin.com/U5DNXZR5
https://pastebin.com/bBDcpTNV
https://pastebin.com/kaQ390Yd
https://pastebin.com/RHfrn44V
https://pastebin.com/ycM6hx0X
https://pastebin.com/sSUD6yBD
https://pastebin.com/vYZwLY68
https://pastebin.com/hsu9R9jj
https://pastebin.com/G6HZUJ4J
https://pastebin.com/tr8ae7XA
https://pastebin.com/j8cttQZk
https://pastebin.com/YeKMiByw
https://pastebin.com/j6Nnu9Qe
https://pastebin.com/LVGaaVQa
https://pastebin.com/JJWVEiS5
https://pastebin.com/GEFCqsT7
https://pastebin.com/3ZjJhLsT
https://pastebin.com/eRuatTXc
https://pastebin.com/6dLRhyiJ
https://pastebin.com/VS65myh8
https://pastebin.com/NK7i1SnE
https://pastebin.com/6Akg12TS
https://pastebin.com/Qs3xpNS5
https://pastebin.com/RjrrPqe0
https://pastebin.com/urvjTD8V
https://pastebin.com/63sbx7mm
https://pastebin.com/CxznX5Js
https://pastebin.com/yL6vTZKF
https://pastebin.com/ExT07Ygf
https://pastebin.com/QeQZdYcJ
https://pastebin.com/BY7bpMhj
https://pastebin.com/yAYUGpkf
https://pastebin.com/YXCzrDjU
https://pastebin.com/S10HgVGn
https://pastebin.com/LY6WLkTu
https://pastebin.com/3pfYGW1q
https://pastebin.com/3pV0G6nk
https://pastebin.com/CW4g7TJv
https://pastebin.com/M8J73bkP
https://pastebin.com/L9vcxB0W
https://pastebin.com/buUr51Ls
问题现象
如下图所示,需求是现在有A、B两个组,A、B两组中的元素可以拖动,并且A组中的元素可以拖动到B组,B组的元素同样可以拖动到A组,请问如何实现这种多组之间相互拖拽的效果?
点击放大
背景知识
使用Grid组件构建网格元素布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
onItemDragStart和onItemDrop事件分别在开始拖拽网格元素时触发和停止拖拽时触发,通过事件的组合完成交换数组位置的逻辑。
解决方案
使用Grid布局构建界面。其中,columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;columnsGap可设置列与列的间距;rowsGap可设置行与行的间距。
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});
if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});
if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation为true,即Grid拖拽元素时支持动画。
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
定义GridItem被拖拽时元素的样式,元素被拖拽时展示浮动内容。
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
判断当前移动的是不是标题,如果是标题禁止拖动。
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
定义拖拽过程中的数组交换逻辑。
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop中完成交换数组位置的逻辑。
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(eventX: ${event.x});
this.moveToIndex(itemIndex, insertIndex);
});
完整示例如下:
@Entry
@Component
struct GridDemo {
@State numbers: string[] = [];
@State numbers2: string[] = [];
@State isShowGroup1: boolean = true;
@State isShowGroup2: boolean = true;
@State text: string = 'drag';
scroller: Scroller = new Scroller();
@Builder
pixelMapBuilder() { // 拖拽过程样式
Column() {
Text('浮动内容' + this.text)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('90%')
.height(30)
.textAlign(TextAlign.Center);
};
}
aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push('组' + i);
this.numbers2.push('组' + i);
}
}
moveToIndex(index1: number, index2: number) { // 将index1位置的元素移动至 index2位置
let temp = '';
let num1Length = 1 + (this.numbers.length - 1) * Number(this.isShowGroup1);
if (index1 <= num1Length) {
temp = this.numbers.splice(index1 - 1, 1)[0];
} else {
index1 = index1 - this.numbers.length - 1;
temp = this.numbers2.splice(index1 - 1, 1)[0];
}
if (index2 <= num1Length) {
this.numbers.splice(index2 - 1, 0, temp);
} else {
index2 = index2 - this.numbers.length - 1;
this.numbers2.splice(index2 - 1, 0, temp);
}
}
// 判断是否是标题
isTitle(index: number) {
if (index === 0) {
return true;
}
if (!this.isShowGroup1 && index === 1) {
return true;
}
if (this.isShowGroup1 && index === this.numbers.length + 1) {
return true;
}
return false;
}
build() {
Column({ space: 5 }) {
Column() {
Grid(this.scroller) {
GridItem() {
Text('标题' + this.numbers.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup1 = !this.isShowGroup1;
});
if (this.isShowGroup1) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
GridItem() {
Text('标题' + this.numbers2.length)
.fontSize(16)
.width('100%')
.height(30)
.padding({ left: 10 })
.textAlign(TextAlign.Start);
}
.onClick(() => {
this.isShowGroup2 = !this.isShowGroup2;
});
if (this.isShowGroup2) {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#f1f3f5')
.width('100%')
.height(30)
.textAlign(TextAlign.Center);
};
});
}
}
.columnsTemplate('1fr')
.columnsGap(2)
.rowsGap(2)
.scrollBar(BarState.Off)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.supportAnimation(true)
.editMode(true) // 设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
if (this.isTitle(itemIndex)) {
return;
}
if (itemIndex > this.numbers.length) {
itemIndex = itemIndex - this.numbers.length - 1;
this.text = this.numbers2[itemIndex - 1];
} else {
this.text = this.numbers[itemIndex - 1];
}
return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number,
insertIndex: number) => { // 绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
console.info(`eventX: ${event.x}`);
this.moveToIndex(itemIndex, insertIndex);
});
};
}.width('100%').margin({ top: 5 });
}
}
https://pastebin.com/ALZ1heJ8
https://pastebin.com/NUA8cAMq
https://pastebin.com/DU7BW7SP
https://pastebin.com/Sh2K4277
https://pastebin.com/MKNCS5xM
https://pastebin.com/jBqWTCsQ
https://pastebin.com/TNhaEKd7
https://pastebin.com/pAcyuLWk
https://pastebin.com/BccwcGVK
https://pastebin.com/ETXeKqiW
https://pastebin.com/DRxMx41K
https://pastebin.com/L15SkLc9
https://pastebin.com/Pyz2h7xc
https://pastebin.com/LWtxbvUn
https://pastebin.com/S7wbsPSF
https://pastebin.com/XmPzcDSQ
https://pastebin.com/zJ2kJ7ZA
https://pastebin.com/wknFnVct
https://pastebin.com/Jx9edSj2
https://pastebin.com/s7w3uv0a
https://pastebin.com/JHwj1NaL
https://pastebin.com/RzdjWfNv
https://pastebin.com/9G3P7TeA
https://pastebin.com/sG5wxN1u
https://pastebin.com/hJ2c716v
https://pastebin.com/4exdhVkn
https://pastebin.com/hUVhcvMP
https://pastebin.com/A4LnBDtM
https://pastebin.com/L6FwB3N9
https://pastebin.com/pt0KRF9P
https://pastebin.com/93GGDNJv
https://pastebin.com/DHdskaAf
https://pastebin.com/1QH5dm9R
https://pastebin.com/P30Qqeif
https://pastebin.com/ngQjFN3W
https://pastebin.com/BRAv0z5d
https://pastebin.com/fdYZAvMX
https://pastebin.com/ciXgk5F6
https://pastebin.com/RjC4wjc9
https://pastebin.com/kEaUKCTq
https://pastebin.com/C3JuxCrQ
https://pastebin.com/y0KUAU4u
https://pastebin.com/nW5AFBcM
https://pastebin.com/yR9yyNZ5
https://pastebin.com/zvH6TX45
https://pastebin.com/RsL54ULk
https://pastebin.com/TRTYXt4k
https://pastebin.com/zCGW0vif
https://pastebin.com/6drbFd0j
https://pastebin.com/D7urfsq6
https://pastebin.com/119esj24
https://pastebin.com/ndHUJCr4
https://pastebin.com/ciSWy0ut
https://pastebin.com/iKf50her
https://pastebin.com/RkE5yyn8
https://pastebin.com/jj99Wa7u
https://pastebin.com/iwJ5us6j
https://pastebin.com/hSuvYeqx
https://pastebin.com/6YQNbNZ6
https://pastebin.com/SsUF39yN
https://pastebin.com/xLFVFWSd
https://pastebin.com/8C1YCkb7
https://pastebin.com/PBRiQwEE
https://pastebin.com/CZw00CFD
https://pastebin.com/MhC3vvEH
https://pastebin.com/NGucrr8R
https://pastebin.com/5inLc8eS
https://pastebin.com/rnNKvRei
https://pastebin.com/LDXYYnhG
https://pastebin.com/5YpXtQeT
https://pastebin.com/mb5JFUs5
https://pastebin.com/HDzxgFHY
https://pastebin.com/Ssx5zf6i
https://pastebin.com/qMCRpKuL
https://pastebin.com/YMyidNzi
https://pastebin.com/vSwjFPu0
https://pastebin.com/yFPJgYtk
https://pastebin.com/5FLbpDXJ
https://pastebin.com/vRj3ezdr
https://pastebin.com/mYUwfwNk
https://pastebin.com/dTx5DQSb
https://pastebin.com/e7jXBCMc
https://pastebin.com/dxgE2bcC
https://pastebin.com/STW6QhxS
https://pastebin.com/bZhrAr9R
https://pastebin.com/nfYTP7Au
https://pastebin.com/aWunSSVB
https://pastebin.com/zh4pAmyd
https://pastebin.com/3pqzBPyc
https://pastebin.com/ddwcDZHC
https://pastebin.com/2ZqNLikt
https://pastebin.com/56U4rWjG
https://pastebin.com/w24SbjAy
https://pastebin.com/by3Jn4Pq
https://pastebin.com/9UkiAZMK
https://pastebin.com/MxhA05RB
https://pastebin.com/4Jshmpfb
https://pastebin.com/L5sHYH9N
https://pastebin.com/0bJWTV7h
https://pastebin.com/AwM6iDiL
https://pastebin.com/Lsr4Cxkg
https://pastebin.com/LAfLC3a3
https://pastebin.com/f7wTnKEC
https://pastebin.com/HSEbHCyh
https://pastebin.com/uv6Hqjs4
https://pastebin.com/i2V8QUcy
https://pastebin.com/ZAPYfBNk
https://pastebin.com/waMXDnj3
https://pastebin.com/GMUKBZUs
https://pastebin.com/pezF3Vbk
https://pastebin.com/n7DYUQ5v
https://pastebin.com/cyT65uSE
https://pastebin.com/sx2iJqcn
https://pastebin.com/ZPqkWKaR
https://pastebin.com/HEyb77U9
https://pastebin.com/4FZufveK
https://pastebin.com/uExv5h76
https://pastebin.com/Ac7CHJc7
https://pastebin.com/M4s8ZR79
https://pastebin.com/nys7xPvA
https://pastebin.com/PehqarxQ
https://pastebin.com/RbdWT272
https://pastebin.com/UfFk8r7t
https://pastebin.com/y1XYanaB
https://pastebin.com/VmifaeiF
https://pastebin.com/2dDAckiN
https://pastebin.com/FAfELYhc
https://pastebin.com/QCFiEVnQ
https://pastebin.com/QbgF4PzX
https://pastebin.com/X4yVdKxp
https://pastebin.com/cuqhnADr
https://pastebin.com/8iUfUnL8
https://pastebin.com/xhFdDU3m
https://pastebin.com/5Yt1eLAQ
https://pastebin.com/XNDHeeM8
https://pastebin.com/kJXfXZ8R
https://pastebin.com/hxwe46wW
https://pastebin.com/B4mB17YB
https://pastebin.com/GQbTpcbY
https://pastebin.com/DpYauvxs
https://pastebin.com/1gsWBrLW
https://pastebin.com/g9WsSEzm
https://pastebin.com/hzqj45RD
https://pastebin.com/vWKXzBJ9
https://pastebin.com/GTajTfJB
https://pastebin.com/d52vdzg9
https://pastebin.com/w1xQpURf
https://pastebin.com/GVzvf2jD
https://pastebin.com/U5DNXZR5
https://pastebin.com/bBDcpTNV
https://pastebin.com/kaQ390Yd
https://pastebin.com/RHfrn44V
https://pastebin.com/ycM6hx0X
https://pastebin.com/sSUD6yBD
https://pastebin.com/vYZwLY68
https://pastebin.com/hsu9R9jj
https://pastebin.com/G6HZUJ4J
https://pastebin.com/tr8ae7XA
https://pastebin.com/j8cttQZk
https://pastebin.com/YeKMiByw
https://pastebin.com/j6Nnu9Qe
https://pastebin.com/LVGaaVQa
https://pastebin.com/JJWVEiS5
https://pastebin.com/GEFCqsT7
https://pastebin.com/3ZjJhLsT
https://pastebin.com/eRuatTXc
https://pastebin.com/6dLRhyiJ
https://pastebin.com/VS65myh8
https://pastebin.com/NK7i1SnE
https://pastebin.com/6Akg12TS
https://pastebin.com/Qs3xpNS5
https://pastebin.com/RjrrPqe0
https://pastebin.com/urvjTD8V
https://pastebin.com/63sbx7mm
https://pastebin.com/CxznX5Js
https://pastebin.com/yL6vTZKF
https://pastebin.com/ExT07Ygf
https://pastebin.com/QeQZdYcJ
https://pastebin.com/BY7bpMhj
https://pastebin.com/yAYUGpkf
https://pastebin.com/YXCzrDjU
https://pastebin.com/S10HgVGn
https://pastebin.com/LY6WLkTu
https://pastebin.com/3pfYGW1q
https://pastebin.com/3pV0G6nk
https://pastebin.com/CW4g7TJv
https://pastebin.com/M8J73bkP
https://pastebin.com/L9vcxB0W
https://pastebin.com/buUr51Ls
苹果 App Store 卡审核一天怎么办?别急着撤回,先看看是不是这几种情况
苹果 App Store 卡审核一天怎么办?别急着撤回,先看看是不是这几种情况
不少开发者在提交 App Store 审核后,发现状态一直停留在“等待审核(Waiting for Review)”或者“审核中(In Review)”超过一天,就开始担心是不是应用出了问题。
实际上,卡审一天并不一定意味着应用有问题。苹果审核时间会受到版本更新、审核队列、应用类型以及开发者账号历史等多方面因素影响。很多应用第二天甚至第三天就顺利通过了审核,因此第一天最重要的是先判断属于哪一种情况,而不是盲目撤回重新提交。
⸻
卡审一天正常吗?
对于大部分应用来说,卡审一天属于比较正常的情况。
苹果每天都会收到大量新的审核请求,审核资源会根据提交时间、应用类型、地区以及审核工作量动态调整。如果刚好遇到新系统发布、节假日或者大型活动期间,审核时间都会比平时更长。
因此,仅仅一天没有审核结果,并不能说明应用已经出现问题。
⸻
为什么会卡审核?
根据我们实际处理审核案例来看,卡审通常有以下几个原因。
1、审核队列较长
这是最常见的情况。
例如:
- 新版 iOS 发布前后
- 节假日前后
- 全球开发者集中提交版本
- 苹果审核资源紧张
这些都会导致等待时间延长。
如果只是等待审核,没有收到任何拒绝信息,通常建议继续耐心等待。
⸻
2、应用需要人工审核
部分应用会进入人工审核流程。
例如:
- AI 工具类
- 金融类
- 医疗健康类
- 社交类
- 用户生成内容(UGC)
- 涉及订阅或支付功能的应用
相比普通工具类应用,这类产品审核时间通常会更长。
⸻
3、版本改动较大
如果本次更新涉及:
- 大量新增功能
- 新增登录方式
- 新增支付能力
- 新增 AI 功能
- 大幅调整业务流程
审核人员通常需要更长时间体验,因此审核时间也可能增加。
⸻
4、开发者账号历史影响
苹果会结合开发者账号的历史审核情况。
如果账号曾经出现过:
- 多次被拒
- 多次修改后重新提交
- 较多相似应用
- 曾出现较长审核记录
新的版本可能会进入更严格的审核流程。
这并不代表一定会被拒,而是审核周期可能更长。
⸻
卡审一天应该怎么办?
如果只是一天,建议按照下面的顺序处理。
第一,先不要撤回版本
很多开发者最大的误区就是:
上午提交。
晚上还没审核。
直接撤回重新提交。
实际上,重新提交以后,审核队列通常会重新计算,反而可能让等待时间变得更长。
如果没有发现明显问题,不建议因为等待一天就撤回版本。
⸻
第二,确认是否收到苹果消息
登录 App Store Connect,查看是否收到:
- 补充审核信息
- 请求测试账号
- 请求演示视频
- 其他审核问题
如果苹果已经发来消息,应尽快回复,否则审核可能一直暂停。
⸻
第三,检查服务器是否正常
如果你的 App 依赖服务器接口,可以确认:
- 服务是否正常运行
- 登录是否正常
- 支付是否正常
- AI 接口是否正常
- 第三方接口是否稳定
审核期间如果服务器异常,也可能影响审核进度。
⸻
第四,耐心等待 24~48 小时
大多数情况下,等待一天属于正常范围。
如果超过两到三天仍没有任何变化,可以考虑联系苹果开发者支持或在 App Store Connect 中礼貌询问审核进度。
⸻
哪些操作不建议做?
卡审期间,建议避免以下操作:
- 不要频繁撤回重新提交。
- 不要连续上传多个新版本。
- 不要频繁修改元数据。
- 不要重复发送催审消息。
- 不要在没有收到反馈前大幅修改应用。
这些操作不一定能够加快审核,反而可能让审核流程重新开始。
⸻
什么情况下需要重点关注?
如果出现以下情况,就需要进一步分析:
- 等待审核超过 3~5 天没有变化。
- 审核中(In Review)停留很长时间。
- 多次提交都出现长时间卡审。
- 同一开发者账号多个应用都出现类似情况。
- 更新版本比首次上架审核时间明显更长。
这种情况可能需要结合应用类型、账号历史、版本改动等因素进行综合判断。
⸻
总结
卡审一天,大多数情况下属于正常现象。
开发者最应该做的是保持版本稳定,确认服务器正常运行,及时查看苹果是否发送审核消息,而不是因为等待时间稍长就反复撤回或重新提交。
如果等待时间持续较长,再结合应用类型、版本改动以及开发者账号情况进行分析,会比盲目修改更有效。保持耐心、及时响应审核要求,往往比频繁操作更有利于顺利完成审核。
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
苹果 App Store 卡审核一天怎么办?别急着撤回,先看看是不是这几种情况
不少开发者在提交 App Store 审核后,发现状态一直停留在“等待审核(Waiting for Review)”或者“审核中(In Review)”超过一天,就开始担心是不是应用出了问题。
实际上,卡审一天并不一定意味着应用有问题。苹果审核时间会受到版本更新、审核队列、应用类型以及开发者账号历史等多方面因素影响。很多应用第二天甚至第三天就顺利通过了审核,因此第一天最重要的是先判断属于哪一种情况,而不是盲目撤回重新提交。
⸻
卡审一天正常吗?
对于大部分应用来说,卡审一天属于比较正常的情况。
苹果每天都会收到大量新的审核请求,审核资源会根据提交时间、应用类型、地区以及审核工作量动态调整。如果刚好遇到新系统发布、节假日或者大型活动期间,审核时间都会比平时更长。
因此,仅仅一天没有审核结果,并不能说明应用已经出现问题。
⸻
为什么会卡审核?
根据我们实际处理审核案例来看,卡审通常有以下几个原因。
1、审核队列较长
这是最常见的情况。
例如:
- 新版 iOS 发布前后
- 节假日前后
- 全球开发者集中提交版本
- 苹果审核资源紧张
这些都会导致等待时间延长。
如果只是等待审核,没有收到任何拒绝信息,通常建议继续耐心等待。
⸻
2、应用需要人工审核
部分应用会进入人工审核流程。
例如:
- AI 工具类
- 金融类
- 医疗健康类
- 社交类
- 用户生成内容(UGC)
- 涉及订阅或支付功能的应用
相比普通工具类应用,这类产品审核时间通常会更长。
⸻
3、版本改动较大
如果本次更新涉及:
- 大量新增功能
- 新增登录方式
- 新增支付能力
- 新增 AI 功能
- 大幅调整业务流程
审核人员通常需要更长时间体验,因此审核时间也可能增加。
⸻
4、开发者账号历史影响
苹果会结合开发者账号的历史审核情况。
如果账号曾经出现过:
- 多次被拒
- 多次修改后重新提交
- 较多相似应用
- 曾出现较长审核记录
新的版本可能会进入更严格的审核流程。
这并不代表一定会被拒,而是审核周期可能更长。
⸻
卡审一天应该怎么办?
如果只是一天,建议按照下面的顺序处理。
第一,先不要撤回版本
很多开发者最大的误区就是:
上午提交。
晚上还没审核。
直接撤回重新提交。
实际上,重新提交以后,审核队列通常会重新计算,反而可能让等待时间变得更长。
如果没有发现明显问题,不建议因为等待一天就撤回版本。
⸻
第二,确认是否收到苹果消息
登录 App Store Connect,查看是否收到:
- 补充审核信息
- 请求测试账号
- 请求演示视频
- 其他审核问题
如果苹果已经发来消息,应尽快回复,否则审核可能一直暂停。
⸻
第三,检查服务器是否正常
如果你的 App 依赖服务器接口,可以确认:
- 服务是否正常运行
- 登录是否正常
- 支付是否正常
- AI 接口是否正常
- 第三方接口是否稳定
审核期间如果服务器异常,也可能影响审核进度。
⸻
第四,耐心等待 24~48 小时
大多数情况下,等待一天属于正常范围。
如果超过两到三天仍没有任何变化,可以考虑联系苹果开发者支持或在 App Store Connect 中礼貌询问审核进度。
⸻
哪些操作不建议做?
卡审期间,建议避免以下操作:
- 不要频繁撤回重新提交。
- 不要连续上传多个新版本。
- 不要频繁修改元数据。
- 不要重复发送催审消息。
- 不要在没有收到反馈前大幅修改应用。
这些操作不一定能够加快审核,反而可能让审核流程重新开始。
⸻
什么情况下需要重点关注?
如果出现以下情况,就需要进一步分析:
- 等待审核超过 3~5 天没有变化。
- 审核中(In Review)停留很长时间。
- 多次提交都出现长时间卡审。
- 同一开发者账号多个应用都出现类似情况。
- 更新版本比首次上架审核时间明显更长。
这种情况可能需要结合应用类型、账号历史、版本改动等因素进行综合判断。
⸻
总结
卡审一天,大多数情况下属于正常现象。
开发者最应该做的是保持版本稳定,确认服务器正常运行,及时查看苹果是否发送审核消息,而不是因为等待时间稍长就反复撤回或重新提交。
如果等待时间持续较长,再结合应用类型、版本改动以及开发者账号情况进行分析,会比盲目修改更有效。保持耐心、及时响应审核要求,往往比频繁操作更有利于顺利完成审核。
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
查看更多文章,看我主页
【需求反馈】关于鸿蒙平台支持页面透明背景的需求反馈
关于 uni-app 鸿蒙平台支持透明窗口页面的需求反馈
一、当前问题
我们在多个项目中,通过 pages\.json 配置透明背景实现透明弹窗页面,该方案在 APP-PLUS(安卓 /iOS)端可正常运行,但在鸿蒙平台不支持,导致原有功能失效。
现有可用配置(APP-PLUS IOS和安卓端)
{
"path": "pages/dialog/copy",
"style": {
"navigationStyle": "custom",
// #ifdef APP-PLUS
"backgroundColor": "transparent",
"backgroundColorTop": "transparent",
"backgroundColorBottom": "transparent",
// #endif
"app-plus": {
"animationType": "fade-in",
"background": "transparent",
"popGesture": "none"
}
}
}
鸿蒙平台存在的缺陷
-
不支持
pages.json中配置backgroundColor: transparent实现透明窗口; -
官方提供的鸿蒙专属 API
uni.setBackgroundColor(OBJECT)不支持设置透明值,无法替代原有方案; -
无官方等效方案,只能将所有页面弹窗改为自定义组件,改造工作量极大。
二、业务影响
-
多个存量项目大量页面使用透明页面做弹窗,无法直接兼容鸿蒙端;
-
若全部改用组件实现,需要大量修改老代码,耗时且容易产生兼容问题;
-
跨端一致性被破坏,APP 端正常运行、鸿蒙端无法使用。
三、需求建议
-
优先兼容原有配置
鸿蒙平台支持pages\.json中backgroundColor: transparent透明配置,与 APP 端保持一致,实现零成本兼容。 -
新增官方透明页面 API
参考 uni-app x 的uni.openDialogPage(options),在 uni-app 标准版鸿蒙平台提供官方透明弹窗页面方法。 -
完善现有 API
升级uni.setBackgroundColor,支持透明色值,补全基础能力。
四、核心诉求
透明窗口是弹窗类核心功能,恳请官方尽快适配鸿蒙平台透明页面能力,保持跨端统一,大幅降低老项目改造工作量。
关于 uni-app 鸿蒙平台支持透明窗口页面的需求反馈
一、当前问题
我们在多个项目中,通过 pages\.json 配置透明背景实现透明弹窗页面,该方案在 APP-PLUS(安卓 /iOS)端可正常运行,但在鸿蒙平台不支持,导致原有功能失效。
现有可用配置(APP-PLUS IOS和安卓端)
{
"path": "pages/dialog/copy",
"style": {
"navigationStyle": "custom",
// #ifdef APP-PLUS
"backgroundColor": "transparent",
"backgroundColorTop": "transparent",
"backgroundColorBottom": "transparent",
// #endif
"app-plus": {
"animationType": "fade-in",
"background": "transparent",
"popGesture": "none"
}
}
}
鸿蒙平台存在的缺陷
-
不支持
pages.json中配置backgroundColor: transparent实现透明窗口; -
官方提供的鸿蒙专属 API
uni.setBackgroundColor(OBJECT)不支持设置透明值,无法替代原有方案; -
无官方等效方案,只能将所有页面弹窗改为自定义组件,改造工作量极大。
二、业务影响
-
多个存量项目大量页面使用透明页面做弹窗,无法直接兼容鸿蒙端;
-
若全部改用组件实现,需要大量修改老代码,耗时且容易产生兼容问题;
-
跨端一致性被破坏,APP 端正常运行、鸿蒙端无法使用。
三、需求建议
-
优先兼容原有配置
鸿蒙平台支持pages\.json中backgroundColor: transparent透明配置,与 APP 端保持一致,实现零成本兼容。 -
新增官方透明页面 API
参考 uni-app x 的uni.openDialogPage(options),在 uni-app 标准版鸿蒙平台提供官方透明弹窗页面方法。 -
完善现有 API
升级uni.setBackgroundColor,支持透明色值,补全基础能力。
四、核心诉求
透明窗口是弹窗类核心功能,恳请官方尽快适配鸿蒙平台透明页面能力,保持跨端统一,大幅降低老项目改造工作量。
收起阅读 »鸿蒙应用商城搜索安装亿企标讯,跪求!!!可私信我付费
我们参加了华为鸿蒙的激励计划,可是下载量一直上不去,希望看到帖子的大大们帮帮忙
我们参加了华为鸿蒙的激励计划,可是下载量一直上不去,希望看到帖子的大大们帮帮忙
鸿蒙征文大赛获奖名单公示
本次鸿蒙征文大赛收到大量优质投稿,开发者们积极分享了基于 uni-app 的鸿蒙开发实践经验,完整征文列表详见:鸿蒙征文
获奖名单
一等奖
| 获奖作者 | 获奖文章 |
|---|---|
| yuhespace | 【鸿蒙征文】从创业小白到省赛获奖:我们用 uni-app 做出了"校园人人帮" |
| d***@qq.com | 【鸿蒙征文】 炸裂!我用uni-app三天让旧应用通杀鸿蒙Next+元服务,华为商店已上架!2W奖励金即将到账。 |
| l***@163.com | 精膳通智慧食堂的鸿蒙开发之旅:因 uni-app 而简化,为国产生态而让利 |
| clearliu | 从痛点到产品:uni-app x + HarmonyOS打造房产投资管理系统全记录 |
| nutpi | 从零到一:使用 uni-app x 开发鸿蒙 GitCode 目录树生成器 |
二等奖
三等奖
方向二特别奖
奖项设置
一等奖(5名):
如上奖品为二选一,随机发放;
- 华为手环7-NFC版
- 华为智能水杯450ml + 露营灯-无级调光-太阳能+Type-C充电 组合
二等奖(15名):
露营灯-无级调光,太阳能 或 Type-C充电,随机发货
三等奖(30名):
HUAWEI 无线蓝牙鼠标-双模办公-灰色 (价值99元)
方向二特别奖(10名):
针对【方向二·案例实战】的优质投稿,额外设立10份 华为手环 9 NFC版作为激励!
奖品领取
请各位获奖作者尽快提交自己的邮寄地址,我们会陆续联系获奖人员发放奖品。
邮寄地址提交方式:登录 ask社区,点击右上角个人头像,进入设置界面,设置界面下方补充快递邮寄地址。
感谢所有参与本次征文活动的开发者,你们的分享让uni-app和鸿蒙生态更加繁荣!
本次鸿蒙征文大赛收到大量优质投稿,开发者们积极分享了基于 uni-app 的鸿蒙开发实践经验,完整征文列表详见:鸿蒙征文
获奖名单
一等奖
| 获奖作者 | 获奖文章 |
|---|---|
| yuhespace | 【鸿蒙征文】从创业小白到省赛获奖:我们用 uni-app 做出了"校园人人帮" |
| d***@qq.com | 【鸿蒙征文】 炸裂!我用uni-app三天让旧应用通杀鸿蒙Next+元服务,华为商店已上架!2W奖励金即将到账。 |
| l***@163.com | 精膳通智慧食堂的鸿蒙开发之旅:因 uni-app 而简化,为国产生态而让利 |
| clearliu | 从痛点到产品:uni-app x + HarmonyOS打造房产投资管理系统全记录 |
| nutpi | 从零到一:使用 uni-app x 开发鸿蒙 GitCode 目录树生成器 |
二等奖
三等奖
方向二特别奖
奖项设置
一等奖(5名):
如上奖品为二选一,随机发放;
- 华为手环7-NFC版
- 华为智能水杯450ml + 露营灯-无级调光-太阳能+Type-C充电 组合
二等奖(15名):
露营灯-无级调光,太阳能 或 Type-C充电,随机发货
三等奖(30名):
HUAWEI 无线蓝牙鼠标-双模办公-灰色 (价值99元)
方向二特别奖(10名):
针对【方向二·案例实战】的优质投稿,额外设立10份 华为手环 9 NFC版作为激励!
奖品领取
请各位获奖作者尽快提交自己的邮寄地址,我们会陆续联系获奖人员发放奖品。
邮寄地址提交方式:登录 ask社区,点击右上角个人头像,进入设置界面,设置界面下方补充快递邮寄地址。
感谢所有参与本次征文活动的开发者,你们的分享让uni-app和鸿蒙生态更加繁荣!
收起阅读 »鸿蒙上架提审驳回理由常见解决方案
本文用于采集应用上架驳回理由,定期汇总到 uniapp 鸿蒙文档中。常见的问题可在 https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html 进行查询。如果你遇到了除此之外的驳回理由,可留言。
tab 走焦
近期有用户反馈,应用无法响应键盘 tab 按键
> 应用/元服务中的走焦事件能够响应tab键或方向键切换。https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/device-compatible
在电脑、平台、折叠电脑上属于规则级别,也就是强制要求。
临时规避方案1,自行明确当前是否需要支持平板,如果目前不需要可在代码中搜索 devicetype,保持为 phone ,在 uniapp 后台、agc 后台表格里只勾选手机,也就是避免支持平板。
解决方案2:你可在指定的组件中,添加 tabindex,从而让功能区支持 tab 切换。目前规则中未明确要求支持回车按键,可忽略处理,也可在组件中,使用 renderjs ,在 renderjs 的 mounted/unmounted 里监听、取消监听 addEventListener keydown 相关事件,主动触发 click
可参考来自社区热心用户的方案,请参考 https://ask.dcloud.net.cn/question/215693 评论区。
后续会针对性优化。
本文用于采集应用上架驳回理由,定期汇总到 uniapp 鸿蒙文档中。常见的问题可在 https://uniapp.dcloud.net.cn/tutorial/harmony/runbuild.html 进行查询。如果你遇到了除此之外的驳回理由,可留言。
tab 走焦
近期有用户反馈,应用无法响应键盘 tab 按键
> 应用/元服务中的走焦事件能够响应tab键或方向键切换。https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/device-compatible
在电脑、平台、折叠电脑上属于规则级别,也就是强制要求。
临时规避方案1,自行明确当前是否需要支持平板,如果目前不需要可在代码中搜索 devicetype,保持为 phone ,在 uniapp 后台、agc 后台表格里只勾选手机,也就是避免支持平板。
解决方案2:你可在指定的组件中,添加 tabindex,从而让功能区支持 tab 切换。目前规则中未明确要求支持回车按键,可忽略处理,也可在组件中,使用 renderjs ,在 renderjs 的 mounted/unmounted 里监听、取消监听 addEventListener keydown 相关事件,主动触发 click
可参考来自社区热心用户的方案,请参考 https://ask.dcloud.net.cn/question/215693 评论区。
后续会针对性优化。
收起阅读 »经验分享 鸿蒙隐私弹窗如何处理
基本背景
鸿蒙元服务不需要自己处理隐私弹窗,鸿蒙强制要求结束隐私协议托管,关联 client_id 之后会自动弹窗隐私协议,下面内容主要针对鸿蒙应用开发用户。
鸿蒙应用上架需要配置隐私协议、用户协议。这部分内容可参考 鸿蒙如何设置隐私协议弹窗
这里做技术实现说明。
内容已迁移到 鸿蒙如何设置隐私协议弹窗
补充:编写 uts 插件
如果应用需要提供不同意隐私协议也要提供基础内容浏览服务,可参考下面 uts 代码单独处理用户动态接受、拒绝隐私协议,主动唤起隐私协议相关逻辑。
编写 uts 插件,完成弹窗,这里提供代码片段。
新建 uts 插件,比如 hamrony-privacy-dialog ,编辑 uni_modules/harmony-privacy-dialog/utssdk/app-harmony/index.uts
填写下面代码,这段代码导出了三个方法,和官方的 ets 代码完全一致,检查隐私协议信息、获取签署状态、主动唤起隐私弹窗。页面中引入代码调用即可。
import { privacyManager } from '@kit.AppGalleryKit';
// import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export const getInfo = () => {
let err = ''
try {
let appPrivacyManageInfo : privacyManager.AppPrivacyMgmtInfo = privacyManager.getAppPrivacyMgmtInfo();
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type: " + appPrivacyManageInfo["type"]);
let privacyLinkInfoArray : privacyManager.AppPrivacyLink[] = appPrivacyManageInfo.privacyInfo;
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo size = " + privacyLinkInfoArray.length);
for (let i = 0; i < privacyLinkInfoArray.length; i++) {
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type = " + privacyLinkInfoArray[i]["type"] + ", version = " + privacyLinkInfoArray[i]["versionCode"] + ", url = " + privacyLinkInfoArray[i]["url"]);
}
} catch (error) {
err = error.message
console.error(0, 'TAG', "GetAppPrivacyManageInfoPublic exception code: " + error.code + ", exception message: " + error.message);
}
return err
}
export const getStatus = () => {
try {
let appPrivacyResults : privacyManager.AppPrivacyResult[] = privacyManager.getAppPrivacyResult();
console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult size = " + appPrivacyResults.length);
for (let i = 0; i < appPrivacyResults.length; i++) {
console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"]);
}
} catch (error) {
console.error(0, 'TAG', "GetAppPrivacyResultPublic exception code: " + error.code + ", exception message: " + error.message);
}
}
export const requestPrivacy = () => {
try {
const uiContext = UTSHarmony.getUIAbilityContext()
// const uiContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
privacyManager.requestAppPrivacyConsent(uiContext).then((consentResult : privacyManager.ConsentResult) => {
let appPrivacyResults : privacyManager.AppPrivacyResult[] = consentResult["results"];
for (let i = 0; i < appPrivacyResults.length; i++) {
console.info(0, 'TAG', "GetAppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"] + ", signingTimeStamp = " + appPrivacyResults[i]["signingTimeStamp"]);
}
}).catch((error : BusinessError<Object>) => {
console.error(0, 'TAG', `requestAppPrivacyConsent failed, Code: ${error.code}, message: ${error.message}`);
});
} catch (error) {
console.error(0, 'TAG', "requestAppPrivacyConsent exception code: " + error.code + ", exception message: " + error.message);
}
}
4 页面中引入
import {
getInfo,
getStatus,requestPrivacy
} from '@/uni_modules/harmony-privacy-dialog' 基本背景
鸿蒙元服务不需要自己处理隐私弹窗,鸿蒙强制要求结束隐私协议托管,关联 client_id 之后会自动弹窗隐私协议,下面内容主要针对鸿蒙应用开发用户。
鸿蒙应用上架需要配置隐私协议、用户协议。这部分内容可参考 鸿蒙如何设置隐私协议弹窗
这里做技术实现说明。
内容已迁移到 鸿蒙如何设置隐私协议弹窗
补充:编写 uts 插件
如果应用需要提供不同意隐私协议也要提供基础内容浏览服务,可参考下面 uts 代码单独处理用户动态接受、拒绝隐私协议,主动唤起隐私协议相关逻辑。
编写 uts 插件,完成弹窗,这里提供代码片段。
新建 uts 插件,比如 hamrony-privacy-dialog ,编辑 uni_modules/harmony-privacy-dialog/utssdk/app-harmony/index.uts
填写下面代码,这段代码导出了三个方法,和官方的 ets 代码完全一致,检查隐私协议信息、获取签署状态、主动唤起隐私弹窗。页面中引入代码调用即可。
import { privacyManager } from '@kit.AppGalleryKit';
// import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export const getInfo = () => {
let err = ''
try {
let appPrivacyManageInfo : privacyManager.AppPrivacyMgmtInfo = privacyManager.getAppPrivacyMgmtInfo();
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type: " + appPrivacyManageInfo["type"]);
let privacyLinkInfoArray : privacyManager.AppPrivacyLink[] = appPrivacyManageInfo.privacyInfo;
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo size = " + privacyLinkInfoArray.length);
for (let i = 0; i < privacyLinkInfoArray.length; i++) {
console.info(0, 'TAG', "Succeeded in getting AppPrivacyManageInfo type = " + privacyLinkInfoArray[i]["type"] + ", version = " + privacyLinkInfoArray[i]["versionCode"] + ", url = " + privacyLinkInfoArray[i]["url"]);
}
} catch (error) {
err = error.message
console.error(0, 'TAG', "GetAppPrivacyManageInfoPublic exception code: " + error.code + ", exception message: " + error.message);
}
return err
}
export const getStatus = () => {
try {
let appPrivacyResults : privacyManager.AppPrivacyResult[] = privacyManager.getAppPrivacyResult();
console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult size = " + appPrivacyResults.length);
for (let i = 0; i < appPrivacyResults.length; i++) {
console.info(0, 'TAG', "Succeeded in getting AppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"]);
}
} catch (error) {
console.error(0, 'TAG', "GetAppPrivacyResultPublic exception code: " + error.code + ", exception message: " + error.message);
}
}
export const requestPrivacy = () => {
try {
const uiContext = UTSHarmony.getUIAbilityContext()
// const uiContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
privacyManager.requestAppPrivacyConsent(uiContext).then((consentResult : privacyManager.ConsentResult) => {
let appPrivacyResults : privacyManager.AppPrivacyResult[] = consentResult["results"];
for (let i = 0; i < appPrivacyResults.length; i++) {
console.info(0, 'TAG', "GetAppPrivacyResult type = " + appPrivacyResults[i]["type"] + ", version = " + appPrivacyResults[i]["versionCode"] + ", result = " + appPrivacyResults[i]["result"] + ", signingTimeStamp = " + appPrivacyResults[i]["signingTimeStamp"]);
}
}).catch((error : BusinessError<Object>) => {
console.error(0, 'TAG', `requestAppPrivacyConsent failed, Code: ${error.code}, message: ${error.message}`);
});
} catch (error) {
console.error(0, 'TAG', "requestAppPrivacyConsent exception code: " + error.code + ", exception message: " + error.message);
}
}
4 页面中引入
import {
getInfo,
getStatus,requestPrivacy
} from '@/uni_modules/harmony-privacy-dialog' 收起阅读 »
【鸿蒙征文】重拾儿时趣味的古诗 App 的开发过程记录
一款无广告重拾儿时趣味的古诗 App 的开发过程记录
一、🌈缘起:让古诗 “活” 起来,而非躺在角落🌈
做这款 App 的初衷,藏在三个真实需求里:
一次偶然的寻找碰上鸿蒙的星光闪烁:翻找学生时代的语文诗词,想重拾那份诗词氛围时,书本早已不见踪影,此时便想在手机上寻找这类APP,市面古诗 App 要么广告扎堆,要么功能枯燥,实在感受不到当年的那种氛围;
身边朋友想重拾古诗,却缺合适的背诵工具,也没有专属交流渠道(总不能天天在朋友圈发古诗);
古诗的意境与字词之美,不该只靠 “死记硬背”—— 结合互动、朗读、释义,让大家 “懂了再背”,才是真正的文化传承。
所以核心需求很明确:做一款 “不枯燥、能互动、够纯粹” 的古诗工具。既要满足学习需求,又要兼顾趣味性,还得适配鸿蒙系统(毕竟鸿蒙用户越来越多,生态也日趋完善)。
二、💡技术选型:为啥 “锁死” UniAPP?💡
选技术栈时纠结了 3 天,最终敲定 UniAPP,全靠 “实用主义”:
排除鸿蒙原生开发:适配鸿蒙虽丝滑,但单人开发要重新学原生语法,还无法兼顾其他平台,效率太低;
排除其他跨平台框架:要么对鸿蒙适配不完善,要么插件生态薄弱,像诗词朗读、音频上传等功能得手动造轮子;
选择 UniAPP 的核心原因:
跨平台省心:一套代码可打包鸿蒙 App,后续扩展安卓、iOS 版本无需重构,对个人开发者极度友好;
- 鸿蒙适配 “自带 buff”:HBuilderX 提供现成打包模板和专属适配插件,不用手动修改大量配置;
- 生态够全:uniCloud 云开发、音频组件、数据库插件,刚好满足古诗存储、朗读上传、接龙匹配等核心需求;
- 学习成本低:有微信小程序开发基础,UniAPP 的 Vue 语法无缝衔接,1 周就能上手。
简单说:用 UniAPP 开发鸿蒙,就像 “站在巨人肩膀上”—— 不用从零学鸿蒙原生,又能享受鸿蒙系统特性,对非专业开发者太友好了!
三、⚠️踩坑实录:鸿蒙适配那些 “磨人的小妖精”⚠️
开发前以为 “跨平台 = 一路顺畅”,结果真正适配鸿蒙时,还是踩了不少坑,分享 3 个最印象深刻的:
权限申请:鸿蒙的 “规矩” 和其他平台不一样
做朗读广场时,需要调用手机麦克风录音、存储音频文件。一开始直接复用了其他平台的权限代码,结果在鸿蒙上直接报错 —— 录音功能打不开!
- 坑点:鸿蒙的 “媒体权限” 需要单独申请,而且必须配置ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA,还得在代码里动态弹窗说明 “为啥要要权限”(比如 “需要录音才能上传你的朗读作品”),光说 “需要权限” 会被系统拒绝;而且必须在鸿蒙的AGC后台进行同步申请该权限,因为该权限属于受限权限;而且!!!如果是使用的鸿蒙自家的隐私托管服务,必须要在隐私托管服务中同步该权限的声明
- 解决:使用uts插件配置权限;然后在AGC后台同步申请该受限权限通过率直接 100%。
// #ifdef APP-HARMONY import "@/uni_modules/harmony-permissions" // #endif
{
"module": {
"name": "uni_modules__harmony_permissions",
"type": "har",
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:media_desc",
"usedScene": {"when": "inuse"}
},{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:media_desc",
"usedScene": {"when": "inuse"}
}
]
}
}
服务卡片适配:桌面卡片让古诗 “触手可及”
鸿蒙的桌面卡片是个大亮点,我想做个 “每日一句” 卡片,用户不用打开 App,桌面就能看到古诗。
- 坑点:UniAPP 打包鸿蒙应用时,卡片的尺寸适配很麻烦,比如 3x2、2x2 的卡片布局会错乱,而且卡片数据刷新需要和主 App 同步;
- 解决:用 UniAPP 提供的uni-app x编译模式(专门优化鸿蒙适配),卡片布局用flex弹性盒,固定宽高比;数据同步用uniCloud实时数据库,主 App 更新 “每日一句” 后,卡片自动刷新,不用用户手动操作 —— 现在不少用户说 “每天解锁手机先看一眼古诗,感觉很治愈”。
音频播放:后台播放老是 “断档”
朗读广场的核心是 “听别人读古诗”,但一开始用户反馈 “退到后台就停了”,体验很差。
- 坑点:鸿蒙对后台音频播放有严格限制,普通的audio组件在后台会被系统暂停;
- 解决:集成了鸿蒙的AVPlayer SDK(UniAPP 可以直接调用鸿蒙原生 SDK),在manifest.json里配置 “后台音频权限”,同时用 UniAPP 的onBackground生命周期监听,退后台时自动切换到鸿蒙原生播放器,现在后台播放能稳定运行,用户还能边听古诗边刷微信~
四、📦功能落地:每个模块都是 “心头好”📦
结合古诗的核心需求,几个功能模块都是 “边踩坑边优化” 出来的,挑 3 个重点说:
-
背一背:用 “艾宾浩斯曲线” 让背诵不费脑
一开始只是简单做了 “列表 + 背诵打卡”,但用户反馈 “背了就忘”。后来用 UniAPP 的storage存储用户背诵记录,结合艾宾浩斯遗忘曲线,在对应时间点推送提醒(比如第 1 天、第 3 天、第 7 天),还加了 “遮字默写” 功能 —— 点击诗句能隐藏关键字,像考试一样检验效果。- 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
<view class="poem-content"> <block v-if="poemStore.showFullContent"> <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="full-line">{{ line }}</text> </block> <block v-else> <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="hide-line">□□□□□□□</text> </block> </view> <button class="toggle-btn" @click="poemStore.toggleFullContent"> {{ poemStore.showFullContent ? '切换到背诵模式' : '查看完整诗句' }} </button><script setup lang="ts"> import { usePoemStore } from '@/stores/poemStore'; import { useRouter } from 'vue-router'; const poemStore = usePoemStore(); const router = useRouter(); const goBack = () => { router.back(); }; const handleCollect = () => { if (poemStore.currentPoem) { poemStore.toggleCollect(poemStore.currentPoem.id); } }; </script>
- 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
-
诗词 / 成语接龙:云开发让匹配更流畅
接龙功能需要实时匹配用户输入的诗句 / 成语,还要校验是否正确、有没有重复。一开始用本地数据库,结果数据量太大(收录了 2 万 + 古诗、1 万 + 成语),App 启动变慢。export interface Poem { id: number; title: string; // 诗名 author: string; // 作者(含朝代) content: string[]; // 诗句数组 category: string; // 分类(如山水、边塞) isCollected: boolean; // 是否收藏 }- 优化:改用uniCloud云开发,把诗词库、成语库存到云端,用云函数做匹配校验(比如输入 “举头望明月”,云函数自动查询以 “月” 开头的诗句),App 端只负责展示和输入,启动速度从 3 秒降到 1 秒,接龙延迟也几乎感知不到。
-
主题颜色:贴合古风的 “颜值小心思”
考虑到用户可能在不同场景使用(比如晚上背古诗要护眼),做了 3 种古风主题:黛青(默认)、朱砂(暖调)、月白(护眼)。- 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
import { defineStore } from 'pinia'; import { Poem } from '@/types/poem'; const mockPoems: Poem[] = [ { id: 1, title: '望庐山瀑布', author: '唐·李白', content: ['日照香炉生紫烟', '遥看瀑布挂前川', '飞流直下三千尺', '疑是银河落九天'], category: '山水', isCollected: false }, { id: 2, title: '静夜思', author: '唐·李白', content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'], category: '思乡', isCollected: false }, { id: 3, title: '春晓', author: '唐·孟浩然', content: ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少'], category: '春景', isCollected: false } ]; export const usePoemStore = defineStore('poem', { state: () => ({ poems: mockPoems as Poem[], // 古诗列表 currentPoem: null as Poem | null, // 当前选中古诗 showFullContent: false // 详情页是否显示完整诗句(背诵模式控制) }), actions: { // 选中古诗 selectPoem(poemId: number) { this.currentPoem = this.poems.find(poem => poem.id === poemId) || null; this.showFullContent = false; // 进入背诵模式,默认隐藏完整诗句 }, // 切换收藏状态 toggleCollect(poemId: number) { const poem = this.poems.find(poem => poem.id === poemId); if (poem) poem.isCollected = !poem.isCollected; }, // 切换完整诗句显示(背诵/查看模式) toggleFullContent() { this.showFullContent = !this.showFullContent; } } });
- 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
五、✅ 后续功能想法 ✅
- “想要更多主题颜色”→ 新增 “竹绿”“藤黄” 2 种配色;
- “朗读广场想给作品点赞”→ 加了点赞功能,用uniCloud存储互动数据;
- “背古诗想有奖励”→ 做了 “背诵勋章”,集齐 5 个勋章能解锁古诗插画壁纸。
虽然还未成功上架市场,但在开发中寻找用户体验过程中,最让我感动的是一条评论:“我家娃以前不爱背古诗,现在每天打卡接龙,还会给同学分享自己的朗读作品,谢谢开发者让传统文化变有趣~” 这种时候,觉得熬夜改 bug、反复适配都值了!
六、🌟星光不负:技术与文化的双向奔赴🌟
从一开始的 “想做个古诗工具”,到现在的 “让自己更了解古诗并爱上它”,用 UniAPP 开发鸿蒙 App 的这 3 个月,不仅让我摸清了跨平台开发的套路,更懂了 “技术是为需求服务”—— 不是堆功能,而是让应用真正有用起来、用得爽。
回头看,选择 UniAPP 是最正确的决定:它让我不用纠结原生语法,能把更多精力放在 “怎么让古诗更有趣” 上;而鸿蒙的开放能力(云开发、云调试、HarmonyOS SDK、云测试)等,又让 App 的体验更上一层楼。就像古诗里说的 “行则将至”,技术之路没有捷径,但只要朝着目标一步步走,星光总会照亮前路。
接下来,我还想加 AR 功能(用鸿蒙的 AR SDK,扫描实景生成古诗意境)、古诗合唱(朗读广场支持多人合拍),让传统文化通过技术 “活” 起来、“火” 起来。如果你也在做 UniAPP + 鸿蒙开发,或者对古诗 App 有更多想法,欢迎一起交流~ 毕竟,码向未来的路上,有人同行更精彩!
一款无广告重拾儿时趣味的古诗 App 的开发过程记录
一、🌈缘起:让古诗 “活” 起来,而非躺在角落🌈
做这款 App 的初衷,藏在三个真实需求里:
一次偶然的寻找碰上鸿蒙的星光闪烁:翻找学生时代的语文诗词,想重拾那份诗词氛围时,书本早已不见踪影,此时便想在手机上寻找这类APP,市面古诗 App 要么广告扎堆,要么功能枯燥,实在感受不到当年的那种氛围;
身边朋友想重拾古诗,却缺合适的背诵工具,也没有专属交流渠道(总不能天天在朋友圈发古诗);
古诗的意境与字词之美,不该只靠 “死记硬背”—— 结合互动、朗读、释义,让大家 “懂了再背”,才是真正的文化传承。
所以核心需求很明确:做一款 “不枯燥、能互动、够纯粹” 的古诗工具。既要满足学习需求,又要兼顾趣味性,还得适配鸿蒙系统(毕竟鸿蒙用户越来越多,生态也日趋完善)。
二、💡技术选型:为啥 “锁死” UniAPP?💡
选技术栈时纠结了 3 天,最终敲定 UniAPP,全靠 “实用主义”:
排除鸿蒙原生开发:适配鸿蒙虽丝滑,但单人开发要重新学原生语法,还无法兼顾其他平台,效率太低;
排除其他跨平台框架:要么对鸿蒙适配不完善,要么插件生态薄弱,像诗词朗读、音频上传等功能得手动造轮子;
选择 UniAPP 的核心原因:
跨平台省心:一套代码可打包鸿蒙 App,后续扩展安卓、iOS 版本无需重构,对个人开发者极度友好;
- 鸿蒙适配 “自带 buff”:HBuilderX 提供现成打包模板和专属适配插件,不用手动修改大量配置;
- 生态够全:uniCloud 云开发、音频组件、数据库插件,刚好满足古诗存储、朗读上传、接龙匹配等核心需求;
- 学习成本低:有微信小程序开发基础,UniAPP 的 Vue 语法无缝衔接,1 周就能上手。
简单说:用 UniAPP 开发鸿蒙,就像 “站在巨人肩膀上”—— 不用从零学鸿蒙原生,又能享受鸿蒙系统特性,对非专业开发者太友好了!
三、⚠️踩坑实录:鸿蒙适配那些 “磨人的小妖精”⚠️
开发前以为 “跨平台 = 一路顺畅”,结果真正适配鸿蒙时,还是踩了不少坑,分享 3 个最印象深刻的:
权限申请:鸿蒙的 “规矩” 和其他平台不一样
做朗读广场时,需要调用手机麦克风录音、存储音频文件。一开始直接复用了其他平台的权限代码,结果在鸿蒙上直接报错 —— 录音功能打不开!
- 坑点:鸿蒙的 “媒体权限” 需要单独申请,而且必须配置ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA,还得在代码里动态弹窗说明 “为啥要要权限”(比如 “需要录音才能上传你的朗读作品”),光说 “需要权限” 会被系统拒绝;而且必须在鸿蒙的AGC后台进行同步申请该权限,因为该权限属于受限权限;而且!!!如果是使用的鸿蒙自家的隐私托管服务,必须要在隐私托管服务中同步该权限的声明
- 解决:使用uts插件配置权限;然后在AGC后台同步申请该受限权限通过率直接 100%。
// #ifdef APP-HARMONY import "@/uni_modules/harmony-permissions" // #endif
{
"module": {
"name": "uni_modules__harmony_permissions",
"type": "har",
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:media_desc",
"usedScene": {"when": "inuse"}
},{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:media_desc",
"usedScene": {"when": "inuse"}
}
]
}
}
服务卡片适配:桌面卡片让古诗 “触手可及”
鸿蒙的桌面卡片是个大亮点,我想做个 “每日一句” 卡片,用户不用打开 App,桌面就能看到古诗。
- 坑点:UniAPP 打包鸿蒙应用时,卡片的尺寸适配很麻烦,比如 3x2、2x2 的卡片布局会错乱,而且卡片数据刷新需要和主 App 同步;
- 解决:用 UniAPP 提供的uni-app x编译模式(专门优化鸿蒙适配),卡片布局用flex弹性盒,固定宽高比;数据同步用uniCloud实时数据库,主 App 更新 “每日一句” 后,卡片自动刷新,不用用户手动操作 —— 现在不少用户说 “每天解锁手机先看一眼古诗,感觉很治愈”。
音频播放:后台播放老是 “断档”
朗读广场的核心是 “听别人读古诗”,但一开始用户反馈 “退到后台就停了”,体验很差。
- 坑点:鸿蒙对后台音频播放有严格限制,普通的audio组件在后台会被系统暂停;
- 解决:集成了鸿蒙的AVPlayer SDK(UniAPP 可以直接调用鸿蒙原生 SDK),在manifest.json里配置 “后台音频权限”,同时用 UniAPP 的onBackground生命周期监听,退后台时自动切换到鸿蒙原生播放器,现在后台播放能稳定运行,用户还能边听古诗边刷微信~
四、📦功能落地:每个模块都是 “心头好”📦
结合古诗的核心需求,几个功能模块都是 “边踩坑边优化” 出来的,挑 3 个重点说:
-
背一背:用 “艾宾浩斯曲线” 让背诵不费脑
一开始只是简单做了 “列表 + 背诵打卡”,但用户反馈 “背了就忘”。后来用 UniAPP 的storage存储用户背诵记录,结合艾宾浩斯遗忘曲线,在对应时间点推送提醒(比如第 1 天、第 3 天、第 7 天),还加了 “遮字默写” 功能 —— 点击诗句能隐藏关键字,像考试一样检验效果。- 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
<view class="poem-content"> <block v-if="poemStore.showFullContent"> <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="full-line">{{ line }}</text> </block> <block v-else> <text v-for="(line, idx) in poemStore.currentPoem.content" :key="idx" class="hide-line">□□□□□□□</text> </block> </view> <button class="toggle-btn" @click="poemStore.toggleFullContent"> {{ poemStore.showFullContent ? '切换到背诵模式' : '查看完整诗句' }} </button><script setup lang="ts"> import { usePoemStore } from '@/stores/poemStore'; import { useRouter } from 'vue-router'; const poemStore = usePoemStore(); const router = useRouter(); const goBack = () => { router.back(); }; const handleCollect = () => { if (poemStore.currentPoem) { poemStore.toggleCollect(poemStore.currentPoem.id); } }; </script>
- 小技巧:用 UniAPP 的uni.createPushMessage调用鸿蒙的通知权限,提醒文案特意用古诗意境,比如 “床前明月光,今天该复习啦~”,比干巴巴的 “请背诵” 更讨喜。
-
诗词 / 成语接龙:云开发让匹配更流畅
接龙功能需要实时匹配用户输入的诗句 / 成语,还要校验是否正确、有没有重复。一开始用本地数据库,结果数据量太大(收录了 2 万 + 古诗、1 万 + 成语),App 启动变慢。export interface Poem { id: number; title: string; // 诗名 author: string; // 作者(含朝代) content: string[]; // 诗句数组 category: string; // 分类(如山水、边塞) isCollected: boolean; // 是否收藏 }- 优化:改用uniCloud云开发,把诗词库、成语库存到云端,用云函数做匹配校验(比如输入 “举头望明月”,云函数自动查询以 “月” 开头的诗句),App 端只负责展示和输入,启动速度从 3 秒降到 1 秒,接龙延迟也几乎感知不到。
-
主题颜色:贴合古风的 “颜值小心思”
考虑到用户可能在不同场景使用(比如晚上背古诗要护眼),做了 3 种古风主题:黛青(默认)、朱砂(暖调)、月白(护眼)。- 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
import { defineStore } from 'pinia'; import { Poem } from '@/types/poem'; const mockPoems: Poem[] = [ { id: 1, title: '望庐山瀑布', author: '唐·李白', content: ['日照香炉生紫烟', '遥看瀑布挂前川', '飞流直下三千尺', '疑是银河落九天'], category: '山水', isCollected: false }, { id: 2, title: '静夜思', author: '唐·李白', content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'], category: '思乡', isCollected: false }, { id: 3, title: '春晓', author: '唐·孟浩然', content: ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少'], category: '春景', isCollected: false } ]; export const usePoemStore = defineStore('poem', { state: () => ({ poems: mockPoems as Poem[], // 古诗列表 currentPoem: null as Poem | null, // 当前选中古诗 showFullContent: false // 详情页是否显示完整诗句(背诵模式控制) }), actions: { // 选中古诗 selectPoem(poemId: number) { this.currentPoem = this.poems.find(poem => poem.id === poemId) || null; this.showFullContent = false; // 进入背诵模式,默认隐藏完整诗句 }, // 切换收藏状态 toggleCollect(poemId: number) { const poem = this.poems.find(poem => poem.id === poemId); if (poem) poem.isCollected = !poem.isCollected; }, // 切换完整诗句显示(背诵/查看模式) toggleFullContent() { this.showFullContent = !this.showFullContent; } } });
- 实现:用 UniAPP 的vuex管理主题状态,结合鸿蒙的 “系统深色模式”,用户切换系统主题时,App 自动适配对应的古风配色 —— 比如系统开深色模式,App 自动切月白主题,字体加粗,保护视力。
五、✅ 后续功能想法 ✅
- “想要更多主题颜色”→ 新增 “竹绿”“藤黄” 2 种配色;
- “朗读广场想给作品点赞”→ 加了点赞功能,用uniCloud存储互动数据;
- “背古诗想有奖励”→ 做了 “背诵勋章”,集齐 5 个勋章能解锁古诗插画壁纸。
虽然还未成功上架市场,但在开发中寻找用户体验过程中,最让我感动的是一条评论:“我家娃以前不爱背古诗,现在每天打卡接龙,还会给同学分享自己的朗读作品,谢谢开发者让传统文化变有趣~” 这种时候,觉得熬夜改 bug、反复适配都值了!
六、🌟星光不负:技术与文化的双向奔赴🌟
从一开始的 “想做个古诗工具”,到现在的 “让自己更了解古诗并爱上它”,用 UniAPP 开发鸿蒙 App 的这 3 个月,不仅让我摸清了跨平台开发的套路,更懂了 “技术是为需求服务”—— 不是堆功能,而是让应用真正有用起来、用得爽。
回头看,选择 UniAPP 是最正确的决定:它让我不用纠结原生语法,能把更多精力放在 “怎么让古诗更有趣” 上;而鸿蒙的开放能力(云开发、云调试、HarmonyOS SDK、云测试)等,又让 App 的体验更上一层楼。就像古诗里说的 “行则将至”,技术之路没有捷径,但只要朝着目标一步步走,星光总会照亮前路。
接下来,我还想加 AR 功能(用鸿蒙的 AR SDK,扫描实景生成古诗意境)、古诗合唱(朗读广场支持多人合拍),让传统文化通过技术 “活” 起来、“火” 起来。如果你也在做 UniAPP + 鸿蒙开发,或者对古诗 App 有更多想法,欢迎一起交流~ 毕竟,码向未来的路上,有人同行更精彩!
【鸿蒙征文】鸿蒙开发踩坑日记:一位前端开发者的血泪成长史
日记一:环境搭建——“Hello, World!” 前的下马威
坑点: 模拟器启动失败,报错 HAXM is not installed或 VT-x is not available。
踩坑过程:
信心满满地安装完 DevEco Studio,创建第一个 Hello World 项目,点击运行模拟器。结果,控制台报出一串红色错误,模拟器屏幕一片漆黑。心里“咯噔”一下,难道第一步就卡住了?
排查与解决:
- 检查BIOS: 重启电脑,狂按 F2/Del 键进入 BIOS 设置。找到
Intel Virtualization Technology(或 AMD 的SVM Mode)选项,确保其状态为 Enabled。这是最根本的原因。 - 开启Windows功能: 在 Windows 搜索栏输入“启用或关闭 Windows 功能”,确保 Hyper-V 和 Windows 虚拟机监控平台 已被勾选。完成后需要重启电脑。
- 选择正确的模拟器: 在 DevEco Studio 的设备管理器中,确保下载的是
API 9或更高版本的模拟器。低版本 API 的模拟器可能存在兼容性问题。
心得: 鸿蒙开发环境的搭建,第一步就是和硬件虚拟化打交道。这提醒我,移动开发生态已与 Web 开发那种“开箱即用”的体验截然不同。
日记二:UTS 类型系统 —— “像”TypeScript,但不是 TypeScript
坑点一:Map.forEach的消失
代码:
let myMap = new Map<string, number>();
myMap.set('apple', 5);
// 在 iOS 上运行良好,在鸿蒙上报错!
myMap.forEach((value, key) => {
console.log(key, value);
});
错误: TypeError: undefined is not callable
解决方案:
// 方案1:使用 for...of 循环
for (let [key, value] of myMap.entries()) {
console.log(key, value);
}
// 方案2:更保险的做法,先判断平台或方法是否存在
if (myMap.forEach) {
myMap.forEach((value, key) => { /* ... */ });
} else {
for (let [key, value] of myMap.entries()) { /* ... */ }
}
坑点二:隐式类型转换的终结
代码:
let data: string | null = getDataFromAPI();
// Web 中常见的真值判断,在 UTS 中报错!
if (data) {
processData(data);
}
错误: The condition expression must be of boolean type.
解决方案:
// 必须进行显式的布尔判断
if (data != null) {
processData(data);
}
// 或者判断字符串长度
if (data?.length > 0) {
processData(data;
}
心得: UTS 的强类型特性像一位严格的老师,逼着我写出更严谨、更少歧义的代码。虽然初期不适应,但从代码质量角度看,是件好事。
日记三:页面布局与样式 —— CSS 的“阉割版”体验
坑点一:100vh的陷阱与滚动失效
代码:
<template>
<view class="container">
<scroll-view scroll-y class="scroll-area">
<!-- 长内容 -->
</scroll-view>
<view class="fixed-bottom">我是一个底部固定栏</view>
</view>
</template>
<style>
.container {
height: 100vh; /* 鸿蒙不支持 vh! */
}
.scroll-area {
height: 100%; /* 继承自一个高度无效的容器,滚动失效 */
}
</style>
现象: 页面无法滚动,scroll-view区域高度为 0。
解决方案:
<template>
<!-- 关键:让 scroll-view 作为根节点或充满整个页面 -->
<scroll-view scroll-y class="page-root">
<!-- 所有内容,包括原本想固定的元素,都放在里面 -->
<view class="content">可滚动内容</view>
<view class="bottom-placeholder"></view>
<view class="fixed-bottom">利用 absolute 或 fixed 定位模拟固定</view>
</scroll-view>
</template>
<style>
.page-root {
width: 100%;
height: 100%; /* 使用 100% 而不是 100vh */
position: relative; /* 为固定定位元素提供参考 */
}
.fixed-bottom {
position: absolute;
bottom: 0;
width: 100%;
}
.bottom-placeholder {
height: 100rpx; /* 为固定底部留出占位空间,避免内容被遮挡 */
}
坑点二:Flex 布局的微妙差异
在 Web 中,display: flex的容器默认是 flex-direction: row。在鸿蒙的 text组件嵌套 span时,我发现 Flex 布局的表现有时与 Web 有细微差别,导致文字排版错乱。
解决方案: 尽量显式地写明 flex-direction,避免依赖默认值。对于复杂布局,多使用 align-items和 justify-content进行微调。
心得: 必须彻底抛弃 Web 的“文档流”思维,拥抱原生应用的“盒子模型”和明确的滚动容器概念。布局要更“笨拙”但更精确。
日记四:数据库操作 —— 最诡异的“数据隐身术”
坑点:查询结果对象“看起来有数据,却取不出来”
代码:
// 执行查询
const result = db.query(‘SELECT name, count FROM ingredients’);
const rows = result.maps;
console.log(‘结果行数:’, rows.length); // 输出: 结果行数: 5
console.log(‘第一行:’, JSON.stringify(rows[0])); // 输出: 第一行: {}
// 以下代码在 iOS 上正常,在鸿蒙上为 undefined
const name = rows[0][‘name’];
现象: 数据“幽灵”,日志显示有数据,但代码无法访问。
根源: 鸿蒙平台返回的 rows不是普通对象数组,而是 Map<string, any>[]! JSON.stringify对 Map 序列化会得到空对象 {}。
解决方案:
// 封装一个强大的兼容性取值函数
function getRowValue(row: any, key: string, defaultValue: any = null): any {
if (row == null) return defaultValue;
// 方法1:判断是否是 Map
if (row instanceof Map) {
return row.get(key) ?? defaultValue;
}
// 方法2:判断是否拥有 get 方法 (更通用)
else if (typeof row.get === ‘function’) {
return row.get(key) ?? defaultValue;
}
// 方法3:默认为普通对象
else {
return row[key] ?? defaultValue;
}
}
// 使用
const name = getRowValue(rows[0], ‘name’);
const count = getRowValue(rows[0], ‘count’, 0);
心得: 跨平台开发中,“数据序列化/反序列化”是头号隐形杀手。不能相信表面现象,必须对关键数据的实际类型进行运行时判断。
日记五:网络请求与生命周期 —— 异步世界的时序陷阱
坑点:onLoad中发起请求,在 onReady中访问数据却为 null
代码:
export default {
data() {
return {
recipeData: null
}
},
onLoad() {
uni.request({
url: ‘/api/recipe’,
success: (res) => {
this.recipeData = res.data; // 异步赋值
}
});
},
onReady() {
// 企图使用数据渲染视图,但 recipeData 很可能还是 null!
this.renderChart(this.recipeData); // 报错!
}
}
解决方案:
-
使用状态管理: 引入 Pinia,在请求成功的回调中提交 mutation 改变状态,组件通过 computed 属性响应式地获取数据。
-
条件渲染: 在模板中根据数据是否存在进行判断。
<template> <view> <chart v-if=“recipeData” :data=“recipeData”></chart> <loading v-else>加载中…</loading> </view> </template> -
使用 Promise/async-await: 确保在数据准备好后再执行后续操作。
async onLoad() { try { this.recipeData = await this.fetchRecipeData(); // 数据获取成功后,再执行需要数据的操作 this.$nextTick(() => { this.renderChart(this.recipeData); }); } catch (error) { console.error(‘数据加载失败:’, error); } }
心得: 生命周期钩子和异步操作的配合,是前端开发永恒的课题。在鸿蒙这种更接近原生的环境中,对时序的控制要求更为严格。
总结:我的踩坑生存法则
- 假设一切都有平台差异: 对任何 API、组件、样式属性,都抱有一丝怀疑,优先查阅鸿蒙专属文档。
- 日志是最好(有时是唯一)的调试工具: 不仅要打印值,还要打印类型(
typeof,instanceof)。 - 封装兼容层: 将平台差异(如数据库取值、网络库)封装成统一的工具函数,是长期项目的必备架构。
- 小步快跑,频繁测试: 每实现一个小功能,就在鸿蒙模拟器或真机上跑一遍,避免错误累积。
- 拥抱社区: uni-app 的官方论坛、钉钉群和 GitHub issues 是解决问题的宝库,你踩的坑,大概率已经有人踩过并分享了解决方案。
踩坑虽苦,但每解决一个坑,对鸿蒙平台和跨平台开发的理解就加深一层。现在回头看,这些坑都成了我宝贵的经验财富。希望这份日记,能为你照亮前行的路,助你少走弯路!
日记一:环境搭建——“Hello, World!” 前的下马威
坑点: 模拟器启动失败,报错 HAXM is not installed或 VT-x is not available。
踩坑过程:
信心满满地安装完 DevEco Studio,创建第一个 Hello World 项目,点击运行模拟器。结果,控制台报出一串红色错误,模拟器屏幕一片漆黑。心里“咯噔”一下,难道第一步就卡住了?
排查与解决:
- 检查BIOS: 重启电脑,狂按 F2/Del 键进入 BIOS 设置。找到
Intel Virtualization Technology(或 AMD 的SVM Mode)选项,确保其状态为 Enabled。这是最根本的原因。 - 开启Windows功能: 在 Windows 搜索栏输入“启用或关闭 Windows 功能”,确保 Hyper-V 和 Windows 虚拟机监控平台 已被勾选。完成后需要重启电脑。
- 选择正确的模拟器: 在 DevEco Studio 的设备管理器中,确保下载的是
API 9或更高版本的模拟器。低版本 API 的模拟器可能存在兼容性问题。
心得: 鸿蒙开发环境的搭建,第一步就是和硬件虚拟化打交道。这提醒我,移动开发生态已与 Web 开发那种“开箱即用”的体验截然不同。
日记二:UTS 类型系统 —— “像”TypeScript,但不是 TypeScript
坑点一:Map.forEach的消失
代码:
let myMap = new Map<string, number>();
myMap.set('apple', 5);
// 在 iOS 上运行良好,在鸿蒙上报错!
myMap.forEach((value, key) => {
console.log(key, value);
});
错误: TypeError: undefined is not callable
解决方案:
// 方案1:使用 for...of 循环
for (let [key, value] of myMap.entries()) {
console.log(key, value);
}
// 方案2:更保险的做法,先判断平台或方法是否存在
if (myMap.forEach) {
myMap.forEach((value, key) => { /* ... */ });
} else {
for (let [key, value] of myMap.entries()) { /* ... */ }
}
坑点二:隐式类型转换的终结
代码:
let data: string | null = getDataFromAPI();
// Web 中常见的真值判断,在 UTS 中报错!
if (data) {
processData(data);
}
错误: The condition expression must be of boolean type.
解决方案:
// 必须进行显式的布尔判断
if (data != null) {
processData(data);
}
// 或者判断字符串长度
if (data?.length > 0) {
processData(data;
}
心得: UTS 的强类型特性像一位严格的老师,逼着我写出更严谨、更少歧义的代码。虽然初期不适应,但从代码质量角度看,是件好事。
日记三:页面布局与样式 —— CSS 的“阉割版”体验
坑点一:100vh的陷阱与滚动失效
代码:
<template>
<view class="container">
<scroll-view scroll-y class="scroll-area">
<!-- 长内容 -->
</scroll-view>
<view class="fixed-bottom">我是一个底部固定栏</view>
</view>
</template>
<style>
.container {
height: 100vh; /* 鸿蒙不支持 vh! */
}
.scroll-area {
height: 100%; /* 继承自一个高度无效的容器,滚动失效 */
}
</style>
现象: 页面无法滚动,scroll-view区域高度为 0。
解决方案:
<template>
<!-- 关键:让 scroll-view 作为根节点或充满整个页面 -->
<scroll-view scroll-y class="page-root">
<!-- 所有内容,包括原本想固定的元素,都放在里面 -->
<view class="content">可滚动内容</view>
<view class="bottom-placeholder"></view>
<view class="fixed-bottom">利用 absolute 或 fixed 定位模拟固定</view>
</scroll-view>
</template>
<style>
.page-root {
width: 100%;
height: 100%; /* 使用 100% 而不是 100vh */
position: relative; /* 为固定定位元素提供参考 */
}
.fixed-bottom {
position: absolute;
bottom: 0;
width: 100%;
}
.bottom-placeholder {
height: 100rpx; /* 为固定底部留出占位空间,避免内容被遮挡 */
}
坑点二:Flex 布局的微妙差异
在 Web 中,display: flex的容器默认是 flex-direction: row。在鸿蒙的 text组件嵌套 span时,我发现 Flex 布局的表现有时与 Web 有细微差别,导致文字排版错乱。
解决方案: 尽量显式地写明 flex-direction,避免依赖默认值。对于复杂布局,多使用 align-items和 justify-content进行微调。
心得: 必须彻底抛弃 Web 的“文档流”思维,拥抱原生应用的“盒子模型”和明确的滚动容器概念。布局要更“笨拙”但更精确。
日记四:数据库操作 —— 最诡异的“数据隐身术”
坑点:查询结果对象“看起来有数据,却取不出来”
代码:
// 执行查询
const result = db.query(‘SELECT name, count FROM ingredients’);
const rows = result.maps;
console.log(‘结果行数:’, rows.length); // 输出: 结果行数: 5
console.log(‘第一行:’, JSON.stringify(rows[0])); // 输出: 第一行: {}
// 以下代码在 iOS 上正常,在鸿蒙上为 undefined
const name = rows[0][‘name’];
现象: 数据“幽灵”,日志显示有数据,但代码无法访问。
根源: 鸿蒙平台返回的 rows不是普通对象数组,而是 Map<string, any>[]! JSON.stringify对 Map 序列化会得到空对象 {}。
解决方案:
// 封装一个强大的兼容性取值函数
function getRowValue(row: any, key: string, defaultValue: any = null): any {
if (row == null) return defaultValue;
// 方法1:判断是否是 Map
if (row instanceof Map) {
return row.get(key) ?? defaultValue;
}
// 方法2:判断是否拥有 get 方法 (更通用)
else if (typeof row.get === ‘function’) {
return row.get(key) ?? defaultValue;
}
// 方法3:默认为普通对象
else {
return row[key] ?? defaultValue;
}
}
// 使用
const name = getRowValue(rows[0], ‘name’);
const count = getRowValue(rows[0], ‘count’, 0);
心得: 跨平台开发中,“数据序列化/反序列化”是头号隐形杀手。不能相信表面现象,必须对关键数据的实际类型进行运行时判断。
日记五:网络请求与生命周期 —— 异步世界的时序陷阱
坑点:onLoad中发起请求,在 onReady中访问数据却为 null
代码:
export default {
data() {
return {
recipeData: null
}
},
onLoad() {
uni.request({
url: ‘/api/recipe’,
success: (res) => {
this.recipeData = res.data; // 异步赋值
}
});
},
onReady() {
// 企图使用数据渲染视图,但 recipeData 很可能还是 null!
this.renderChart(this.recipeData); // 报错!
}
}
解决方案:
-
使用状态管理: 引入 Pinia,在请求成功的回调中提交 mutation 改变状态,组件通过 computed 属性响应式地获取数据。
-
条件渲染: 在模板中根据数据是否存在进行判断。
<template> <view> <chart v-if=“recipeData” :data=“recipeData”></chart> <loading v-else>加载中…</loading> </view> </template> -
使用 Promise/async-await: 确保在数据准备好后再执行后续操作。
async onLoad() { try { this.recipeData = await this.fetchRecipeData(); // 数据获取成功后,再执行需要数据的操作 this.$nextTick(() => { this.renderChart(this.recipeData); }); } catch (error) { console.error(‘数据加载失败:’, error); } }
心得: 生命周期钩子和异步操作的配合,是前端开发永恒的课题。在鸿蒙这种更接近原生的环境中,对时序的控制要求更为严格。
总结:我的踩坑生存法则
- 假设一切都有平台差异: 对任何 API、组件、样式属性,都抱有一丝怀疑,优先查阅鸿蒙专属文档。
- 日志是最好(有时是唯一)的调试工具: 不仅要打印值,还要打印类型(
typeof,instanceof)。 - 封装兼容层: 将平台差异(如数据库取值、网络库)封装成统一的工具函数,是长期项目的必备架构。
- 小步快跑,频繁测试: 每实现一个小功能,就在鸿蒙模拟器或真机上跑一遍,避免错误累积。
- 拥抱社区: uni-app 的官方论坛、钉钉群和 GitHub issues 是解决问题的宝库,你踩的坑,大概率已经有人踩过并分享了解决方案。
踩坑虽苦,但每解决一个坑,对鸿蒙平台和跨平台开发的理解就加深一层。现在回头看,这些坑都成了我宝贵的经验财富。希望这份日记,能为你照亮前行的路,助你少走弯路!
收起阅读 »【鸿蒙征文】uni-app 打通鸿蒙踩坑日记:从迷茫到上架的完整心路历程
缘起:为什么选择鸿蒙
作为一名前端开发者,我一直在关注各种新兴的技术生态。当华为宣布鸿蒙系统将独立发展时,我就意识到这可能是下一个重要的技术风口。但真正促使我动手的,还是实际的需求痛点。
我们团队有一个成熟的 uni-app 项目,已经在 iOS 和 Android 上稳定运行了两年。但随着华为设备市场份额的不断扩大,越来越多的用户反馈希望有鸿蒙版本。看着应用商店里竞品陆续上线鸿蒙版本,我决定亲自趟一趟这趟"浑水"。
从今年 3 月开始,我花了近三个月时间,完成了从技术调研到实际上架的完整过程。今天就来分享这段充满挑战又收获满满的经历。
技术选型:为什么坚持 uni-app
在开始之前,团队内部有过激烈的讨论:是重新开发原生鸿蒙应用,还是基于现有 uni-app 项目进行适配?
重新开发原生鸿蒙的优势:
- 性能最优,体验最佳
- 可以充分利用鸿蒙的特有能力
- 技术栈更纯粹
但最终我们还是选择了 uni-app 适配,原因很现实:
- 开发成本:团队熟悉 Vue 技术栈,重新学习 ArkTS 成本太高
- 维护成本:三套代码意味着三倍的维护工作量
- 迭代速度:业务需求变化快,需要快速响应
- DCloud 的承诺:官方明确表示会持续加强鸿蒙支持
事后证明,这个选择是正确的,但过程远比想象中曲折。
环境搭建:第一个坑就栽了跟头
按照官方文档,我信心满满地开始环境搭建。HBuilderX、DevEco Studio、Node.js,一切看起来都很简单。
第一个坑:模拟器选择
文档说"下载 API 19 模拟器即可",但我下载后始终无法启动。各种错误提示看得我头皮发麻:
模拟器启动失败:硬件加速未开启
VT-x 不可用,请检查 BIOS 设置
我花了整整两天时间:
- 重启电脑进入 BIOS 开启虚拟化
- 在 Windows 功能中启用 Hyper-V
- 更新显卡驱动
- 重装 DevEco Studio
最后发现是杀毒软件冲突。关闭某数字卫士后,模拟器终于正常启动了。
经验教训:环境问题不要死磕,及时换思路。后来我直接使用真机调试,效率反而更高。
项目迁移:看似顺利的开始
将现有 uni-app 项目迁移到鸿蒙,最初进展出乎意料的顺利。
基础页面适配
大部分 Vue 页面无需修改即可运行,uni-app 的跨端能力确实令人印象深刻:
<template>
<view class="container">
<text class="title">{{ title }}</text>
<button @click="handleClick">点击我</button>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello HarmonyOS'
}
},
methods: {
handleClick() {
uni.showToast({
title: '点击成功'
})
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 18px;
color: #333;
}
</style>
路由配置
pages.json的配置也完全兼容:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页"
}
}
]
}
静态资源
static目录下的图片、字体等资源都能正常加载。前 80% 的工作在两天内就完成了,我甚至觉得鸿蒙开发不过如此。
但接下来的 20%,花了我 80% 的时间。
深水区:平台特定适配
导航栏差异
第一个平台差异出现在导航栏。在 iOS 和 Android 上正常的导航栏,在鸿蒙上出现了错位。
问题现象:
- 标题位置偏移
- 返回按钮点击区域异常
- 状态栏颜色不匹配
解决方案:
在 pages.json中为鸿蒙平台单独配置:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"app-plus": {
"titleNView": {
// iOS/Android 配置
}
},
"harmony": {
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "首页",
"navigationStyle": "default"
}
}
}
]
}
组件兼容性问题
更大的挑战来自组件兼容性。我们项目中使用了大量第三方组件,有些在鸿蒙上表现异常。
案例:图片裁剪组件
在 iOS/Android 上正常的图片裁剪组件,在鸿蒙上出现以下问题:
- 触摸事件响应错乱
- 裁剪框位置计算错误
- 性能卡顿明显
排查过程:
- 首先怀疑是触摸事件机制差异
- 尝试修改事件处理逻辑,效果有限
- 深入组件源码,发现使用了 DOM API
- 鸿蒙不支持部分 DOM API
最终方案:
寻找替代组件,选择明确支持鸿蒙的图片裁剪库。这个过程让我意识到:不是所有 web 生态都能无缝迁移到鸿蒙。
API 兼容性
uni-app 的 API 在鸿蒙上的支持程度不一:
完全兼容的:
uni.showToastuni.requestuni.getStorageuni.chooseImage
需要适配的:
uni.getSystemInfo返回的信息结构不同uni.onKeyboardHeightChange在鸿蒙上触发机制不同- 部分设备 API 需要鸿蒙特定实现
不支持的:
- 某些平台特定的扩展 API
我们建立了兼容性检查清单,对每个 API 进行测试:
// 兼容性封装示例
export const getSafeArea = () => {
return new Promise((resolve, reject) => {
uni.getSystemInfo({
success: (res) => {
// 鸿蒙平台特殊处理
if (res.platform === 'harmony') {
resolve({
top: res.statusBarHeight || 0,
bottom: 0,
left: 0,
right: 0
})
} else {
resolve(res.safeArea)
}
},
fail: reject
})
})
}
性能优化:从卡顿到流畅
首次在真机上运行应用时,性能问题让我震惊:列表滚动卡顿、页面切换白屏、内存占用过高。
列表性能优化
问题:商品列表页有 1000+ 项目,滚动时严重卡顿。
分析:
- 鸿蒙的列表渲染机制与 web 不同
- 大量 DOM 节点导致内存压力
- 图片加载没有做优化
解决方案:
- 虚拟列表:
<template>
<view class="list-container">
<unicloud-db
ref="udb"
collection="goods"
where="category_id == categoryId"
orderby="create_time desc"
:page-size="20"
@load="onListLoad"
v-slot:default="{data, loading, error}">
<virtual-list
:data="data"
:item-size="100"
key-field="_id">
<template v-slot:default="{item}">
<goods-item :item="item" />
</template>
</virtual-list>
</unicloud-db>
</view>
</template>
- 图片懒加载:
<image
:src="item.image"
mode="aspectFill"
lazy-load
:fade-show="false"
@load="onImageLoad"
@error="onImageError">
</image>
- 内存管理:
// 监听页面生命周期
onPageScroll(e) {
// 可视区域外的图片取消加载
this.checkVisibleImages()
},
onHide() {
// 页面隐藏时释放大资源
this.clearCache()
}
启动速度优化
问题:应用冷启动需要 3-5 秒,体验较差。
优化措施:
- 代码分包:
// pages.json
{
"subPackages": [
{
"root": "pagesA",
"pages": [
"page1/page1",
"page2/page2"
]
},
{
"root": "pagesB",
"pages": [
"page3/page3",
"page4/page4"
]
}
]
}
- 预加载关键资源:
// app.vue
onLaunch() {
// 预加载首屏必要数据
this.preloadEssentialData()
// 异步加载非关键资源
setTimeout(() => {
this.preloadSecondaryResources()
}, 1000)
}
- 减少同步操作:
// 优化前:同步阻塞
const userInfo = uni.getStorageSync('userInfo')
const settings = uni.getStorageSync('settings')
// 优化后:异步并行
Promise.all([
this.getStorage('userInfo'),
this.getStorage('settings')
]).then(([userInfo, settings]) => {
// 处理数据
})
经过优化后,启动时间缩短到 1-2 秒,列表滚动帧率稳定在 60fps。
打包发布:证书的"坑爹"之旅
如果说代码适配是技术挑战,那证书和打包就是流程挑战。
证书配置的坑
第一个坑:证书类型混淆
我一开始把调试证书当成发布证书使用,结果打包时各种错误:
错误: 证书类型不匹配
错误: 签名验证失败
解决方案:
- 调试证书:用于开发阶段真机调试
- 发布证书:用于应用商店上架
- 两者不能混用
第二个坑:证书有效期
发布证书有一年有效期,但测试证书只有三个月。我第一次提交审核时,证书差点过期。
经验:设置证书到期提醒,提前一个月更新。
打包流程优化
手动打包效率低下,我建立了自动化流程:
// package.json 脚本
{
"scripts": {
"build:harmony:debug": "uni build --platform harmony --mode development",
"build:harmony:release": "uni build --platform harmony --mode production",
"pack:harmony": "node scripts/pack-harmony.js",
"deploy:harmony": "npm run build:harmony:release && npm run pack:harmony"
}
}
// scripts/pack-harmony.js
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
console.log('🚀 开始鸿蒙应用打包...')
// 检查构建产物
const buildPath = path.join(__dirname, '../dist/build/harmony')
if (!fs.existsSync(buildPath)) {
console.error('❌ 构建产物不存在,请先执行构建')
process.exit(1)
}
// 执行鸿蒙特定打包逻辑
try {
execSync('node scripts/harmony-specific-pack.js', { stdio: 'inherit' })
console.log('✅ 鸿蒙应用打包完成')
} catch (error) {
console.error('❌ 打包失败:', error)
process.exit(1)
}
上架审核:意料之外的拒绝
第一次提交审核时,我信心满满,觉得经过充分测试的应用肯定能一次通过。结果被打脸了。
审核拒绝原因:
- 隐私政策不完整缺少数据存储说明未说明第三方 SDK 数据收集
- 权限说明不清晰相机权限用途描述过于简单位置权限必要性未充分说明
- 应用截图不规范截图包含状态栏时间"17:30"部分截图尺寸不一致
解决方案:
隐私政策完善:
## 数据存储说明
本应用使用本地存储保存用户偏好设置,使用华为云存储备份用户数据。所有数据均加密存储。
## 第三方 SDK 说明
- 华为账号 SDK:用于用户登录认证
- 华为推送 SDK:用于消息推送
- 微信分享 SDK:用于内容分享
权限说明重写:
## 相机权限
用途:用于扫描商品条形码、拍摄商品照片
必要性:核心功能需要,无法替代
数据处理:图片仅在本地处理,不上传服务器
## 位置权限
用途:用于推荐附近门店、配送地址定位
必要性:增值功能需要,非核心功能
数据处理:位置信息加密传输到服务器
截图规范:
- 使用模拟器统一截图
- 隐藏状态栏敏感信息
- 确保所有截图尺寸一致
- 展示核心功能流程
第二次提交后,顺利通过审核。
监控与反馈:上架只是开始
应用上架后,真正的挑战才开始。用户反馈和线上监控发现了我们测试中未发现的问题。
线上问题排查
案例:特定机型闪退
有用户反馈在华为 Mate 40 上频繁闪退,但我们测试机上正常。
排查过程:
- 查看 AGC 崩溃日志
- 发现内存溢出错误
- 定位到图片加载组件
- 特定机型内存管理更严格
解决方案:
// 图片加载优化
loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image()
// 限制图片尺寸
if (this.isLowMemoryDevice()) {
url = this.addImageSizeParams(url, '800x600')
}
img.onload = () => {
// 及时释放引用
img.onload = null
img.onerror = null
resolve(img)
}
img.onerror = reject
img.src = url
})
}
用户反馈处理
建立用户反馈响应机制:
- 24 小时内响应严重问题
- 每周汇总用户反馈
- 每月发布优化版本
经验总结与建议
技术层面
- 组件选择要谨慎优先选择官方组件验证第三方组件兼容性准备备用方案
- 性能优化要前置开始就考虑性能问题建立性能监控体系定期进行性能回归测试
流程层面
- 证书管理要规范区分调试和发布证书设置证书到期提醒备份证书文件
- 审核准备要充分仔细阅读审核指南准备完整的说明材料提前测试审核流程
- 监控体系要完善建立崩溃监控收集用户反馈定期分析使用数据
给后来者的建议
- 不要畏惧鸿蒙开发uni-app 已经提供了很好的支持大部分代码可以复用社区资源越来越丰富
- 从小功能开始尝试先移植简单页面逐步增加复杂度积累经验再处理核心功能
- 善用官方资源DCloud 文档很详细华为开发者社区活跃官方技术支持响应快
- 加入开发者社区学习他人经验分享自己的收获共同推动生态发展
未来规划
鸿蒙版本的成功上线,给了我们很大信心。下一步计划:
- 深度集成鸿蒙特性原子化服务探索分布式能力应用鸿蒙特有 UI 交互
- 性能持续优化启动速度优化到 1 秒内内存占用降低 30%功耗优化
- 多端协同与 iOS/Android 版本功能同步数据互通体验优化统一的设计语言
写在最后
三个月的鸿蒙适配之旅,让我深刻体会到:技术转型虽然痛苦,但收获远超预期。
最大的收获不是技术上架,而是思维转变:从"web 思维"到"多端思维",从"功能实现"到"体验优化"。
鸿蒙生态还在快速发展,现在入局正是好时机。uni-app 为跨端开发提供了很好的基础,让我们能够以较低成本探索新平台。
如果你也在考虑鸿蒙开发,我的建议是:不要观望,直接开始。从简单的 demo 开始,逐步深入,你会发现鸿蒙开发没有想象中那么难。
在这个过程中,你会遇到各种问题,但也会收获解决问题的成就感。更重要的是,你将成为新生态的早期参与者,这本身就是宝贵的机会。
希望这篇文章能帮助到正在或准备进行鸿蒙开发的你。如果遇到问题,欢迎交流讨论,我们一起推动鸿蒙生态的繁荣!
缘起:为什么选择鸿蒙
作为一名前端开发者,我一直在关注各种新兴的技术生态。当华为宣布鸿蒙系统将独立发展时,我就意识到这可能是下一个重要的技术风口。但真正促使我动手的,还是实际的需求痛点。
我们团队有一个成熟的 uni-app 项目,已经在 iOS 和 Android 上稳定运行了两年。但随着华为设备市场份额的不断扩大,越来越多的用户反馈希望有鸿蒙版本。看着应用商店里竞品陆续上线鸿蒙版本,我决定亲自趟一趟这趟"浑水"。
从今年 3 月开始,我花了近三个月时间,完成了从技术调研到实际上架的完整过程。今天就来分享这段充满挑战又收获满满的经历。
技术选型:为什么坚持 uni-app
在开始之前,团队内部有过激烈的讨论:是重新开发原生鸿蒙应用,还是基于现有 uni-app 项目进行适配?
重新开发原生鸿蒙的优势:
- 性能最优,体验最佳
- 可以充分利用鸿蒙的特有能力
- 技术栈更纯粹
但最终我们还是选择了 uni-app 适配,原因很现实:
- 开发成本:团队熟悉 Vue 技术栈,重新学习 ArkTS 成本太高
- 维护成本:三套代码意味着三倍的维护工作量
- 迭代速度:业务需求变化快,需要快速响应
- DCloud 的承诺:官方明确表示会持续加强鸿蒙支持
事后证明,这个选择是正确的,但过程远比想象中曲折。
环境搭建:第一个坑就栽了跟头
按照官方文档,我信心满满地开始环境搭建。HBuilderX、DevEco Studio、Node.js,一切看起来都很简单。
第一个坑:模拟器选择
文档说"下载 API 19 模拟器即可",但我下载后始终无法启动。各种错误提示看得我头皮发麻:
模拟器启动失败:硬件加速未开启
VT-x 不可用,请检查 BIOS 设置
我花了整整两天时间:
- 重启电脑进入 BIOS 开启虚拟化
- 在 Windows 功能中启用 Hyper-V
- 更新显卡驱动
- 重装 DevEco Studio
最后发现是杀毒软件冲突。关闭某数字卫士后,模拟器终于正常启动了。
经验教训:环境问题不要死磕,及时换思路。后来我直接使用真机调试,效率反而更高。
项目迁移:看似顺利的开始
将现有 uni-app 项目迁移到鸿蒙,最初进展出乎意料的顺利。
基础页面适配
大部分 Vue 页面无需修改即可运行,uni-app 的跨端能力确实令人印象深刻:
<template>
<view class="container">
<text class="title">{{ title }}</text>
<button @click="handleClick">点击我</button>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello HarmonyOS'
}
},
methods: {
handleClick() {
uni.showToast({
title: '点击成功'
})
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 18px;
color: #333;
}
</style>
路由配置
pages.json的配置也完全兼容:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页"
}
}
]
}
静态资源
static目录下的图片、字体等资源都能正常加载。前 80% 的工作在两天内就完成了,我甚至觉得鸿蒙开发不过如此。
但接下来的 20%,花了我 80% 的时间。
深水区:平台特定适配
导航栏差异
第一个平台差异出现在导航栏。在 iOS 和 Android 上正常的导航栏,在鸿蒙上出现了错位。
问题现象:
- 标题位置偏移
- 返回按钮点击区域异常
- 状态栏颜色不匹配
解决方案:
在 pages.json中为鸿蒙平台单独配置:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"app-plus": {
"titleNView": {
// iOS/Android 配置
}
},
"harmony": {
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "首页",
"navigationStyle": "default"
}
}
}
]
}
组件兼容性问题
更大的挑战来自组件兼容性。我们项目中使用了大量第三方组件,有些在鸿蒙上表现异常。
案例:图片裁剪组件
在 iOS/Android 上正常的图片裁剪组件,在鸿蒙上出现以下问题:
- 触摸事件响应错乱
- 裁剪框位置计算错误
- 性能卡顿明显
排查过程:
- 首先怀疑是触摸事件机制差异
- 尝试修改事件处理逻辑,效果有限
- 深入组件源码,发现使用了 DOM API
- 鸿蒙不支持部分 DOM API
最终方案:
寻找替代组件,选择明确支持鸿蒙的图片裁剪库。这个过程让我意识到:不是所有 web 生态都能无缝迁移到鸿蒙。
API 兼容性
uni-app 的 API 在鸿蒙上的支持程度不一:
完全兼容的:
uni.showToastuni.requestuni.getStorageuni.chooseImage
需要适配的:
uni.getSystemInfo返回的信息结构不同uni.onKeyboardHeightChange在鸿蒙上触发机制不同- 部分设备 API 需要鸿蒙特定实现
不支持的:
- 某些平台特定的扩展 API
我们建立了兼容性检查清单,对每个 API 进行测试:
// 兼容性封装示例
export const getSafeArea = () => {
return new Promise((resolve, reject) => {
uni.getSystemInfo({
success: (res) => {
// 鸿蒙平台特殊处理
if (res.platform === 'harmony') {
resolve({
top: res.statusBarHeight || 0,
bottom: 0,
left: 0,
right: 0
})
} else {
resolve(res.safeArea)
}
},
fail: reject
})
})
}
性能优化:从卡顿到流畅
首次在真机上运行应用时,性能问题让我震惊:列表滚动卡顿、页面切换白屏、内存占用过高。
列表性能优化
问题:商品列表页有 1000+ 项目,滚动时严重卡顿。
分析:
- 鸿蒙的列表渲染机制与 web 不同
- 大量 DOM 节点导致内存压力
- 图片加载没有做优化
解决方案:
- 虚拟列表:
<template>
<view class="list-container">
<unicloud-db
ref="udb"
collection="goods"
where="category_id == categoryId"
orderby="create_time desc"
:page-size="20"
@load="onListLoad"
v-slot:default="{data, loading, error}">
<virtual-list
:data="data"
:item-size="100"
key-field="_id">
<template v-slot:default="{item}">
<goods-item :item="item" />
</template>
</virtual-list>
</unicloud-db>
</view>
</template>
- 图片懒加载:
<image
:src="item.image"
mode="aspectFill"
lazy-load
:fade-show="false"
@load="onImageLoad"
@error="onImageError">
</image>
- 内存管理:
// 监听页面生命周期
onPageScroll(e) {
// 可视区域外的图片取消加载
this.checkVisibleImages()
},
onHide() {
// 页面隐藏时释放大资源
this.clearCache()
}
启动速度优化
问题:应用冷启动需要 3-5 秒,体验较差。
优化措施:
- 代码分包:
// pages.json
{
"subPackages": [
{
"root": "pagesA",
"pages": [
"page1/page1",
"page2/page2"
]
},
{
"root": "pagesB",
"pages": [
"page3/page3",
"page4/page4"
]
}
]
}
- 预加载关键资源:
// app.vue
onLaunch() {
// 预加载首屏必要数据
this.preloadEssentialData()
// 异步加载非关键资源
setTimeout(() => {
this.preloadSecondaryResources()
}, 1000)
}
- 减少同步操作:
// 优化前:同步阻塞
const userInfo = uni.getStorageSync('userInfo')
const settings = uni.getStorageSync('settings')
// 优化后:异步并行
Promise.all([
this.getStorage('userInfo'),
this.getStorage('settings')
]).then(([userInfo, settings]) => {
// 处理数据
})
经过优化后,启动时间缩短到 1-2 秒,列表滚动帧率稳定在 60fps。
打包发布:证书的"坑爹"之旅
如果说代码适配是技术挑战,那证书和打包就是流程挑战。
证书配置的坑
第一个坑:证书类型混淆
我一开始把调试证书当成发布证书使用,结果打包时各种错误:
错误: 证书类型不匹配
错误: 签名验证失败
解决方案:
- 调试证书:用于开发阶段真机调试
- 发布证书:用于应用商店上架
- 两者不能混用
第二个坑:证书有效期
发布证书有一年有效期,但测试证书只有三个月。我第一次提交审核时,证书差点过期。
经验:设置证书到期提醒,提前一个月更新。
打包流程优化
手动打包效率低下,我建立了自动化流程:
// package.json 脚本
{
"scripts": {
"build:harmony:debug": "uni build --platform harmony --mode development",
"build:harmony:release": "uni build --platform harmony --mode production",
"pack:harmony": "node scripts/pack-harmony.js",
"deploy:harmony": "npm run build:harmony:release && npm run pack:harmony"
}
}
// scripts/pack-harmony.js
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
console.log('🚀 开始鸿蒙应用打包...')
// 检查构建产物
const buildPath = path.join(__dirname, '../dist/build/harmony')
if (!fs.existsSync(buildPath)) {
console.error('❌ 构建产物不存在,请先执行构建')
process.exit(1)
}
// 执行鸿蒙特定打包逻辑
try {
execSync('node scripts/harmony-specific-pack.js', { stdio: 'inherit' })
console.log('✅ 鸿蒙应用打包完成')
} catch (error) {
console.error('❌ 打包失败:', error)
process.exit(1)
}
上架审核:意料之外的拒绝
第一次提交审核时,我信心满满,觉得经过充分测试的应用肯定能一次通过。结果被打脸了。
审核拒绝原因:
- 隐私政策不完整缺少数据存储说明未说明第三方 SDK 数据收集
- 权限说明不清晰相机权限用途描述过于简单位置权限必要性未充分说明
- 应用截图不规范截图包含状态栏时间"17:30"部分截图尺寸不一致
解决方案:
隐私政策完善:
## 数据存储说明
本应用使用本地存储保存用户偏好设置,使用华为云存储备份用户数据。所有数据均加密存储。
## 第三方 SDK 说明
- 华为账号 SDK:用于用户登录认证
- 华为推送 SDK:用于消息推送
- 微信分享 SDK:用于内容分享
权限说明重写:
## 相机权限
用途:用于扫描商品条形码、拍摄商品照片
必要性:核心功能需要,无法替代
数据处理:图片仅在本地处理,不上传服务器
## 位置权限
用途:用于推荐附近门店、配送地址定位
必要性:增值功能需要,非核心功能
数据处理:位置信息加密传输到服务器
截图规范:
- 使用模拟器统一截图
- 隐藏状态栏敏感信息
- 确保所有截图尺寸一致
- 展示核心功能流程
第二次提交后,顺利通过审核。
监控与反馈:上架只是开始
应用上架后,真正的挑战才开始。用户反馈和线上监控发现了我们测试中未发现的问题。
线上问题排查
案例:特定机型闪退
有用户反馈在华为 Mate 40 上频繁闪退,但我们测试机上正常。
排查过程:
- 查看 AGC 崩溃日志
- 发现内存溢出错误
- 定位到图片加载组件
- 特定机型内存管理更严格
解决方案:
// 图片加载优化
loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image()
// 限制图片尺寸
if (this.isLowMemoryDevice()) {
url = this.addImageSizeParams(url, '800x600')
}
img.onload = () => {
// 及时释放引用
img.onload = null
img.onerror = null
resolve(img)
}
img.onerror = reject
img.src = url
})
}
用户反馈处理
建立用户反馈响应机制:
- 24 小时内响应严重问题
- 每周汇总用户反馈
- 每月发布优化版本
经验总结与建议
技术层面
- 组件选择要谨慎优先选择官方组件验证第三方组件兼容性准备备用方案
- 性能优化要前置开始就考虑性能问题建立性能监控体系定期进行性能回归测试
流程层面
- 证书管理要规范区分调试和发布证书设置证书到期提醒备份证书文件
- 审核准备要充分仔细阅读审核指南准备完整的说明材料提前测试审核流程
- 监控体系要完善建立崩溃监控收集用户反馈定期分析使用数据
给后来者的建议
- 不要畏惧鸿蒙开发uni-app 已经提供了很好的支持大部分代码可以复用社区资源越来越丰富
- 从小功能开始尝试先移植简单页面逐步增加复杂度积累经验再处理核心功能
- 善用官方资源DCloud 文档很详细华为开发者社区活跃官方技术支持响应快
- 加入开发者社区学习他人经验分享自己的收获共同推动生态发展
未来规划
鸿蒙版本的成功上线,给了我们很大信心。下一步计划:
- 深度集成鸿蒙特性原子化服务探索分布式能力应用鸿蒙特有 UI 交互
- 性能持续优化启动速度优化到 1 秒内内存占用降低 30%功耗优化
- 多端协同与 iOS/Android 版本功能同步数据互通体验优化统一的设计语言
写在最后
三个月的鸿蒙适配之旅,让我深刻体会到:技术转型虽然痛苦,但收获远超预期。
最大的收获不是技术上架,而是思维转变:从"web 思维"到"多端思维",从"功能实现"到"体验优化"。
鸿蒙生态还在快速发展,现在入局正是好时机。uni-app 为跨端开发提供了很好的基础,让我们能够以较低成本探索新平台。
如果你也在考虑鸿蒙开发,我的建议是:不要观望,直接开始。从简单的 demo 开始,逐步深入,你会发现鸿蒙开发没有想象中那么难。
在这个过程中,你会遇到各种问题,但也会收获解决问题的成就感。更重要的是,你将成为新生态的早期参与者,这本身就是宝贵的机会。
希望这篇文章能帮助到正在或准备进行鸿蒙开发的你。如果遇到问题,欢迎交流讨论,我们一起推动鸿蒙生态的繁荣!
收起阅读 »【鸿蒙征文】从零到上线:uni-app vue2适配鸿蒙 NEXT +uts实况窗的实战成长纪实
作为一个靠 uni-app 做跨端开发的 “老玩家”,之前已经用它上线了五款 Vue2 的 App —— 不过这次多了个新目标:适配鸿蒙 NEXT 系统。目前成功打包上架鸿蒙两款APP,中间踩了不少坑,也攒了些实打实的经验,今天就把整个过程捋一捋,希望能帮到同样在做鸿蒙适配的朋友。
一、技术选型:为什么选 uni-app 做鸿蒙开发?
其实最开始也纠结过要不要用鸿蒙原生开发,但手里的项目是现成的 Vue2 代码,还要兼顾微信小程序 ——uni-app 的 “一次开发多端跑” 刚好戳中需求。后来查了文档才知道,uni-app 对鸿蒙 NEXT 的支持已经比较成熟了,尤其是 UTS 插件能直接调用鸿蒙原生能力,不用从头写原生代码,省了不少事。这也是我最终敲定用 uni-app 做适配的核心原因:能复用老项目代码,还能兼顾跨端,效率比重新开发高太多。
二、鸿蒙迁移实战
1.手里的 App 都是 Vue2 写的,但鸿蒙只支持 Vue3 的 uni-app 项目,所以第一步就得把 Vue2 迁移到 Vue3。需要适配vue3包括项目代码和相关组件。
首先是生命周期,Vue2 里的beforeDestroy得改成beforeUnmount,destroyed改成unmounted,一开始没注意,控制台报了一堆错;然后是 v-model,Vue3 里父子组件传值得写成:modelValue="xxx"和@update:modelValue="xxx=$event",之前的写法直接用不了;还有路由,Vue2 里是new VueRouter(),Vue3 得用createRouter和createWebHistory,这些基础语法得逐个核对。
组件方面,之前用的 Element UI 在 Vue3 里不兼容,只能换成 Element Plus。这里要注意,Element Plus 的引入方式和 Element UI 不一样,得在 main.js 里用createApp(App).use(ElementPlus),还得单独引入样式文件,不然组件样式会乱。我当时漏引了样式,页面加载出来全是裸奔的 HTML 元素,排查了半天才发现问题。
2.引入harmony-configs,注意里面的client_id的取值。
3.华为登录:必须做的 “硬性要求”
鸿蒙上架有个规定:只要 App 接入了第三方登录(比如微信、QQ 登录),就必须集成华为登录。不过好在 uni-app 已经封装了华为登录的 API,不用自己写原生代码,直接调用uni.login({provider: 'huawei'})就行。
但这里有个细节:调用登录前得确保harmony-configs已经初始化,不然会报 “client_id 未配置” 的错。我当时是在 main.js 里先初始化harmony-configs,再调用登录接口,代码大概是这样:
import harmonyConfigs from '@dcloudio/harmony-configs'
// 初始化,client_id从AGC拿
harmonyConfigs.init({
clientId: '这里填你的client_id',
scope: 'openid profile email'
})
// 后续调用华为登录
uni.login({
provider: 'huawei',
success: (res) => {
// 拿到code后去后端换token
console.log('华为登录code:', res.code)
},
fail: (err) => {
uni.showToast({ title: '登录失败:' + err.errMsg, icon: 'none' })
}
})
4.发布到鸿蒙开发平台,开发服务中要添加发布证书/调试证书 SHA256证书/公钥指纹。
迁移完代码,就得搭鸿蒙开发环境了。首先得装 DevEco Studio,我下的是 5.0 版本。然后是证书 —— 这是我踩的第二个 “坑”。
鸿蒙开发需要调试证书和发布证书,还得配置 SHA256 指纹。步骤其实不复杂:先在 DevEco Studio 里生成.p12 格式的密钥库,再用这个密钥库生成.csr 文件,然后去华为 AGC 平台(鸿蒙开发平台)上传.csr,申请发布证书(.cer 格式)和 Profile 文件(.p7b 格式)。最后要在 AGC 的 “应用信息” 里填 SHA256 指纹 —— 这里一定要注意,调试证书和发布证书的指纹要分开填,我一开始把调试指纹当成发布指纹填了,导致后来打包发布版登录失败,折腾了好久才发现。
另外,引入harmony-configs的时候,里面的client_id得从 AGC 平台拿 —— 在 “我的应用” 里找到对应的 App,进入 “配置” 页面,OAuth 2.0 客户端 ID 就是client_id,填错了后续华为登录会调不通。
5.鸿蒙隐私政策完善:请在相应场景补充HarmonyOS NEXT操作系统的介绍/选项/设备标识符等信息。可参考《审核指南》第1.14项:https://developer.huawei.com/consumer/cn/doc/app/50104-01
三、鸿蒙原生能力:uts实现实况窗接入
鸿蒙 NEXT 的实况窗是个特色功能,我做的App 是计时工具,刚好能用实况窗显示剩余时间,提升用户体验。这里就用到了 uni-app 的 UTS 插件,分享下具体怎么集成的。
UTS 插件结构
uni_modules/harmony-liveview/
├── package.json # 插件配置
├── utssdk/
│ ├── interface.uts # TypeScript 接口定义
│ └── app-harmony/
│ └── index.uts # 鸿蒙平台实现
├── readme.md # 使用文档
├── changelog.md # 更新日志
└── 使用说明.md # 本文件
核心 API
isLiveViewEnabled()- 检查实况窗是否可用startLiveView()- 启动实况窗updateLiveView()- 更新实况窗显示pauseLiveView()- 暂停/继续计时stopLiveView()- 停止实况窗
完整流程:
1.UTS 插件创建:自己搭了个简单的插件
我在uni_modules下建了个harmony-liveview文件夹,专门放实况窗相关的代码。结构很简单:utssdk文件夹里放接口定义(interface.uts)和鸿蒙原生实现(app-harmony/index.uts),还有个package.json配置插件信息。
interface.uts主要定义了几个核心方法的接口,比如检查实况窗是否可用、启动、停止、更新,这样 Vue 组件里调用的时候有类型提示;
index.uts是真正对接鸿蒙原生 LiveView API 的地方,比如startLiveView方法里,会先检查设备是否支持实况窗,再调用鸿蒙的LiveViewManager.start()启动,还加了定时更新数据的逻辑,默认 10 秒更一次。
2.uniapp vue组件集成:和业务逻辑绑在一起
我的计时页面是clock.vue,在里面导入了 UTS 插件的方法,然后把实况窗的启动 / 停止和计时功能绑在一起:用户点 “开始” 按钮,调用startTimer的时候,自动触发startHarmonyLiveView启动实况窗;点 “停止” 的时候,调用stopTimer,同时触发stopHarmonyLiveView停止实况窗。这里要注意,启动实况窗前得先调用isLiveViewEnabled()检查设备是否支持,不然在不支持的设备上会报错。我还加了个提示,不支持的话就弹个 Toast 告诉用户,体验会好一些。
3.本地调试,先申请权限也需要添加设备token,参考官方文档,添加后才能进行测试,只有调试没有问题才可以提交单独的实况窗开通审核。
4.实况窗上线:审核比想象中严格
实况窗要上线,得先在 AGC 申请权限,还得提交设计方案 —— 包括功能场景、界面布局图、资源占用情况。我当时提交的方案里,因为没写清楚 “为什么需要实时更新”,被审核打回来了,后来补充了 “计时工具需要用户在桌面就能看到剩余时间,不用打开 App” 的说明,才通过。
另外,本地测试的时候,要在 AGC 里添加测试设备的 Token,不然实况窗调不起来。我一开始没加,以为是代码有问题,查了半天文档才知道要配置设备 Token,这点大家要注意。
UTS代码片段:
参考资料:
- 鸿蒙 LiveView Kit 官方文档
- uni-app UTS 插件开发文档
- https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-Live-View-Flight
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/liveview-create-locally
四、上架前的 “避坑指南”:我踩过的 3 个关键问题
1.发布版白屏:云调试救了我
本地调试的时候一切正常,但打包发布版提审,鸿蒙审核那边反馈说白屏。我自己下载发布包安装,也遇到了同样的问题,不知道哪里出了错。后来想起 AGC 有个 “云调试” 功能,能选不同型号的鸿蒙设备测试,还能看日志。我用云调试跑了一遍,发现是 SHA256证书/公钥指纹的问题。
2.登录失败:指纹配置错了
调试版登录没问题,但发布版一直报 “Fingerprint verification error”。查了 AGC 的日志,发现是发布证书的 SHA256 指纹没配置 —— 我之前只填了调试指纹,发布指纹忘了填。补填之后,还得在manifest.json里把versionCode改大一点(比如从 100 改成 101),再让测试的手机把旧版本删掉,不然缓存会导致配置不生效。
3.鸿蒙备案:提前一周做准备
鸿蒙上架前必须完成 鸿蒙App 备案。备案提交后,审核界面的校验不是立马生效的,我当时等了差不多 1 天半才显示备案通过。
五、总结:几个实用的小经验
1.上线前可以用 AGC 的云调试测一遍,很多本地测不出来的问题(比如白屏、登录失败),云调试能快速帮你找到原因;
2.Vue2 迁 Vue3 的时候,别着急删老代码,先建个分支慢慢改,遇到问题还能回滚;
3.实况窗的设计方案要写详细,尤其是 “为什么需要这个功能”,不然审核容易被打回;
4.证书和指纹一定要区分调试版和发布版,别搞混了。
总的来说,用 uni-app 适配鸿蒙 NEXT 的流程不算复杂,只要把 Vue3 迁移、证书配置、华为登录这几个关键点搞定,后续上架就顺理成章了。中间虽然踩了不少坑,但解决问题的过程也是技术成长的过程。希望这篇记录能帮到大家,也欢迎大家在评论区分享自己的适配经验,一起交流进步!
作为一个靠 uni-app 做跨端开发的 “老玩家”,之前已经用它上线了五款 Vue2 的 App —— 不过这次多了个新目标:适配鸿蒙 NEXT 系统。目前成功打包上架鸿蒙两款APP,中间踩了不少坑,也攒了些实打实的经验,今天就把整个过程捋一捋,希望能帮到同样在做鸿蒙适配的朋友。
一、技术选型:为什么选 uni-app 做鸿蒙开发?
其实最开始也纠结过要不要用鸿蒙原生开发,但手里的项目是现成的 Vue2 代码,还要兼顾微信小程序 ——uni-app 的 “一次开发多端跑” 刚好戳中需求。后来查了文档才知道,uni-app 对鸿蒙 NEXT 的支持已经比较成熟了,尤其是 UTS 插件能直接调用鸿蒙原生能力,不用从头写原生代码,省了不少事。这也是我最终敲定用 uni-app 做适配的核心原因:能复用老项目代码,还能兼顾跨端,效率比重新开发高太多。
二、鸿蒙迁移实战
1.手里的 App 都是 Vue2 写的,但鸿蒙只支持 Vue3 的 uni-app 项目,所以第一步就得把 Vue2 迁移到 Vue3。需要适配vue3包括项目代码和相关组件。
首先是生命周期,Vue2 里的beforeDestroy得改成beforeUnmount,destroyed改成unmounted,一开始没注意,控制台报了一堆错;然后是 v-model,Vue3 里父子组件传值得写成:modelValue="xxx"和@update:modelValue="xxx=$event",之前的写法直接用不了;还有路由,Vue2 里是new VueRouter(),Vue3 得用createRouter和createWebHistory,这些基础语法得逐个核对。
组件方面,之前用的 Element UI 在 Vue3 里不兼容,只能换成 Element Plus。这里要注意,Element Plus 的引入方式和 Element UI 不一样,得在 main.js 里用createApp(App).use(ElementPlus),还得单独引入样式文件,不然组件样式会乱。我当时漏引了样式,页面加载出来全是裸奔的 HTML 元素,排查了半天才发现问题。
2.引入harmony-configs,注意里面的client_id的取值。
3.华为登录:必须做的 “硬性要求”
鸿蒙上架有个规定:只要 App 接入了第三方登录(比如微信、QQ 登录),就必须集成华为登录。不过好在 uni-app 已经封装了华为登录的 API,不用自己写原生代码,直接调用uni.login({provider: 'huawei'})就行。
但这里有个细节:调用登录前得确保harmony-configs已经初始化,不然会报 “client_id 未配置” 的错。我当时是在 main.js 里先初始化harmony-configs,再调用登录接口,代码大概是这样:
import harmonyConfigs from '@dcloudio/harmony-configs'
// 初始化,client_id从AGC拿
harmonyConfigs.init({
clientId: '这里填你的client_id',
scope: 'openid profile email'
})
// 后续调用华为登录
uni.login({
provider: 'huawei',
success: (res) => {
// 拿到code后去后端换token
console.log('华为登录code:', res.code)
},
fail: (err) => {
uni.showToast({ title: '登录失败:' + err.errMsg, icon: 'none' })
}
})
4.发布到鸿蒙开发平台,开发服务中要添加发布证书/调试证书 SHA256证书/公钥指纹。
迁移完代码,就得搭鸿蒙开发环境了。首先得装 DevEco Studio,我下的是 5.0 版本。然后是证书 —— 这是我踩的第二个 “坑”。
鸿蒙开发需要调试证书和发布证书,还得配置 SHA256 指纹。步骤其实不复杂:先在 DevEco Studio 里生成.p12 格式的密钥库,再用这个密钥库生成.csr 文件,然后去华为 AGC 平台(鸿蒙开发平台)上传.csr,申请发布证书(.cer 格式)和 Profile 文件(.p7b 格式)。最后要在 AGC 的 “应用信息” 里填 SHA256 指纹 —— 这里一定要注意,调试证书和发布证书的指纹要分开填,我一开始把调试指纹当成发布指纹填了,导致后来打包发布版登录失败,折腾了好久才发现。
另外,引入harmony-configs的时候,里面的client_id得从 AGC 平台拿 —— 在 “我的应用” 里找到对应的 App,进入 “配置” 页面,OAuth 2.0 客户端 ID 就是client_id,填错了后续华为登录会调不通。
5.鸿蒙隐私政策完善:请在相应场景补充HarmonyOS NEXT操作系统的介绍/选项/设备标识符等信息。可参考《审核指南》第1.14项:https://developer.huawei.com/consumer/cn/doc/app/50104-01
三、鸿蒙原生能力:uts实现实况窗接入
鸿蒙 NEXT 的实况窗是个特色功能,我做的App 是计时工具,刚好能用实况窗显示剩余时间,提升用户体验。这里就用到了 uni-app 的 UTS 插件,分享下具体怎么集成的。
UTS 插件结构
uni_modules/harmony-liveview/
├── package.json # 插件配置
├── utssdk/
│ ├── interface.uts # TypeScript 接口定义
│ └── app-harmony/
│ └── index.uts # 鸿蒙平台实现
├── readme.md # 使用文档
├── changelog.md # 更新日志
└── 使用说明.md # 本文件
核心 API
isLiveViewEnabled()- 检查实况窗是否可用startLiveView()- 启动实况窗updateLiveView()- 更新实况窗显示pauseLiveView()- 暂停/继续计时stopLiveView()- 停止实况窗
完整流程:
1.UTS 插件创建:自己搭了个简单的插件
我在uni_modules下建了个harmony-liveview文件夹,专门放实况窗相关的代码。结构很简单:utssdk文件夹里放接口定义(interface.uts)和鸿蒙原生实现(app-harmony/index.uts),还有个package.json配置插件信息。
interface.uts主要定义了几个核心方法的接口,比如检查实况窗是否可用、启动、停止、更新,这样 Vue 组件里调用的时候有类型提示;
index.uts是真正对接鸿蒙原生 LiveView API 的地方,比如startLiveView方法里,会先检查设备是否支持实况窗,再调用鸿蒙的LiveViewManager.start()启动,还加了定时更新数据的逻辑,默认 10 秒更一次。
2.uniapp vue组件集成:和业务逻辑绑在一起
我的计时页面是clock.vue,在里面导入了 UTS 插件的方法,然后把实况窗的启动 / 停止和计时功能绑在一起:用户点 “开始” 按钮,调用startTimer的时候,自动触发startHarmonyLiveView启动实况窗;点 “停止” 的时候,调用stopTimer,同时触发stopHarmonyLiveView停止实况窗。这里要注意,启动实况窗前得先调用isLiveViewEnabled()检查设备是否支持,不然在不支持的设备上会报错。我还加了个提示,不支持的话就弹个 Toast 告诉用户,体验会好一些。
3.本地调试,先申请权限也需要添加设备token,参考官方文档,添加后才能进行测试,只有调试没有问题才可以提交单独的实况窗开通审核。
4.实况窗上线:审核比想象中严格
实况窗要上线,得先在 AGC 申请权限,还得提交设计方案 —— 包括功能场景、界面布局图、资源占用情况。我当时提交的方案里,因为没写清楚 “为什么需要实时更新”,被审核打回来了,后来补充了 “计时工具需要用户在桌面就能看到剩余时间,不用打开 App” 的说明,才通过。
另外,本地测试的时候,要在 AGC 里添加测试设备的 Token,不然实况窗调不起来。我一开始没加,以为是代码有问题,查了半天文档才知道要配置设备 Token,这点大家要注意。
UTS代码片段:
参考资料:
- 鸿蒙 LiveView Kit 官方文档
- uni-app UTS 插件开发文档
- https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-Live-View-Flight
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/liveview-create-locally
四、上架前的 “避坑指南”:我踩过的 3 个关键问题
1.发布版白屏:云调试救了我
本地调试的时候一切正常,但打包发布版提审,鸿蒙审核那边反馈说白屏。我自己下载发布包安装,也遇到了同样的问题,不知道哪里出了错。后来想起 AGC 有个 “云调试” 功能,能选不同型号的鸿蒙设备测试,还能看日志。我用云调试跑了一遍,发现是 SHA256证书/公钥指纹的问题。
2.登录失败:指纹配置错了
调试版登录没问题,但发布版一直报 “Fingerprint verification error”。查了 AGC 的日志,发现是发布证书的 SHA256 指纹没配置 —— 我之前只填了调试指纹,发布指纹忘了填。补填之后,还得在manifest.json里把versionCode改大一点(比如从 100 改成 101),再让测试的手机把旧版本删掉,不然缓存会导致配置不生效。
3.鸿蒙备案:提前一周做准备
鸿蒙上架前必须完成 鸿蒙App 备案。备案提交后,审核界面的校验不是立马生效的,我当时等了差不多 1 天半才显示备案通过。
五、总结:几个实用的小经验
1.上线前可以用 AGC 的云调试测一遍,很多本地测不出来的问题(比如白屏、登录失败),云调试能快速帮你找到原因;
2.Vue2 迁 Vue3 的时候,别着急删老代码,先建个分支慢慢改,遇到问题还能回滚;
3.实况窗的设计方案要写详细,尤其是 “为什么需要这个功能”,不然审核容易被打回;
4.证书和指纹一定要区分调试版和发布版,别搞混了。
总的来说,用 uni-app 适配鸿蒙 NEXT 的流程不算复杂,只要把 Vue3 迁移、证书配置、华为登录这几个关键点搞定,后续上架就顺理成章了。中间虽然踩了不少坑,但解决问题的过程也是技术成长的过程。希望这篇记录能帮到大家,也欢迎大家在评论区分享自己的适配经验,一起交流进步!
收起阅读 »【鸿蒙征文】开源支持鸿蒙!uView Pro 开源三个月,近期更新全面大盘点,及未来计划
uView Pro 开源近三个月以来,收到了良好的反馈和迭代。目前 uView Pro 已经迭代了 40+ 个版本,平均每两天就会发布版本,主要是优化性能、新增\增强组件功能、bug修复、兼容性完善等。
所以目前 uView Pro 在稳定性、功能性与跨平台兼容性方面已经有了良好的表现。主要实现了 APP、鸿蒙、微信、支付宝、头条等小程序平台的兼容,后续也会继续进行迭代。
本文基于最近的 changelog 汇总,面向开发者与项目贡献者,系统介绍新增组件、关键修复、工具能力以及如何在项目中快速体验这些特性,并提供示例代码与资源链接,方便你在实际工程中落地使用。
一、总体概览
目前最新版本(0.3.15 及此前若干小版本)覆盖三大方向:
- 平台兼容与 bug 修复:适配更多小程序平台(包括鸿蒙/各小程序支持的完善),修复了 canvas 渲染、表单响应、picker 初始化、组件兼容性等若干跨端问题。
- 新组件与用户体验优化:推出并增强若干特色组件,如
u-fab(悬浮按钮)、u-text、u-loading-popup、u-textarea、u-safe-bottom、u-status-bar、u-root-portal,以满足常见 UI 场景需求。 - 工具链与框架能力:增强
http插件与useCompRelation(组件关系管理 Hooks),使业务层网络请求与复杂组件协作更便捷。
接下来我们把重点放在新增与优化的功能、示例使用以及工程实践建议上。
详情可查看官网及近期更新日志:https://uviewpro.cn/
二、亮点功能与新增组件(逐个拆解)
1) u-fab(悬浮按钮)
简介:u-fab 是面向移动端常见的悬浮操作入口,支持多种预设定位、拖动吸边(autoStick)以及 gap 属性的精细化配置。该组件在交互与无障碍体验上进行了增强,能兼容多端布局差异。
主要特性:
- 预设 position(如右下、左下、右中等)便于在不同 UI 布局中快速放置。
- 支持 gap 的对象式配置(top/right/bottom/left),使 demo 与真实项目兼容性更好。
- autoStick:拖动后自动吸边,提升交互体验。
示例:
示例(Vue 3 Composition API):
<template>
<u-fab position="right-bottom" :gap="gapObj" :draggable="true" :autoStick="true">
<template #default>
<u-button shape="circle" size="mini" type="primary" @click="onFabClick">
<u-icon name="thumb-up" size="40"></u-icon>
</u-button>
</template>
</u-fab>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const gapObj = { top: 20, right: 16, bottom: 16, left: 16 };
function onFabClick() {
uni.showToast({ title: '悬浮按钮点击' });
}
</script>
建议:在移动端应结合 safe area(如 u-safe-bottom)与页面常驻按钮布局谨慎使用 u-fab,避免遮挡关键内容。
更多用法请参考文档:https://uviewpro.cn/zh/components/fab.html
2) u-text
简介:u-text 提供更灵活的文字样式与插槽支持,能在长文本、富文本展示场景中替代常规标签并统一样式控制。
主要特性:
- 支持默认插槽与多种文本截断/换行策略。
- 更友好的样式穿透能力,方便主题化。
示例:
<!-- 主题颜色文字 -->
<u-text text="主色文字" type="primary"></u-text>
<!-- 拨打电话 -->
<u-text mode="phone" text="15019479320"></u-text>
<!-- 日期格式化 -->
<u-text mode="date" text="1612959739"></u-text>
<!-- 超链接 -->
<u-text mode="link" text="Go to uView Pro docs" href="https://uviewpro.cn"></u-text>
<!-- 姓名脱敏 -->
<u-text mode="name" text="张三三" format="encrypt"></u-text>
<!-- 显示金额 -->
<u-text mode="price" text="728732.32"></u-text>
<!-- 默认插槽 -->
<u-text class="desc">这是一个示例文本,支持自定义插槽与样式</u-text>
更多用法请参考文档:https://uviewpro.cn/zh/components/text.html
3) u-loading-popup
简介:一个可配置的加载弹窗组件,支持多种加载风格与遮罩配置,方便替代项目中散落的 loading 逻辑。
示例(最小用法):
<!-- 默认纵向加载 -->
<u-loading-popup v-model="loading" text="正在加载..." />
<!-- 横向加载 -->
<u-loading-popup v-model="loading" direction="horizontal" text="正在加载..." />
更多用法请参考文档:https://uviewpro.cn/zh/components/loadingPopup.html
4) u-textarea
简介:独立的 u-textarea 组件从 u-input 中拆分而来,增强了字数统计、伸缩、和独立样式控制能力,满足复杂表单与长文本输入场景。
示例:
<!-- 字数统计 -->
<u-textarea v-model="content" :maxlength="500" count />
<!-- 自动高度 -->
<u-textarea v-model="content" placeholder="请输入内容" autoHeight></u-textarea>
更多用法请参考文档:https://uviewpro.cn/zh/components/textarea.html
5) u-safe-bottom 与 u-status-bar
用途:与设备安全区(notch/safearea)相关的布局组件,用来保证底部/状态栏的展示在不同平台上都不会被遮挡或错位。适配了多端差异(iOS、Android、不同小程序宿主)。
如果有需要,您可以在任何地方引用它,它会自动判断在并且在 IPhone X 等机型的时候,给元素加上一个适当 底部内边距,在 APP 上,即使您保留了原生安全区占位(offset设置为auto),也不会导致底部出现双倍的空白区域,也即 APP 上 offset 设置为 auto 时。
<template>
<view>
......
<u-safe-bottom></u-safe-bottom>
</view>
</template>
更多用法请参考文档:https://uviewpro.cn/zh/components/safeAreaInset.html
6) u-root-portal
简介:提供将节点传送到根节点的能力(Portal 模式),适用于模态、全局浮层等需要脱离当前 dom 层级的场景,兼容多端实现细节。
根节点传送组件仅支持微信小程序、支付宝小程序、APP和H5平台,组件会自动根据平台选择合适的实现方式:
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身的代码是在同一个单文件组件中,因为它们都与组件的开关状态有关。
<u-button type="primary" @click="show = true">显示弹窗</u-button>
<u-root-portal v-if="show">
<view class="modal">
<view class="modal-content">
<text>这是一个全局弹窗</text>
<u-button @click="show = false">关闭</u-button>
</view>
</view>
</u-root-portal>
更多用法请参考文档:https://uviewpro.cn/zh/components/rootPortal.html
7) 自定义主题
uView Pro 目前可以自定主题色,字体颜色,边框颜色等,所有组件内部的样式,都基于同一套主题,比如您修改了primary主题色,所有用到了primary颜色 的组件都会受影响。
由于 uView 官方版本,组件内部存在许多硬编码颜色配置,无法动态根据 scss 变量,现在,我们可以统一跟随主题配置了。
通过官网主题颜色配置完后,在页面底部下载文件,会得到一个名为uview-pro.theme.scss和uview-pro.theme.ts的文件。
配置 scss 变量
/* uni.scss */
@import 'uview-pro/theme.scss';
配置 ts 变量
// main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import theme from '@/common/uview-pro.theme'
import uViewPro from 'uview-pro'
export function createApp() {
const app = createSSRApp(App)
// 引入uView Pro 主库,及theme主题
app.use(uViewPro, { theme })
return {
app
}
}
以上步骤完成之后,所有颜色均跟随主题色。
更多用法请参考文档:https://uviewpro.cn/zh/guide/theme.html
8) 自定义样式
uView Pro 默认提供了一套美观且统一的组件样式,但在实际项目开发中,往往需要根据业务需求进行个性化定制。参考自定义主题。
然而,如果仅是需要覆盖组件的默认样式,或增加样式,uView Pro 则支持两种主流的自定义样式方式,灵活满足各种场景:
目前,所有组件均支持 custom-class 样式穿透和 custom-style 内联样式
<view class="my-page">
<!-- custom-class 样式穿透 -->
<u-button custom-class="my-btn"></u-button>
<!-- 自定义内联样式 -->
<u-button
custom-style="background: linear-gradient(90deg,#2979ff,#00c6ff);color:#fff;border-radius:8px;"
></u-button>
</view>
<style lang="scss">
.my-page {
:deep(.my-btn) {
background-color: #2979ff;
color: #fff;
border-radius: 8px;
}
}
</style>
更多用法请参考文档:https://uviewpro.cn/zh/guide/style.html
三、工具链改进与新能力
1) http 插件(httpPlugin)
简介:提供统一的请求封装,支持 TypeScript、Vue3、组合式 API,插件化、全局配置、请求/响应拦截器、请求元信息类型(toast/loading 灵活控制),开箱即用,便于在项目中进行全局化网络管理。。
示例:基本请求
import { http } from 'uview-pro'
// GET
http.get('/api/user', { id: 1 }).then(res => {
/* ... */
})
// POST
http.post('/api/login', { username: 'xx', password: 'xx' }).then(res => {
/* ... */
})
// PUT/DELETE
http.put('/api/user/1', { name: 'new' })
http.delete('/api/user/1')
高级:支持请求拦截器、全局错误处理与 meta 配置,适合接入鉴权、重试、限流等策略。
最佳实践:定义拦截器配置 => 注册拦截器 => 统一 API 管理
定义拦截器配置
import type { RequestConfig, RequestInterceptor, RequestMeta, RequestOptions } from 'uview-pro'
import { useUserStore } from '@/store'
// 全局请求配置
export const httpRequestConfig: RequestConfig = {
baseUrl,
header: {
'content-type': 'application/json'
},
meta: {
originalData: true,
toast: true,
loading: true
}
}
// 全局请求/响应拦截器
export const httpInterceptor: RequestInterceptor = {
request: (config: RequestOptions) => {
// 请求拦截
return config
},
response: (response: any) => {
// 响应拦截
return response.data
}
}
注册拦截器:
import { createSSRApp } from 'vue'
import uViewPro, { httpPlugin } from 'uview-pro'
import { httpInterceptor, httpRequestConfig } from 'http.interceptor'
export function createApp() {
const app = createSSRApp(App)
// 注册uView-pro
app.use(uViewPro)
// 注册http插件
app.use(httpPlugin, {
interceptor: httpInterceptor,
requestConfig: httpRequestConfig
})
return { app }
}
统一 API 管理
// api/index.ts
import { http } from 'uview-pro'
export const login = data => http.post('/api/login', data, { meta: { loading: true, toast: true } })
export const getUser = id => http.get('/api/user', { id }, { meta: { loading: false } })
以上示例为经典最佳实践,更多用法请查看 http 插件文档:https://uviewpro.cn/zh/tools/http.html
2) useCompRelation(组件关系管理 Hooks)
目的:替代传统的 provide/inject 在多平台(尤其是一些小程序宿主)可能存在的兼容问题,提供更可靠的父子组件连接和事件广播机制。
应用场景:复杂表单、级联菜单、带有子项动态增删的组件集合等。
父组件示例(伪代码):
import { useParent } from 'uview-pro';
const { children, broadcast } = useParent('u-dropdown');
// 广播调用子组件函数
broadcast('childFunctionName', { payload });
// 收集所有子组件指定值
function getChildrenValues() {
let values: any[] = [];
children.forEach((child: any) => {
if (child.getExposed?.()?.isChecked.value) {
values.push(child.getExposed?.()?.name);
}
});
}
子组件示例(伪代码):
const { parentExposed, emitToParent } = useChildren('u-dropdown-item', 'u-dropdown');
// 触发父组件的函数
emitToParent('parentFunctionName');
// 获取父组件的变量
const activeColor = computed(() => parentExposed.value?.activeColor);
更多用法请参考组件源码:useCompRelation.ts
3) 提供 llms.txt
llms.txt的作用是什么,一般它用来告诉大模型是否允许抓取网站数据用于训练的文件,类似于 robots.txt 控制爬虫权限,因此 uView Pro 也提供了即时更新的 llms.txt 文件,便于训练大模型,更好的为我们服务,链接如下:
https://uviewpro.cn/llms-full.txt
四、多脚手架支持
1) create-uni
create-uni 提供一键生成、模板丰富的项目引导能力,旨在增强 uni-app 系列产品的开发体验,官网:https://uni-helper.cn/create-uni/core
pnpm create uni <项目名称> --ts -m pinia -m unocss -u uview-pro -e
表示:
- 启用 TypeScript
- 集成 ESLint 代码规范
- 启用 pinia
- 集成 unocss
- 选择 uview-pro组件库
如果你想用 create-uni 交互式创建一个项目,请执行以下命令:
pnpm create uni
进入交互式选择界面,选择 uView Pro 模板或组件,其他的相关插件可按需选择:
使用 create-uni 快速创建 uView Pro Starter 启动模板,请执行以下命令:
pnpm create uni <项目名称> -t uview-pro-starter
使用 create-uni 快速创建 uView Pro 完整组件演示模板,请执行以下命令:
pnpm create uni <项目名称> -t uview-pro-demo
2) unibest
unibest 是目前最火的 uni-app 脚手架,它是菲鸽大佬联同众多 uni-app 开发者共同贡献的 uni-app 框架,集成了最新技术栈和开发工具,官网:https://unibest.tech/
如果你想用 unibest 和 uView Pro 来创建项目,请执行以下命令:
一行代码创建项目:
pnpm create unibest <项目名称> -t base-uview-pro
交互式创建项目:
pnpm create unibest
选择 base-uview-pro 模板:
3) 官方cli
第一种:创建以 javascript 开发的工程
npx degit dcloudio/uni-preset-vue#vite my-vue3-project
第二种:创建以 typescript 开发的工程
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project
引入uview—pro组件库即可,不再过多介绍,可参考快速配置:https://uviewpro.cn/zh/components/setting.html
五、近期修复若干关键问题
- u-circle-progress 的 canvas 渲染问题已修复,解决了微信小程序 canvas 2D 在不同平台上下文差异导致的绘制异常。
- u-form 相关多个修复:处理 model 替换导致校验失效、resetFields 修复、u-form-item 样式与光标问题修复,提升表单在小程序端兼容性。
- picker、index-list、popup 等组件的跨端兼容修复,减少在头条/支付宝/微信等宿主上的差异表现。
这些修复的综合效果是:在多端使用 uView‑Pro 构建页面时,出现的平台差异与边缘 bug 大幅减少,开发成本降低。
六、跨平台支持说明
当前 uView‑Pro 已兼容并在以下平台进行适配与测试:
- 鸿蒙(HarmonyOS)
- Android(原生应用及 WebView)
- iOS(原生应用及 WebView)
- 微信小程序
- 支付宝小程序
- 头条小程序
后续仍然会对多端小程序兼容性的持续投入,很多修复直接针对宿主差异展开(例如 Canvas 行为、provide/inject 实现差异、样式差异等)。
近期在鸿蒙6.0系统上运行uView Pro源码,效果还不错,如下:
七、未来计划
根据规划,未来几个方向包括:
- 持续优化现有组件,新增组件,提升用户体验
- 国际化(i18n)支持:统一组件的语言切换能力,方便多语言产品线接入。
- 暗黑模式(Dark Mode):与运行时主题切换能力结合,提供暗色皮肤一键切换体验。
- 优化现有平台兼容性,扩展更多平台的适配测试(保持对小程序宿主的兼容修复)。
- uni-app x 支持:目前还在调研中。
- mcp 支持。
八、结语
如果你在项目中使用到以上组件或工具,并希望参与贡献,请参考仓库的贡献指南。欢迎提 issue、提交 PR,或在插件市场与社区中反馈使用体验。
uView Pro 开源近三个月以来,收到了良好的反馈和迭代。目前 uView Pro 已经迭代了 40+ 个版本,平均每两天就会发布版本,主要是优化性能、新增\增强组件功能、bug修复、兼容性完善等。
所以目前 uView Pro 在稳定性、功能性与跨平台兼容性方面已经有了良好的表现。主要实现了 APP、鸿蒙、微信、支付宝、头条等小程序平台的兼容,后续也会继续进行迭代。
本文基于最近的 changelog 汇总,面向开发者与项目贡献者,系统介绍新增组件、关键修复、工具能力以及如何在项目中快速体验这些特性,并提供示例代码与资源链接,方便你在实际工程中落地使用。
一、总体概览
目前最新版本(0.3.15 及此前若干小版本)覆盖三大方向:
- 平台兼容与 bug 修复:适配更多小程序平台(包括鸿蒙/各小程序支持的完善),修复了 canvas 渲染、表单响应、picker 初始化、组件兼容性等若干跨端问题。
- 新组件与用户体验优化:推出并增强若干特色组件,如
u-fab(悬浮按钮)、u-text、u-loading-popup、u-textarea、u-safe-bottom、u-status-bar、u-root-portal,以满足常见 UI 场景需求。 - 工具链与框架能力:增强
http插件与useCompRelation(组件关系管理 Hooks),使业务层网络请求与复杂组件协作更便捷。
接下来我们把重点放在新增与优化的功能、示例使用以及工程实践建议上。
详情可查看官网及近期更新日志:https://uviewpro.cn/
二、亮点功能与新增组件(逐个拆解)
1) u-fab(悬浮按钮)
简介:u-fab 是面向移动端常见的悬浮操作入口,支持多种预设定位、拖动吸边(autoStick)以及 gap 属性的精细化配置。该组件在交互与无障碍体验上进行了增强,能兼容多端布局差异。
主要特性:
- 预设 position(如右下、左下、右中等)便于在不同 UI 布局中快速放置。
- 支持 gap 的对象式配置(top/right/bottom/left),使 demo 与真实项目兼容性更好。
- autoStick:拖动后自动吸边,提升交互体验。
示例:
示例(Vue 3 Composition API):
<template>
<u-fab position="right-bottom" :gap="gapObj" :draggable="true" :autoStick="true">
<template #default>
<u-button shape="circle" size="mini" type="primary" @click="onFabClick">
<u-icon name="thumb-up" size="40"></u-icon>
</u-button>
</template>
</u-fab>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const gapObj = { top: 20, right: 16, bottom: 16, left: 16 };
function onFabClick() {
uni.showToast({ title: '悬浮按钮点击' });
}
</script>
建议:在移动端应结合 safe area(如 u-safe-bottom)与页面常驻按钮布局谨慎使用 u-fab,避免遮挡关键内容。
更多用法请参考文档:https://uviewpro.cn/zh/components/fab.html
2) u-text
简介:u-text 提供更灵活的文字样式与插槽支持,能在长文本、富文本展示场景中替代常规标签并统一样式控制。
主要特性:
- 支持默认插槽与多种文本截断/换行策略。
- 更友好的样式穿透能力,方便主题化。
示例:
<!-- 主题颜色文字 -->
<u-text text="主色文字" type="primary"></u-text>
<!-- 拨打电话 -->
<u-text mode="phone" text="15019479320"></u-text>
<!-- 日期格式化 -->
<u-text mode="date" text="1612959739"></u-text>
<!-- 超链接 -->
<u-text mode="link" text="Go to uView Pro docs" href="https://uviewpro.cn"></u-text>
<!-- 姓名脱敏 -->
<u-text mode="name" text="张三三" format="encrypt"></u-text>
<!-- 显示金额 -->
<u-text mode="price" text="728732.32"></u-text>
<!-- 默认插槽 -->
<u-text class="desc">这是一个示例文本,支持自定义插槽与样式</u-text>
更多用法请参考文档:https://uviewpro.cn/zh/components/text.html
3) u-loading-popup
简介:一个可配置的加载弹窗组件,支持多种加载风格与遮罩配置,方便替代项目中散落的 loading 逻辑。
示例(最小用法):
<!-- 默认纵向加载 -->
<u-loading-popup v-model="loading" text="正在加载..." />
<!-- 横向加载 -->
<u-loading-popup v-model="loading" direction="horizontal" text="正在加载..." />
更多用法请参考文档:https://uviewpro.cn/zh/components/loadingPopup.html
4) u-textarea
简介:独立的 u-textarea 组件从 u-input 中拆分而来,增强了字数统计、伸缩、和独立样式控制能力,满足复杂表单与长文本输入场景。
示例:
<!-- 字数统计 -->
<u-textarea v-model="content" :maxlength="500" count />
<!-- 自动高度 -->
<u-textarea v-model="content" placeholder="请输入内容" autoHeight></u-textarea>
更多用法请参考文档:https://uviewpro.cn/zh/components/textarea.html
5) u-safe-bottom 与 u-status-bar
用途:与设备安全区(notch/safearea)相关的布局组件,用来保证底部/状态栏的展示在不同平台上都不会被遮挡或错位。适配了多端差异(iOS、Android、不同小程序宿主)。
如果有需要,您可以在任何地方引用它,它会自动判断在并且在 IPhone X 等机型的时候,给元素加上一个适当 底部内边距,在 APP 上,即使您保留了原生安全区占位(offset设置为auto),也不会导致底部出现双倍的空白区域,也即 APP 上 offset 设置为 auto 时。
<template>
<view>
......
<u-safe-bottom></u-safe-bottom>
</view>
</template>
更多用法请参考文档:https://uviewpro.cn/zh/components/safeAreaInset.html
6) u-root-portal
简介:提供将节点传送到根节点的能力(Portal 模式),适用于模态、全局浮层等需要脱离当前 dom 层级的场景,兼容多端实现细节。
根节点传送组件仅支持微信小程序、支付宝小程序、APP和H5平台,组件会自动根据平台选择合适的实现方式:
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身的代码是在同一个单文件组件中,因为它们都与组件的开关状态有关。
<u-button type="primary" @click="show = true">显示弹窗</u-button>
<u-root-portal v-if="show">
<view class="modal">
<view class="modal-content">
<text>这是一个全局弹窗</text>
<u-button @click="show = false">关闭</u-button>
</view>
</view>
</u-root-portal>
更多用法请参考文档:https://uviewpro.cn/zh/components/rootPortal.html
7) 自定义主题
uView Pro 目前可以自定主题色,字体颜色,边框颜色等,所有组件内部的样式,都基于同一套主题,比如您修改了primary主题色,所有用到了primary颜色 的组件都会受影响。
由于 uView 官方版本,组件内部存在许多硬编码颜色配置,无法动态根据 scss 变量,现在,我们可以统一跟随主题配置了。
通过官网主题颜色配置完后,在页面底部下载文件,会得到一个名为uview-pro.theme.scss和uview-pro.theme.ts的文件。
配置 scss 变量
/* uni.scss */
@import 'uview-pro/theme.scss';
配置 ts 变量
// main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import theme from '@/common/uview-pro.theme'
import uViewPro from 'uview-pro'
export function createApp() {
const app = createSSRApp(App)
// 引入uView Pro 主库,及theme主题
app.use(uViewPro, { theme })
return {
app
}
}
以上步骤完成之后,所有颜色均跟随主题色。
更多用法请参考文档:https://uviewpro.cn/zh/guide/theme.html
8) 自定义样式
uView Pro 默认提供了一套美观且统一的组件样式,但在实际项目开发中,往往需要根据业务需求进行个性化定制。参考自定义主题。
然而,如果仅是需要覆盖组件的默认样式,或增加样式,uView Pro 则支持两种主流的自定义样式方式,灵活满足各种场景:
目前,所有组件均支持 custom-class 样式穿透和 custom-style 内联样式
<view class="my-page">
<!-- custom-class 样式穿透 -->
<u-button custom-class="my-btn"></u-button>
<!-- 自定义内联样式 -->
<u-button
custom-style="background: linear-gradient(90deg,#2979ff,#00c6ff);color:#fff;border-radius:8px;"
></u-button>
</view>
<style lang="scss">
.my-page {
:deep(.my-btn) {
background-color: #2979ff;
color: #fff;
border-radius: 8px;
}
}
</style>
更多用法请参考文档:https://uviewpro.cn/zh/guide/style.html
三、工具链改进与新能力
1) http 插件(httpPlugin)
简介:提供统一的请求封装,支持 TypeScript、Vue3、组合式 API,插件化、全局配置、请求/响应拦截器、请求元信息类型(toast/loading 灵活控制),开箱即用,便于在项目中进行全局化网络管理。。
示例:基本请求
import { http } from 'uview-pro'
// GET
http.get('/api/user', { id: 1 }).then(res => {
/* ... */
})
// POST
http.post('/api/login', { username: 'xx', password: 'xx' }).then(res => {
/* ... */
})
// PUT/DELETE
http.put('/api/user/1', { name: 'new' })
http.delete('/api/user/1')
高级:支持请求拦截器、全局错误处理与 meta 配置,适合接入鉴权、重试、限流等策略。
最佳实践:定义拦截器配置 => 注册拦截器 => 统一 API 管理
定义拦截器配置
import type { RequestConfig, RequestInterceptor, RequestMeta, RequestOptions } from 'uview-pro'
import { useUserStore } from '@/store'
// 全局请求配置
export const httpRequestConfig: RequestConfig = {
baseUrl,
header: {
'content-type': 'application/json'
},
meta: {
originalData: true,
toast: true,
loading: true
}
}
// 全局请求/响应拦截器
export const httpInterceptor: RequestInterceptor = {
request: (config: RequestOptions) => {
// 请求拦截
return config
},
response: (response: any) => {
// 响应拦截
return response.data
}
}
注册拦截器:
import { createSSRApp } from 'vue'
import uViewPro, { httpPlugin } from 'uview-pro'
import { httpInterceptor, httpRequestConfig } from 'http.interceptor'
export function createApp() {
const app = createSSRApp(App)
// 注册uView-pro
app.use(uViewPro)
// 注册http插件
app.use(httpPlugin, {
interceptor: httpInterceptor,
requestConfig: httpRequestConfig
})
return { app }
}
统一 API 管理
// api/index.ts
import { http } from 'uview-pro'
export const login = data => http.post('/api/login', data, { meta: { loading: true, toast: true } })
export const getUser = id => http.get('/api/user', { id }, { meta: { loading: false } })
以上示例为经典最佳实践,更多用法请查看 http 插件文档:https://uviewpro.cn/zh/tools/http.html
2) useCompRelation(组件关系管理 Hooks)
目的:替代传统的 provide/inject 在多平台(尤其是一些小程序宿主)可能存在的兼容问题,提供更可靠的父子组件连接和事件广播机制。
应用场景:复杂表单、级联菜单、带有子项动态增删的组件集合等。
父组件示例(伪代码):
import { useParent } from 'uview-pro';
const { children, broadcast } = useParent('u-dropdown');
// 广播调用子组件函数
broadcast('childFunctionName', { payload });
// 收集所有子组件指定值
function getChildrenValues() {
let values: any[] = [];
children.forEach((child: any) => {
if (child.getExposed?.()?.isChecked.value) {
values.push(child.getExposed?.()?.name);
}
});
}
子组件示例(伪代码):
const { parentExposed, emitToParent } = useChildren('u-dropdown-item', 'u-dropdown');
// 触发父组件的函数
emitToParent('parentFunctionName');
// 获取父组件的变量
const activeColor = computed(() => parentExposed.value?.activeColor);
更多用法请参考组件源码:useCompRelation.ts
3) 提供 llms.txt
llms.txt的作用是什么,一般它用来告诉大模型是否允许抓取网站数据用于训练的文件,类似于 robots.txt 控制爬虫权限,因此 uView Pro 也提供了即时更新的 llms.txt 文件,便于训练大模型,更好的为我们服务,链接如下:
https://uviewpro.cn/llms-full.txt
四、多脚手架支持
1) create-uni
create-uni 提供一键生成、模板丰富的项目引导能力,旨在增强 uni-app 系列产品的开发体验,官网:https://uni-helper.cn/create-uni/core
pnpm create uni <项目名称> --ts -m pinia -m unocss -u uview-pro -e
表示:
- 启用 TypeScript
- 集成 ESLint 代码规范
- 启用 pinia
- 集成 unocss
- 选择 uview-pro组件库
如果你想用 create-uni 交互式创建一个项目,请执行以下命令:
pnpm create uni
进入交互式选择界面,选择 uView Pro 模板或组件,其他的相关插件可按需选择:
使用 create-uni 快速创建 uView Pro Starter 启动模板,请执行以下命令:
pnpm create uni <项目名称> -t uview-pro-starter
使用 create-uni 快速创建 uView Pro 完整组件演示模板,请执行以下命令:
pnpm create uni <项目名称> -t uview-pro-demo
2) unibest
unibest 是目前最火的 uni-app 脚手架,它是菲鸽大佬联同众多 uni-app 开发者共同贡献的 uni-app 框架,集成了最新技术栈和开发工具,官网:https://unibest.tech/
如果你想用 unibest 和 uView Pro 来创建项目,请执行以下命令:
一行代码创建项目:
pnpm create unibest <项目名称> -t base-uview-pro
交互式创建项目:
pnpm create unibest
选择 base-uview-pro 模板:
3) 官方cli
第一种:创建以 javascript 开发的工程
npx degit dcloudio/uni-preset-vue#vite my-vue3-project
第二种:创建以 typescript 开发的工程
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project
引入uview—pro组件库即可,不再过多介绍,可参考快速配置:https://uviewpro.cn/zh/components/setting.html
五、近期修复若干关键问题
- u-circle-progress 的 canvas 渲染问题已修复,解决了微信小程序 canvas 2D 在不同平台上下文差异导致的绘制异常。
- u-form 相关多个修复:处理 model 替换导致校验失效、resetFields 修复、u-form-item 样式与光标问题修复,提升表单在小程序端兼容性。
- picker、index-list、popup 等组件的跨端兼容修复,减少在头条/支付宝/微信等宿主上的差异表现。
这些修复的综合效果是:在多端使用 uView‑Pro 构建页面时,出现的平台差异与边缘 bug 大幅减少,开发成本降低。
六、跨平台支持说明
当前 uView‑Pro 已兼容并在以下平台进行适配与测试:
- 鸿蒙(HarmonyOS)
- Android(原生应用及 WebView)
- iOS(原生应用及 WebView)
- 微信小程序
- 支付宝小程序
- 头条小程序
后续仍然会对多端小程序兼容性的持续投入,很多修复直接针对宿主差异展开(例如 Canvas 行为、provide/inject 实现差异、样式差异等)。
近期在鸿蒙6.0系统上运行uView Pro源码,效果还不错,如下:
七、未来计划
根据规划,未来几个方向包括:
- 持续优化现有组件,新增组件,提升用户体验
- 国际化(i18n)支持:统一组件的语言切换能力,方便多语言产品线接入。
- 暗黑模式(Dark Mode):与运行时主题切换能力结合,提供暗色皮肤一键切换体验。
- 优化现有平台兼容性,扩展更多平台的适配测试(保持对小程序宿主的兼容修复)。
- uni-app x 支持:目前还在调研中。
- mcp 支持。
八、结语
如果你在项目中使用到以上组件或工具,并希望参与贡献,请参考仓库的贡献指南。欢迎提 issue、提交 PR,或在插件市场与社区中反馈使用体验。
- Github:https://github.com/anyup/uView-Pro
- Gitee:https://gitee.com/anyup/uView-Pro
- 快速启动仓库:https://github.com/anyup/uView-Pro-Starter
- 插件市场(DCloud):https://ext.dcloud.net.cn/plugin?id=24633
- 官网:https://uviewpro.cn
























