HarmonyOS 鸿蒙Next 本地数据库实现《菜谱大全》app(二)
HarmonyOS 鸿蒙Next 本地数据库实现《菜谱大全》app(二)
有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html
关于HarmonyOS 鸿蒙Next 本地数据库实现《菜谱大全》app(二)的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。
本地数据库实现菜谱大全app主框架已经写好,那么接下来就是各个页面之间的代码编写了。
这里我们使用Navigation的动态路由功能进行各个页面之间的跳转。
1.获取本地数据库实例
export async function getRdbStore(context: Context, onGetStore: (store: relationalStore.RdbStore | undefined) => void) {
relationalStore.getRdbStore(context, {
name: Constant.DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1
}, (err, store) => {
if (err) {
console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
return;
} else {
console.info(`Succeeded in getting RdbStore.`);
onGetStore(store)
console.log("=======version:" + store.version)
}
})
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
2.创建一个moudule作为动态路由的管理
import { logger } from '[@ohos](/user/ohos)/base/Index';
import { RouterInfo } from '../constants/RouterInfo';
/**
- 动态路由
- 实现步骤:
- 1.将主模块的NavPathStack传入createRouter接口,注册路由
- 2.通过registerBuilder接口,将需要动态加载的模块注册到路由中
- 3.通过push接口,跳转到指定的模块页面
*/
const LOGGER_TAG: string = 'Dynamics import failed , reason : ';
export class DynamicsRouter {
// 管理需要动态导入的模块,key是模块名,value是WrappedBuilder对象,动态调用创建页面的接口
static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>();
static navPathStack: NavPathStack = new NavPathStack();
// 通过数组实现自定义栈的管理
static routerStack: Array<RouterInfo> = new Array();
static referrer: string[] = [];
// 通过名称注册builder
private static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void {
DynamicsRouter.builderMap.set(builderName, builder);
}
// 通过名称获取builder
public static getBuilder(builderName: string): WrappedBuilder<[object]> {
const builder = DynamicsRouter.builderMap.get(builderName);
if (!builder) {
const MSG = “not found builder”;
logger.info(MSG + builderName);
}
return builder as WrappedBuilder<[object]>;
}
// 注册router
public static createNavPathStack(navPathStack: NavPathStack): void {
DynamicsRouter.navPathStack = navPathStack;
// 初始化时来源页为未定义
DynamicsRouter.routerStack.push(RouterInfo.HOME_PAGE)
logger.info(DynamicsRouter create routerStack Home is: ${RouterInfo.HOME_PAGE.moduleName} + ${RouterInfo.HOME_PAGE.pageName}
);
}
// 通过名称获取router
private static getNavPathStack(): NavPathStack {
return DynamicsRouter.navPathStack;
}
// 获取路由来源页面栈
public static getRouterReferrer(): string[] {
return DynamicsRouter.referrer;
}
// 通过获取页面栈跳转到指定页面
public static async push(routerInfo: RouterInfo, param?: ESObject): Promise<void> {
const pageName: string = routerInfo.pageName;
const moduleName: string = routerInfo.moduleName;
const FullScreenArray: string[] = [’@ohos/foldablescreencases’];
// 动态加载模块是否成功
let isImportSucceed: boolean = false;
// 模块是否需要转场动画
let isNeedFullScreen: boolean = true;
// TODO:知识点:通过动态import的方式引入模块,在需要进入页面时才加载模块,可以减少主页面的初始化时间及占用的内存
await import(moduleName).then((result: ESObject) => {
// 动态加载模块成功时,通过模块中的harInit接口加载页面
result.harInit(pageName);
isImportSucceed = true;
if (FullScreenArray.includes(moduleName)) {
isNeedFullScreen = false;
}
}, (error: ESObject) => {
// 动态加载模块失败时,打印错误日志
logger.error(LOGGER_TAG, error);
});
if (isImportSucceed) {
// 使用moduleName和pageName生成builderName,通过pushPath加载页面
const builderName: string = moduleName + “/” + pageName;
// 查找到对应的路由栈进行跳转
DynamicsRouter.getNavPathStack().pushPath({ name: builderName, param: param }, isNeedFullScreen);
// 自定义栈也加入指定页面
DynamicsRouter.routerStack.push(routerInfo);
const referrerModel: RouterInfo = DynamicsRouter.routerStack[DynamicsRouter.routerStack.length - 2];
DynamicsRouter.referrer[0] = referrerModel.moduleName;
DynamicsRouter.referrer[1] = referrerModel.pageName;
logger.info(From DynamicsRouter.routerStack push preview module name is + ${DynamicsRouter.referrer[<span class="hljs-number"><span class="hljs-number">0</span></span>]}, path is ${DynamicsRouter.referrer[<span class="hljs-number"><span class="hljs-number">1</span></span>]}
);
}
}
// 通过获取页面栈并pop
public static pop(): void {
// pop前记录的来源页为当前栈顶
const referrerModel: RouterInfo = DynamicsRouter.routerStack[DynamicsRouter.routerStack.length - 1];
DynamicsRouter.referrer[0] = referrerModel.moduleName;
DynamicsRouter.referrer[1] = referrerModel.pageName;
logger.info(From DynamicsRouter.routerStack pop preview module name is + ${DynamicsRouter.referrer[<span class="hljs-number"><span class="hljs-number">0</span></span>]}, path is ${DynamicsRouter.referrer[<span class="hljs-number"><span class="hljs-number">1</span></span>]}
);
if (DynamicsRouter.routerStack.length > 1) {
DynamicsRouter.routerStack.pop();
} else {
logger.info(“DynamicsRouter.routerStack is only Home.”);
}
// 查找到对应的路由栈进行pop
DynamicsRouter.getNavPathStack().pop();
}
// 通过获取页面栈并将其清空
public static clear(): void {
// 查找到对应的路由栈进行pop
DynamicsRouter.getNavPathStack().clear();
}
/**
- 注册动态路由需要加载的页面
- @param pageName 页面名,需要动态加载的页面名称
@param wrapBuilder wrapBuilder对象 */ public static registerRouterPage(routerInfo: RouterInfo, wrapBuilder: WrappedBuilder<[object]>): void { const builderName: string = routerInfo.moduleName + “/” + routerInfo.pageName; if (!DynamicsRouter.getBuilder(builderName)) { DynamicsRouter.registerBuilder(builderName, wrapBuilder); } } }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
// 动态路由中需要加载的模块名和模块路径 export class RouterInfo { moduleName: string = ‘’; pageName: string = ‘’;
constructor(moduleName: string, pageName: string) { this.moduleName = moduleName; this.pageName = pageName; } static readonly SEARCH: RouterInfo = new RouterInfo(’@ohos/food’, ‘SearchView’) static readonly FOOD_DETAIL: RouterInfo = new RouterInfo(’@ohos/food’, ‘FooDetailView’); static readonly COOK_METHOD: RouterInfo = new RouterInfo(’@ohos/food’, ‘CookMethodView’); static readonly TASTES: RouterInfo = new RouterInfo(’@ohos/food’, ‘TastesView’); static readonly FOOD_LIST: RouterInfo = new RouterInfo(’@ohos/food’, ‘FoodListPage’); static readonly COLLECT_LIST: RouterInfo = new RouterInfo(’@ohos/food’, ‘CollectListPage’); }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
3.创建一个新的module作为菜谱各个页面的展示
4.entry依赖
将新建的moudle添加到entry主模块的依赖中去
{
“name”: “entry”,
“version”: “1.0.0”,
“description”: “Please describe the basic information.”,
“main”: “”,
“author”: “”,
“license”: “”,
“dependencies”: {
“@ohos/base”: “file:…/…/common/utils”,
// 动态路由模块
“@ohos/routermodule”: “file:…/…/feature/routermodule”,
//菜谱相关页面
“@ohos/food”: “file:…/…/feature/food”,
“@ohos/account”: “file:…/…/feature/account”
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
5.app主页面代码
主页面有四部分组成,TitleBar、选项卡、搜索框、瀑布流
import { MenuBean, menuModel, TitleBar } from ‘@ohos/base/Index’ import { relationalStore } from ‘@kit.ArkData’ import { Constant } from ‘@ohos/base/src/main/ets/constant/Constant’ import { Food } from ‘@ohos/base/src/main/ets/model/Food’ import { DynamicsRouter, RouterInfo } from ‘@ohos/routermodule/Index’
@Component export struct MenuView { @Consume store: relationalStore.RdbStore @State menus: Array<MenuBean> = [new MenuBean(“烹饪方法”, $r(“app.media.menu_method”)), new MenuBean(“口味”, $r(“app.media.menu_taste”))] @State foods: Array<Food> = new Array<Food>()
getFoods() { menuModel.getFoods(this.store, (res: Array<Food>) => { if (res !== undefined && res.length > 0) { this.foods = this.foods.concat(res) } }) }
aboutToAppear(): void { setTimeout(() => { this.getFoods() }, 1000)
}
build() { Column() { TitleBar({ isShowBackButton: false, title: “烹饪” }) Scroll() { Column() { this.menuBuilder() this.searchBuilder() this.FoodListViewBuilder() }
} .layoutWeight(<span class="hljs-number"><span class="hljs-number">1</span></span>) .align(Alignment.TopStart) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .onReachEnd(() => { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.getFoods() }) }.height(<span class="hljs-string"><span class="hljs-string">"100%"</span></span>) .backgroundColor($r(<span class="hljs-string"><span class="hljs-string">"app.color.white"</span></span>))
}
@Builder FoodListViewBuilder() { Column() { WaterFlow() { ForEach(this.foods, (item: Food, index: number) => { FlowItem() { this.foodItem(item) } }) } .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) .columnsTemplate(“1fr 1fr”) .columnsGap(10) .rowsGap(10) .padding({ left: 15, right: 15, top: 10 })
}
}
@Builder foodItem(food: Food) { Column() { Image(food.img) .borderRadius(10) .objectFit(ImageFit.Contain) Text(food.name) .margin({ top: 8 })
}.alignItems(HorizontalAlign.Start) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_SCALE }) .onClick(() => { DynamicsRouter.push(RouterInfo.FOOD_DETAIL, { foodId: food.meauID }) })
}
@Builder searchBuilder() { Row() { Text(“搜索美食”) .margin({ left: 15, right: 15 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_SCALE }) .fontSize($r(“app.float.middle_text_size”)) .fontColor($r(“app.color._999999”)) .borderRadius(90) .backgroundColor($r(“app.color.window_bg”)) .padding(15) .layoutWeight(1) .onClick(() => { DynamicsRouter.push(RouterInfo.SEARCH) }) }.width(“100%”)
}
@Builder menuBuilder() { Flex({ direction: FlexDirection.Row }) { ForEach(this.menus, (e: MenuBean) => { this.menuItemBuilder(e) }) } .backgroundColor(Color.White) .margin({ left: 10, right: 10, top: 15, bottom: 15 })
}
@Builder menuItemBuilder(menuBean: MenuBean) { Column() { Image(menuBean.imgRes) .width(30) .height(30) Text(menuBean.title) .margin({ top: 5 }) .fontColor($r(“app.color.common_text”)) .fontSize($r(“app.float.middle_text_size”)) } .width(“50%”) .borderRadius(10) .margin({ left: 5, right: 5 }) .padding(10) .shadow({ radius: 3, type: ShadowType.COLOR, color: $r(“app.color.divider_color”) }) .padding({ top: 10, bottom: 10 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_SCALE }) .alignItems(HorizontalAlign.Center) .onClick(() => { if (menuBean.title === “烹饪方法”) { DynamicsRouter.push(RouterInfo.COOK_METHOD) } else if (menuBean.title === “口味”) { DynamicsRouter.push(RouterInfo.TASTES) } else if (menuBean.title === “初级入门”) {
} <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (menuBean.title === <span class="hljs-string"><span class="hljs-string">"厨神驾到"</span></span>) { } })
} }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
TItleBar
import { Constant } from ‘…/…/constant/Constant’
@Component @Preview export struct TitleBar { @State title: string = “” @Prop rightTitle: string = “” @Consume(‘pageStack’) pageStack: NavPathStack OnClickRightTitle?: (msg:string) => void @State isShowBackButton?:boolean = true
build() { Column() { Row() { Row() { Image($r(“app.media.come_back”)) .width(20) .height(20) .margin({ left: $r(“app.float.common_margin”) }) Text($r(“app.string.back”)) .fontColor($r(“app.color.white”)) .fontSize($r(“app.float.big_text_size”)) .layoutWeight(1) }.layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_ICON_SCALE })//点击效果 .onClick(() => { // router.back() this.pageStack.pop() // DynamicsRouter.pop() }) .visibility(this.isShowBackButton?Visibility.Visible:Visibility.Hidden)
Row() { Text(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.title) .fontSize($r(<span class="hljs-string"><span class="hljs-string">"app.float.titleBar_text_size"</span></span>))<span class="hljs-comment"><span class="hljs-comment">// .fontWeight(FontWeight.Bold)</span></span> .maxLines(<span class="hljs-number"><span class="hljs-number">1</span></span>) .textOverflow({ overflow: TextOverflow.Ellipsis }) }.layoutWeight(<span class="hljs-number"><span class="hljs-number">1</span></span>) .justifyContent(FlexAlign.Center) Row() { Text(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.rightTitle) .fontColor($r(<span class="hljs-string"><span class="hljs-string">"app.color.primary"</span></span>)) .margin({ right: $r(<span class="hljs-string"><span class="hljs-string">"app.float.common_margin"</span></span>) }) }.layoutWeight(<span class="hljs-number"><span class="hljs-number">1</span></span>) .justifyContent(FlexAlign.End) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_ICON_SCALE }) .onClick(()=>{ <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.OnClickRightTitle&&<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.OnClickRightTitle(<span class="hljs-string"><span class="hljs-string">""</span></span>) }) }.width(<span class="hljs-string"><span class="hljs-string">"100%"</span></span>) .height(Constant.TITLE_BAR_HEIGHT) .backgroundColor(Color.White) Divider() }
} }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
6.初级和高级Tab页
初级和高级类似使用的是相同的页面结构
import { Constant } from ‘@ohos/base/src/main/ets/constant/Constant’ import { FoodListView } from ‘@ohos/food/src/main/ets/widget/FoodListView’
@Component export struct ChujiView { build() { Column() { Tabs() { TabContent() { Column() { FoodListView({ level: “新手尝试” }) } } .tabBar(this.tabBuilder(“新手”, 0))
TabContent() { Column() { FoodListView({ level: <span class="hljs-string"><span class="hljs-string">"初级入门"</span></span> }) } } .tabBar(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.tabBuilder(<span class="hljs-string"><span class="hljs-string">"初级"</span></span>, <span class="hljs-number"><span class="hljs-number">1</span></span>)) TabContent() { Column() { FoodListView({ level: <span class="hljs-string"><span class="hljs-string">"初中水平"</span></span> }) } } .tabBar(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.tabBuilder(<span class="hljs-string"><span class="hljs-string">"初中"</span></span>, <span class="hljs-number"><span class="hljs-number">2</span></span>)) } .barMode(BarMode.Scrollable) .onChange((index: number) => { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.currentIndex = index }) .onTabBarClick((index: number) => { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.currentIndex = index }) }.height(<span class="hljs-string"><span class="hljs-string">"100%"</span></span>)
}
@State currentIndex: number = 0
@Builder tabBuilder(title: string, targetIndex: number) { Column() { Text(title) .fontColor(this.currentIndex === targetIndex ? Color.White : $r(“app.color.primary”)) .backgroundColor(this.currentIndex === targetIndex ? $r(“app.color.primary”) : Color.White) .width(60) .height(32) .textAlign(TextAlign.Center) .border({ width: 1, color: $r(“app.color.primary”) }) .fontSize($r(“app.float.common_margin”)) .borderRadius({ topLeft: targetIndex == 0 ? 5 : 0, bottomLeft: targetIndex == 0 ? 5 : 0, topRight: targetIndex == 2 ? 5 : 0, bottomRight: targetIndex == 2 ? 5 : 0 }) .margin(0) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: Constant.CLICK_SCALE }) } } }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
列表页
import { menuModel } from ‘@ohos/base/src/main/ets/db/MenuModel’ import { Food } from ‘@ohos/base/src/main/ets/model/Food’ import { relationalStore } from ‘@kit.ArkData’ import { DynamicsRouter, RouterInfo } from ‘@ohos/routermodule/Index’ @Component export struct FoodListView { @State foods: Array<Food> = new Array() @State method: string | undefined = undefined @State taste: string | undefined = undefined @State level:string|undefined= undefined @Consume store: relationalStore.RdbStore @State limit: number = 10 @State skip: number = 0 @State isNoMore: boolean = false
getFoods() { if (!this.isNoMore) { if (this.method) { menuModel.getFoodsByMethod(this.store, this.method, (res: Array<Food>) => { if (res && res.length > 0) { this.foods = this.foods.concat(res) this.skip = this.skip + this.limit }
}, () => { }, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip) } <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.taste) { menuModel.getFoodsByTaste(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.store, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.taste, (res: <span class="hljs-built_in"><span class="hljs-built_in">Array</span></span><Food>) => { <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (res && res.length > <span class="hljs-number"><span class="hljs-number">0</span></span>) { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.foods = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.foods.concat(res) <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip + <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit } }, () => { }, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip) }<span class="hljs-keyword"><span class="hljs-keyword">else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.level) { menuModel.getFoodsByLevel(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.store, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.level, (res: <span class="hljs-built_in"><span class="hljs-built_in">Array</span></span><Food>) => { <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (res && res.length > <span class="hljs-number"><span class="hljs-number">0</span></span>) { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.foods = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.foods.concat(res) <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip + <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit } }, () => { }, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit, <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.skip) } }
}
aboutToAppear(): void { this.getFoods() }
build() { Column() { List() { ForEach(this.foods, (f: Food) => { ListItem() { Row() { Image(f.img) .width(100) .height(80) .borderRadius(10) Column() { Text(f.name) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(f.taste ? f.taste : “” + " " + f.peopleNum ? f.peopleNum : “”) .fontColor($r(“app.color._999999”)) .fontSize($r(“app.float.small_text_size”)) Text(“准备” + f.prepareTime + " 烹饪" + f.cookTime) .fontColor($r(“app.color._999999”)) .fontSize($r(“app.float.small_text_size”)) } .margin({ left: 10 }) .height(80) .alignItems(HorizontalAlign.Start) .layoutWeight(1) .justifyContent(FlexAlign.SpaceAround)
}.backgroundColor(Color.White) .borderRadius(<span class="hljs-number"><span class="hljs-number">10</span></span>) .padding(<span class="hljs-number"><span class="hljs-number">10</span></span>) .margin({ left: <span class="hljs-number"><span class="hljs-number">15</span></span>, right: <span class="hljs-number"><span class="hljs-number">15</span></span>, top: <span class="hljs-number"><span class="hljs-number">10</span></span> }) }.onClick(() => { DynamicsRouter.push(RouterInfo.FOOD_DETAIL, { foodId: f.meauID }) }) }) }.scrollBar(BarState.Off) .onReachEnd(() => { <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.foods.length >= <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.limit) { <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.getFoods() } }) .layoutWeight(<span class="hljs-number"><span class="hljs-number">1</span></span>) }.height(<span class="hljs-string"><span class="hljs-string">"100%"</span></span>) .backgroundColor($r(<span class="hljs-string"><span class="hljs-string">"app.color.window_bg"</span></span>))
} }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
更多关于HarmonyOS 鸿蒙Next 本地数据库实现《菜谱大全》app(二)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html