/ 今日科技快讯 /
近日,浙江凌昇动力科技有限公司发生工商变更,注册资本由2亿人民币增至5.5亿人民币。该公司成立于2021年12月,法定代表人为朱江明,经营范围含汽车零部件研发、汽车零配件零售、能量回收系统研发、电机及其控制系统研发、新能源汽车电附件销售、智能控制系统集成、数据处理和存储支持服务、互联网数据服务等。股东信息显示,该公司由零跑汽车全资持股。
/ 作者简介 /
本篇文章来自JasonYin~的投稿,文章主要分享了HarmonyOS NEXT开发中如何封装多种样式导航栏组件,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
https://blog.csdn.net/qq_40533422?type=blog
/ 正文 /
涉及知识点和装饰器
- @ComponentV2,@Local,@Builder,@BuilderParam,@Extend,@Require,@Param,@Event等
- 第三方库:ZRouter ,如项目中本来就用了ZRouter路由库,案例中点返回按钮直接使用了 ZRouter.pop(),没有用到的话也支持自定义返回事件。
背景
在项目开发进程中,导航栏的应用场景颇为繁多。以我的页面为例,其导航栏呈现为图标、文字与箭头相组合的样式;而设置页面的导航栏则是图标、文字、右侧文字以及小红点的搭配形式;至于公用顶部导航栏,又表现为左侧返回图标、中间文字、右侧图标与文字的布局。倘若针对每一处用到导航栏的地方均单独编写代码,那么代码的重复编写现象将极为严重。基于此,我们可采用自定义封装的方式构建公用组件。如此一来,不仅为项目后期的维护与拓展提供了极大的便利,同时也能够显著提升开发效率,让开发者有更多精力投入到更具价值的工作思考中,减少不必要的重复劳作时间消耗。
先上效果图
图一
图二
图三
实现图一效果图
1、首先需要定义好类型,比如图片+文字+小红点+返回右键等。
@ObservedV2
export class TabHorizontalModel {
title: string;
index: number; //下标
icon: string | Resource;
hasIcon: boolean; //是否显示图
@Trace rightTitle: string;
hasRightTitle: boolean;
@Trace hasNew: boolean; //是否显示红点
hasRightIcon: boolean; //是否显示图
constructor(title: string, index: number = -1, icon: string | Resource = '', hasIcon: boolean = false, rightTitle: string = '', hasRightTitle: boolean = false, hasNew: boolean = false,
hasRightIcon: boolean = true) {
this.icon = icon;
this.hasIcon = hasIcon;
this.title = title;
this.rightTitle = rightTitle;
this.hasRightTitle = hasRightTitle;
this.hasNew = hasNew && rightTitle !== '';
this.index = index;
this.hasRightIcon = hasRightIcon;
}
}
2、封装一个通用的横向Tab 图片、文字、右边文字、小红点组件。
import { CommonConst } from "utils"
import { TabHorizontalModel } from "../model/TabHorizontalModel"
/**
* Author:J
* Describe: 横向Tab 图片、文字、右边文字、小红点
*/
@ComponentV2
export struct HorizontalTabItemComp {
@Param @Require tabItem: TabHorizontalModel= new TabHorizontalModel('')
@Param onItemClick?: () => void = undefined
build() {
Row() {
Image(this.tabItem.icon)
.width(24)
.margin({ right: 12 })
.visibility(this.tabItem.hasIcon ? Visibility.Visible : Visibility.None)
Text(this.tabItem.title)
.fontSize(16)
.fontColor($r('app.color.color_222222'))
.layoutWeight(1)
if (this.tabItem.hasNew) {
Badge({
value: '',
position: BadgePosition.Right,
style: { badgeSize: 7, badgeColor: $r('app.color.color_FA2A2D') }
}) {
Text(this.tabItem.rightTitle)
.fontSize(16)
.fontColor($r('app.color.color_222222'))
.visibility(this.tabItem.hasRightTitle ? Visibility.Visible : Visibility.None)
.margin({ right: 20 })
}
} else {
Text(this.tabItem.rightTitle)
.fontSize(16)
.fontColor($r('app.color.color_222222'))
.visibility(this.tabItem.hasRightTitle ? Visibility.Visible : Visibility.None)
}
Image($r('app.media.ic_arrow_right_gray_small'))
.width(24)
.margin({ left: 12 })
.visibility(this.tabItem.hasRightIcon ? Visibility.Visible : Visibility.None)
}
.width(CommonConst.FULL_PARENT)
.height(44)
.backgroundColor($r('app.color.white'))
.onClick(() => {
this.onItemClick?.()
})
}
}
使用案例
针对于一个,可以用下面的代码,但是对于一个页面有多个的话,要是一行行的写,虽然可以,但是不建议,而且也不优雅,所以需要用到ForEach来实现。
HorizontalTabItemComp({
tabItem: new TabHorizontalModel("我的积分", 0, $r('app.media.ic_coin'), true),
onItemClick: () => {
ToastUtil.showToast('我的积分')
}
}).margin({ left: 12, right: 12 })
定义一组数据,塞到数组里。
/** 横向Tab */
export const horizontalTabItemData: Array = [
new TabHorizontalModel("我的积分", 0, $r('app.media.ic_coin'), true, '666', true),
new TabHorizontalModel("我的分享", 1, $r('app.media.ic_share_article'), true),
new TabHorizontalModel("我的收藏", 2, $r('app.media.ic_collect'), true),
new TabHorizontalModel("我的书签", 3, $r('app.media.ic_read_later'), true),
new TabHorizontalModel("阅读历史", 4, $r('app.media.ic_read_record'), true),
new TabHorizontalModel("开源项目", 5, $r('app.media.ic_github'), true),
new TabHorizontalModel("关于作者", 6, $r('app.media.ic_about'), true, '请他喝杯咖啡~', true),
]
使用ForEach来实现。
ForEach(horizontalTabItemData, (item: TabHorizontalModel, index: number) => {
HorizontalTabItemComp({
tabItem: item,
onItemClick: () => {
this.onItemClick(item)
}
}).margin({ left: 12, right: 12 })
})
/** 点击事件 */
private onItemClick(item: TabHorizontalModel) {
ToastUtil.showToast(item.title)
if (item.index == 0) {
} else if (item.index == 1) {
} else if (item.index == 2) {
} else if (item.index == 3) {
} else if (item.index == 4) {
} else if (item.index == 5) {
} else if (item.index == 6) {
}
}
实现图二效果图
1、首先需要定义好需要的参数。
/** 标题 */
@Param title: ResourceStr = '';
/** 返回按钮的点击事件 */
@Param backClick?: (event?: ClickEvent) => void = undefined;
/** 是否显示右侧按钮 */
@Param isShowRight: boolean = false;
/** 右侧标题 */
@Param rightTitle: ResourceStr = '';
/** 右侧图片 */
@Param rightImage: ResourceStr = '';
/** 右侧点击事件 */
@Param rightClick?: (event?: ClickEvent) => void = undefined;
2、封装一个公用的自定义导航栏组件,内置了导航栏的返回按钮、标题、右侧按钮等,完整代码如下:
import { ZRouter } from '@hzw/zrouter';
import { CommonConst } from 'utils';
/**
* Author:J
* Describe:自定义导航栏组件
* 内置了导航栏的返回按钮、标题、右侧按钮等
*/
@ComponentV2
export struct TitleBarComp {
/** 标题 */
@Param title: ResourceStr = '';
/** 返回按钮的点击事件 */
@Param backClick?: (event?: ClickEvent) => void = undefined;
/** 是否显示右侧按钮 */
@Param isShowRight: boolean = false;
/** 右侧标题 */
@Param rightTitle: ResourceStr = '';
/** 右侧图片 */
@Param rightImage: ResourceStr = '';
/** 右侧点击事件 */
@Param rightClick?: (event?: ClickEvent) => void = undefined;
build() {
Column() {
Row() {
Image($r('app.media.ic_arrow_left')).width(44).padding(8).onClick(() => {
if (this.backClick) {
this.backClick()
} else {
ZRouter.pop()
}
})
Text(this.title)
.fontColor($r('app.color.color_222222'))
.fontSize(16)
.maxLines(1)
.fontWeight(FontWeight.Bold)
Row() {
if (this.rightTitle) {
Text(this.rightTitle)
.fontColor($r('app.color.color_222222'))
.fontSize(16)
.margin({ right: 10 })
} else {
Image(this.rightImage ? this.rightImage : $r('app.media.ic_local_search'))
.width(44)
.padding(10)
}
}
.onClick(this.rightClick)
.visibility(this.isShowRight ? Visibility.Visible : Visibility.Hidden)
}
.width(CommonConst.FULL_PARENT)
.height(44)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor($r('app.color.white'))
Divider()
.width(CommonConst.FULL_PARENT)
.color($r('app.color.color_F0F0F0'))
}
.width(CommonConst.FULL_PARENT)
.height(45)
}
}
3、使用案例,包含了多种样式使用。
NavDestination() {
Column({space:8}) {
Text('第一种样式').fontColor(Color.Red)
TitleBarComp({ title: '设置' })
Text('第二种样式,自定义返回事件').fontColor(Color.Red)
TitleBarComp({
title: '设置二', backClick: () => {
ToastUtil.showToast('自定义返回事件')
}
})
Text('第三种样式,右边有文字').fontColor(Color.Red)
TitleBarComp({
title: '设置三',
isShowRight: true,
rightTitle: '右边',
rightClick: () => {
ToastUtil.showToast('右边')
}
})
Text('第四种,右边有图片').fontColor(Color.Red)
TitleBarComp({
title: '设置四',
isShowRight: true,
rightImage: $r('app.media.ic_share_article'),
rightClick: () => {
ToastUtil.showToast('右边')
}
})
}
.width(CommonConst.FULL_PARENT)
.height(CommonConst.FULL_PARENT)
.backgroundColor($r('app.color.white'))
}
.hideTitleBar(true)
实现图三效果图
背景
这个逻辑比较复杂,一步步优化实现,为啥还需要自定义,直接用官方自带的Tabs+TabContent就可以实现啊;如果只是针对于一行都是简单文字切换那还好,但是对于那种,左边、右边是图片+中间是文字用自带的就不行了,因为下面的内容的宽度是铺满屏幕的宽度的,所以需要自定义。
1、定义需要的参数,自定义左边视图,右边视图,内容,下划线,是否滑动等,具体可以看完整代码。
@Param currentTabIndex: number = 0;
@Param tabContentArr: boolean[] = []; //存储页面状态
private tabsController: TabsController = new TabsController();
@Param tabs: Array = [];
//左边视图
@BuilderParam tabBarLeft: () => void = this.barLeft;
//右边视图
@BuilderParam tabBarRight: () => void = this.barRight;
//内容
@BuilderParam tabContentBuilder: ($$: TabBarModel) => void = this._TabContentBuilder;
//是否显示下划线
@Param isShowDivider: boolean = false;
//是否滑动
@Param scrollable: boolean = false;
//顶部中间视图是否居中 true居中 false 默认 居左
@Param isTabBarCenter: boolean = false;
//选中字体颜色
@Param selectFontColor: ResourceColor = $r('app.color.color_222222');
//滑动条是否显示
@Param isDividerVisible: boolean = true;
//更新
@Event changeFactory: (currentTabIndex: number, isShowDivider: boolean) => void = (currentTabIndex: number, isShowDivider: boolean) => {
}
2、自定义顶部视图,List 替换 tabBar 配合 Tabs 左视图–tabBar–右视图。
Column() {
//切换
this.customTabBar()
//下划线
Divider()
.color($r('app.color.color_F0F0F0'))
.visibility(this.isShowDivider ? Visibility.Visible : Visibility.None)
//TabContent中的tabBar居中显示,所以暂时不用tabBar
Tabs({ controller: this.tabsController, barPosition: BarPosition.Start }) {
ForEach(this.tabs, (item: TabBarModel, index: number) => {
TabContent() {
//滑到哪个页面再加载,防止一块加载
if (this.currentTabIndex === index || this.tabContentArr[index]) {
this.tabContentBuilder(item)
}
}
// .tabBar()
}, (item: string) => item)
}
.layoutWeight(1)
.barHeight(0) //隐藏tabBar
.scrollable(this.scrollable)
.onChange(index => {
this.tabContentArr[index] = true
this.changeFactory(index,this.tabs[index].isShowDivider)
})
}.width(CommonConst.FULL_PARENT)
.backgroundColor($r('app.color.white'))
3、 List实现【标题+横线】选中效果。
@Builder
customTabBar() {
Row() {
//左边自定义
this.tabBarLeft()
//中间
CustomTabBarComp({
currentTabIndex: this.currentTabIndex,
tabs: this.tabs,
selectFontColor: this.selectFontColor,
isTabBarCenter: this.isTabBarCenter,
onTabClick: (index: number) => {
this.tabsController.changeIndex(index)
},
isDividerVisible: this.isDividerVisible
})
//右边自定义
this.tabBarRight()
}
.width(CommonConst.FULL_PARENT)
.height(44)
}
4、标题+横线 List 和 TabContent.tabBar 都可以用。
@ComponentV2
export struct TabBarViewComp {
@Param private index: number = 0
@Param currentTabIndex: number = 0
@Param tabs: Array = new Array()
//选中字体颜色
@Param selectFontColor: ResourceColor = $r('app.color.color_222222');
@Param onTabClick: (index: number) => void = () => {
};
@Param isDividerVisible: boolean = true;
build() {
Column() {
//右上角图片
Image(this.tabs[this.index].rightSrc)
.height(11)
.margin({ left: 46 }).visibility(this.tabs[this.index].isShowRightSrc ? Visibility.Visible : Visibility.None)
Text(this.tabs[this.index].name)
.fontSize(this.currentTabIndex == this.index ? 16 : 14)
.fontColor(this.currentTabIndex == this.index ? this.selectFontColor : $r('app.color.color_505050'))
.fontWeight(this.currentTabIndex == this.index ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: this.tabs[this.index].isShowRightSrc ? 0 : 11 })
Divider()
.width(16)
.height(4)
.backgroundColor($r('app.color.colorPrimary'))
.margin({ top: 4, bottom: 4 })
.borderRadius(12)
.visibility(this.isDividerVisible && this.currentTabIndex == this.index ? Visibility.Visible : Visibility.Hidden)
}
.margin({ right: 15 })
.onClick(() => {
this.onTabClick(this.index)
})
}
}
5、完整代码如下
import { CommonConst } from "utils";
import { TabBarModel } from "../model/TabBarModel";
/**
* Author:J
* Describe:自定义tabBar 左视图--tabBar--右视图
*
* ListWithTabBarView({}) List替换tabBar 配合Tabs 左视图--tabBar--右视图
* CustomTabBarComp({}) List实现【标题+横线】选中效果
* TabBarViewComp({}) 标题+横线 List和TabContent.tabBar都可以用
*/
@Preview
@ComponentV2
export struct ListWithTabBarView {
@Param currentTabIndex: number = 0;
@Param tabContentArr: boolean[] = []; //存储页面状态
private tabsController: TabsController = new TabsController();
@Param tabs: Array = [];
//左边视图
@BuilderParam tabBarLeft: () => void = this.barLeft;
//右边视图
@BuilderParam tabBarRight: () => void = this.barRight;
//内容
@BuilderParam tabContentBuilder: ($$: TabBarModel) => void = this._TabContentBuilder;
//是否显示下划线
@Param isShowDivider: boolean = false;
//是否滑动
@Param scrollable: boolean = false;
//顶部中间视图是否居中 true居中 false 默认 居左
@Param isTabBarCenter: boolean = false;
//选中字体颜色
@Param selectFontColor: ResourceColor = $r('app.color.color_222222');
//滑动条是否显示
@Param isDividerVisible: boolean = true;
//更新
@Event changeFactory: (currentTabIndex: number, isShowDivider: boolean) => void = (currentTabIndex: number, isShowDivider: boolean) => {
}
aboutToAppear() {
for (let index = 0; index < this.tabs.length; index++) {
this.tabContentArr.push(index == 0 ? true : false)
}
}
build() {
Column() {
//切换
this.customTabBar()
//下划线
Divider()
.color($r('app.color.color_F0F0F0'))
.visibility(this.isShowDivider ? Visibility.Visible : Visibility.None)
//TabContent中的tabBar居中显示,所以暂时不用tabBar
Tabs({ controller: this.tabsController, barPosition: BarPosition.Start }) {
ForEach(this.tabs, (item: TabBarModel, index: number) => {
TabContent() {
//滑到哪个页面再加载,防止一块加载
if (this.currentTabIndex === index || this.tabContentArr[index]) {
this.tabContentBuilder(item)
}
}
// .tabBar()
}, (item: string) => item)
}
.layoutWeight(1)
.barHeight(0) //隐藏tabBar
.scrollable(this.scrollable)
.onChange(index => {
this.tabContentArr[index] = true
this.changeFactory(index,this.tabs[index].isShowDivider)
})
}.width(CommonConst.FULL_PARENT)
.backgroundColor($r('app.color.white'))
// .padding({ left: 12, right: 12 })
}
@Builder
_TabContentBuilder($$: TabBarModel) {
Text("tabContentBuilder:()=>{your @Builder View}")
}
@Builder
customTabBar() {
Row() {
//左边自定义
this.tabBarLeft()
//中间
CustomTabBarComp({
currentTabIndex: this.currentTabIndex,
tabs: this.tabs,
selectFontColor: this.selectFontColor,
isTabBarCenter: this.isTabBarCenter,
onTabClick: (index: number) => {
this.tabsController.changeIndex(index)
},
isDividerVisible: this.isDividerVisible
})
//右边自定义
this.tabBarRight()
}
.width(CommonConst.FULL_PARENT)
.height(44)
}
@Builder
barLeft() {
}
@Builder
barRight() {
}
}
@ComponentV2
export struct CustomTabBarComp {
@Param currentTabIndex: number = 0;
@Param tabs: Array = new Array()
//选中字体颜色
@Param selectFontColor: ResourceColor = $r('app.color.color_222222');
@Param onTabClick: (index: number) => void = () => {
};
@Param isTabBarCenter: boolean = false;
@Param isDividerVisible: boolean = true;
build() {
Row() {
List() {
ForEach(this.tabs, (item: TabBarModel, index: number) => {
ListItem() {
TabBarViewComp({
index: index,
currentTabIndex: this.currentTabIndex,
tabs: this.tabs,
selectFontColor: this.selectFontColor,
onTabClick: (index: number) => {
this.onTabClick(index)
},
isDividerVisible: this.isDividerVisible
})
}
})
}
// .width(Constants.FULL_PARENT)
.height(44)
.listDirection(Axis.Horizontal)
.alignListItem(ListItemAlign.Center)
.scrollBar(BarState.Off)
// .margin({ right: 8 })
}
.layoutWeight(1)
.justifyContent(this.isTabBarCenter ? FlexAlign.Center : FlexAlign.Start)
}
}
@ComponentV2
export struct TabBarViewComp {
@Param private index: number = 0
@Param currentTabIndex: number = 0
@Param tabs: Array = new Array()
//选中字体颜色
@Param selectFontColor: ResourceColor = $r('app.color.color_222222');
@Param onTabClick: (index: number) => void = () => {
};
@Param isDividerVisible: boolean = true;
build() {
Column() {
//右上角图片
Image(this.tabs[this.index].rightSrc)
.height(11)
.margin({ left: 46 }).visibility(this.tabs[this.index].isShowRightSrc ? Visibility.Visible : Visibility.None)
Text(this.tabs[this.index].name)
.fontSize(this.currentTabIndex == this.index ? 16 : 14)
.fontColor(this.currentTabIndex == this.index ? this.selectFontColor : $r('app.color.color_505050'))
.fontWeight(this.currentTabIndex == this.index ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: this.tabs[this.index].isShowRightSrc ? 0 : 11 })
Divider()
.width(16)
.height(4)
.backgroundColor($r('app.color.colorPrimary'))
.margin({ top: 4, bottom: 4 })
.borderRadius(12)
.visibility(this.isDividerVisible && this.currentTabIndex == this.index ? Visibility.Visible : Visibility.Hidden)
}
.margin({ right: 15 })
.onClick(() => {
this.onTabClick(this.index)
})
}
}
6、使用案例如下,多种样式的使用:
import { ToastUtil } from '@pura/harmony-utils'
import { ListWithTabBarView, TabBarModel } from 'uicomponents'
import { CommonConst } from 'utils'
import { GetFourStyleTabData, GetOneStyleTabData, GetThreeStyleTabData, GetTwoStyleTabData } from './TestModel'
@Preview
@ComponentV2
export struct TestTabBarView {
@Local currentTabIndex: number = 0
@Local isShowDivider: boolean = GetOneStyleTabData[0].isShowDivider
@Local currentTabIndex2: number = 0
@Local currentTabIndex3: number = 0
@Local currentTabIndex4: number = 0
build() {
NavDestination() {
Column() {
Text('第一种样式').text(Color.Red)
ListWithTabBarView({
currentTabIndex: this.currentTabIndex,
tabs: GetOneStyleTabData,
tabBarLeft: this.tabBarLeft,
tabBarRight: this.tabBarRight,
tabContentBuilder: this.tabContentBuilder,
isShowDivider: this.isShowDivider,
changeFactory: (currentTabIndex, isShowDivider) => {
this.currentTabIndex = currentTabIndex
this.isShowDivider = isShowDivider
}
}).height(80)
Text('第二种样式').text(Color.Pink)
ListWithTabBarView({
currentTabIndex: this.currentTabIndex2,
tabs: GetTwoStyleTabData,
tabBarLeft: this.tabBarLeft2,
tabBarRight: this.tabBarRight2,
tabContentBuilder: this.tabContentBuilder,
changeFactory: (currentTabIndex) => {
this.currentTabIndex2 = currentTabIndex
},
isTabBarCenter: true
}).height(80)
Text('第三种样式').text(Color.Blue)
ListWithTabBarView({
currentTabIndex: this.currentTabIndex,
tabs: GetThreeStyleTabData,
tabBarLeft: this.tabBarLeft3,
tabBarRight: this.tabBarRight3,
tabContentBuilder: this.tabContentBuilder,
isTabBarCenter: true,
changeFactory: (currentTabIndex) => {
this.currentTabIndex = currentTabIndex
}
}).height(80)
Text('第四种样式').text(Color.Grey)
ListWithTabBarView({
currentTabIndex: this.currentTabIndex4,
tabs: GetFourStyleTabData,
tabBarLeft: this.tabBarLeft4,
tabContentBuilder: (tab): void => this.tabContentBuilder(tab),
isShowDivider: true,
changeFactory: (currentTabIndex) => {
this.currentTabIndex4 = currentTabIndex
}
}).layoutWeight(1)
.height(80)
}
.width(CommonConst.FULL_PARENT)
.height(CommonConst.FULL_PARENT)
}
}
@Builder
tabBarLeft() {
Text().width(12)
}
@Builder
tabBarRight() {
Row({ space: 12 }) {
Image($r('app.media.app_icon'))
.width(24)
.visibility(this.currentTabIndex == 0 || this.currentTabIndex == 1 ? Visibility.Visible : Visibility.Hidden)
.onClick(() => {
ToastUtil.showToast('点了1')
})
Image($r('app.media.app_icon')).width(24).onClick(() => {
ToastUtil.showToast('点了')
})
}.padding({ right: 12 })
}
@Builder
tabContentBuilder($$: TabBarModel) {
Text($$.id)
}
@Builder
tabBarLeft2() {
Image($r('app.media.ic_arrow_left')).width(24)
.margin({ left: 12, right: 6 })
.onClick(() => {
ToastUtil.showToast('返回键')
// ZRouter.pop()
})
}
@Builder
tabBarRight2() {
Image($r('app.media.app_icon'))
.width(24)
.margin({ right: 12 })
.onClick(() => {
ToastUtil.showToast('点了')
})
}
@Builder
tabBarLeft3() {
Image($r('app.media.app_icon')).width(24).fillColor(Color.Black)
.margin({ left: 12, right: 20 })
.onClick(() => {
ToastUtil.showToast('设置')
})
}
@Builder
tabBarRight3() {
Image(this.currentTabIndex == 1 ? $r('app.media.ic_next') : $r('app.media.ic_local_search'))
.width(24)
.margin({ right: 12 })
.visibility(this.currentTabIndex != 2 ? Visibility.Visible : Visibility.Hidden)
.onClick(() => {
ToastUtil.showToast(this.currentTabIndex == 1 ? '点了1' : '搜索')
})
}
@Builder
tabBarLeft4() {
Text().width(12)
}
}
@Extend(Text)
function text(color: ResourceColor) {
.height(44).width(CommonConst.FULL_PARENT).fontColor(Color.White).backgroundColor(color)
}
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注