Loading

工程实践作业——生意专家

1 第一周 开发环境搭建

1.1 第一步:下载安装Node.js

安装过程略,检查是否安装成功,可以执行下面的命令:

node -v
npm -v

出现版本号提示表示安装成功。

1.2 第二步:安装cnpm

cnpm是淘宝 NPM 镜像。进入命令提示符(cmd),执行下面的命令:

npm install -g cnpm --registry=https://registry.npm.taobao.org

检查是否安装成功可以执行下面命令:

cnpm -v

1.3 第三步:安装cordova

cnpm install -g cordova

检查有没有安装成功可以执行命令:

cordova -v

1.4 第四步:安装Ionic CLI

cnpm install -g @ionic/cli

检查有没有安装成功可以执行命令:

ionic -v

1.5 第五步:安装jdk和android sdk

1.6 第六步:创建Ionic工程

ionic start shengyizhuanjia tabs --type=angular --no-deps

老师改需求了,sidemenus换成tabs

命令执行成功后,进入项目的根目录,执行命令:

cnpm install

1.7 第七步:运行Ionic工程

在命令提示符中(cmd)进入项目的根目录,执行下面的命令:

ionic serve

命令执行成功后,会自动打开默认的浏览器(建议使用谷歌浏览器),默认网址:http://localhost:8100/
按F12打开开发者工具,模拟手机设备。

1.7.1 构建Android应用程序

添加Android平台

ionic cordova platform add android

编译

ionic cordova build android

完成后生成shengyizhuanjia\platforms\android\app\build\outputs\apk\debug\app-debug.apk

1.8 第八步:制作App图标和启动屏幕

在项目的目录找到resources文件夹。在文件夹中都放入icon.png(应用图标,最小1024x1024px,不带圆角),splash.png(启动屏幕,最小2732x2732px)(可以是png、psd、ai) 在cmd中进入项目所在文件夹执行:

ionic cordova resources  

执行该命令后,会自动在resources文件夹下创建已添加的平台名称的文件夹,如:android,其中会自动将图片进行缩放、裁剪,生成不同分辨率的图片,并在config.xml中添加相应内容。

注意最好选择正确的分辨率,不然容易报错。分辨率可以用画图软件打开图片查看并修改。

若提示缺少模块,安装相应模块即可

npm install --save module_name(你的module名称)

2 第二周 欢迎页的实现

2.1 创建欢迎页组件

在src\app目录下创建pages文件夹,在命令符号(cmd)下,进入项目的根目录执行下面的命令:

ionic generate page pages/guide

学号尾号单数pages双数guides
welcome改成guide

该命令会在src\app\routes目录中自动生成以下几个文件。

文件名 说明
guide.page.html HTML模板
guide.module.ts 模块
guide.page.scss 私有的样式表,app-welcome{}是一个元素选择器,名称和welcome.page.ts文件中元数据的选择器是一致的, selector: 'app-welcome'。相当于有一个自定义的元素<app-welcome></app-welcome>
guide.page.ts 组件的类(class)代码
guide.routing.ts 路由模块文件
guide.module>ts 模块文件

2.2 将欢迎页设置成默认页

修改app-routing.module.ts文件。
src\app\app-routing.module.ts

const routes: Routes = [
  {
    path: '',
    redirectTo: 'guide',
    pathMatch: 'full'
  },
  {
    path: 'guide',
    loadChildren: () => import('./pages/guide/guide.module').then( m => m.GuidePageModule)
  }
];

2.3 为界面添加轮播

修改HTML模板文件,为<ion-content>元素添加<ion-slides>子元素。

\src\app\pages\guide\guide.page.html

<ion-header class="ion-no-border">
</ion-header>

<ion-content>
  <ion-slides #slides pager="true" (ionSlideWillChange)="onSlideWillChange($event)" style="height: 100%;">
    <ion-slide>
      <img src="/assets/img/splsh_one.png" alt="">
    </ion-slide>
    <ion-slide>
      <img src="./assets/img/splsh_two.png" alt="">
    </ion-slide>
    <ion-slide>
      <img src="assets/img/splsh_three.png">
    </ion-slide>
  </ion-slides>
</ion-content>

ion-content:内容组件提供了易于使用的内容区域。
ion-slides:幻灯片(轮播、旋转木马)组件是个多节容器。每个部分都可以在其间滑动或拖动。它包含任意数量的Slide组件。
ion-slide:滑动组件是Slides的子组件。任何幻灯片内容都应该写在此组件中,并且应该与幻灯片一起使用。

2.4 添加跳过按钮

在模板文件中添加按钮组件。
/src/app/pages/guide/guide.page.html

<ion-header class="ion-no-border">
  <ion-toolbar>
    <ion-buttons slot="end">
      <ion-button color="primary">跳过</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

ion-header:标题组件是包含工具栏组件的父级组件。注意:ion-header必须是页面的三个根元素之一(ion-content,ion-footer)。
ion-toolbar:工具栏组件
ion-buttons:按钮组组件,用于存放1个或者多个按钮。
ion-button:按钮组件
借助标题等组件,可以使用Ionic提供的默认样式,帮助我们快速定义好按钮的外观及位置。但是正常的欢迎页面是不出现标题栏的,可以通过设置css中的background和bordy-color两个属性为透明,“隐藏”标题栏。

设置工具栏透明。(已失效)
/src/app/pages/guide/guide.scss

    ion-toolbar {
      --background: transparent;
      --border-color: transparent;
    }

在组件类中修改装饰器,添加encapsulation元数据,提供模板和 CSS 样式使用的样式封装策略。
/src/app/pages/guide/guide.ts

@Component({
  selector: 'app-guide',
  templateUrl: './guide.page.html',
  styleUrls: ['./guide.page.scss'],
  encapsulation: ViewEncapsulation.None
})

2.5 控制“跳过”按钮的显示或隐藏

在组件类中添加showSkip属性控制跳过按钮的显示或者隐藏。

当showSkip值为true时,显示“跳过”按钮,当showSkip值为false时,隐藏“跳过”按钮。 /src/app/pages/guide/guide.ts

showSkip = true;

设置元素hidden属性的绑定。
/src/app/pages/guide/guide.page.html

<ion-button color="primary" [hidden]="!showSkip">跳过</ion-button>

利用slides的事件控制showSkip的值。

为组件类添加onSlideWillChange方法。
/src/app/pages/guide/guide.ts

@ViewChild('slides', {static: false}) slides: any;
onSlideWillChange(event) {
    console.log(event);
    this.slides.isEnd().then((end) => {
      this.showSkip = !end;
    });
  }

在模板中实现事件绑定。
/src/app/pages/guide/guide.html

<ion-slides #slides pager="true" (ionSlideWillChange)="onSlideWillChange($event)">

2.6 添加登录和注册按钮

在第三个幻灯片中添加登录和注册两个按钮,并且把这两个按钮固定在界面的底部。

添加.fixed-bottom样式。
/src/app/pages/guide/guide.scss

    .fixed-bottom{
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 10;
    }

在guide.html文件中添加登录和注册按钮

修改ion-slides元素中的第三个ion-slide元素。
/src/app/pages/guide/guide.html

    <ion-slide>
      <img src="assets/img/splsh_three.png">
      <ion-grid class="fixed-bottom">
        <ion-row>
          <ion-col>
            <ion-button color="primary" fill="outline" expand="block">登录</ion-button>
          </ion-col>
          <ion-col>
            <ion-button color="primary" expand="block">注册</ion-button>
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-slide>

3 第三周 程序第一次运行的实现

3.1 创建shared模块

ionic g module shared

命令执行后,会在 src/app/shared/目录中创建shared.module.ts文件。

在AppModule(应用程序根模块)中,修改@NgModule的参数(元数据对象)的imports属性,导入SharedModule。
src\app\app.module.ts

  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    SharedModule // 添加的代码
  ],

自动生成下面的代码

import { SharedModule } from './shared/shared.module';

3.2 创建本地存储的服务

在命令符号(cmd)中,切换到项目根目录中,执行命令:

ionic g service shared/services/LocalStorage

以上命令会在src\app\shared目录中创建services文件夹,并创建local-storage.ts

为LocalStorageService添加一个属性storage。
src\app\shared\services\local-storage.service.ts

private storage: any = window.localStorage; // 创建一个名为storage的变量,类型是any,值是window.localStorage

3.2.1 从本地获取数据

添加一个名叫get方法,根据key获取数据,如果key不存在返回默认值。

src\app\shared\services\local-storage.service.ts

  get(key: string, defaultValue: any): any { // 创建一个名为get的方法,根据key获取数据,如果key不存在返回默认值。
    let value: any = this.storage.getItem(key); // let相当于更完美的var
    try{
      value = JSON.parse(value);
    } catch (error) {
      value = null;
    }
    if (value === null && defaultValue) {
      value = defaultValue;
    }
    return value;
  }

3.2.2 添加或修改本地存储中的数据

添加一个名叫set方法,根据key设置数据。如果key不存在相当于添加操作,如果key存在相当于修改操作。
src\app\shared\services\local-storage.service.ts

  set(key: string, value: any) { // 添加一个名叫set方法,根据key设置数据。如果key不存在相当于添加操作,如果key存在相当于修改操作。
    this.storage.setItem(key, JSON.stringify(value));
  }

3.2.3 删除本地存储中的数据

添加一个名叫remove方法。
src\app\shared\services\local-storage.service.ts

  remove(key: string) { // 添加一个名叫remove方法。
    this.storage.removeItem(key);
  }

3.3 使用本地存储保存程序运行状态

3.3.1 注册服务器供应商

修改@NgModule元数据的providers属性,为数组添加LocalStorageService成员。
src\app\app.module.ts

  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    LocalStorageService // 添加的代码
  ],

3.3.2 依赖注入

在构造函数中依赖注入LocalStorageService。
src\app\pages\welcome\welcome.page.ts

constructor(private localStorageService:LocalStorageService) {}// 在构造函数中依赖注入LocalStorageService

3.3.3 利用本地存储判断程序是否是第一次运行

在组件类中添加ionViewWillEnter方法,从本地存储中获取之前保存的App数据,根据属性hssRun判断程序是否是第一次运行。如果值为真表示第一次运行,否则表示程序已经运行过,使用Router来跳转页面。 在构造函数中添加Router的依赖注入。
src\app\pages\welcome\welcome.page.ts

 constructor(private localStorageService: LocalStorageService, private router: Router) {} 

Angular的路由器(Router)能够从一个页面导航到另外一个页面。
src\app\pages\welcome\welcome.page.ts

export const APP_KEY: string = 'App';
ngOnInit() {
    // 第一次调用get方法时,'App'这个key不存在,第二个参数会作为默认值返回
    let appConfig: any = this.localStorageService.get(APP_KEY, { // 调用local-storage.service.ts里写的get方法,传入两个参数
      isLaunched: false,
      version: '1.0.0'
    });
    if ( appConfig.isLaunched === false ) { // 如果是第一次启动
      appConfig.isLaunched = true;
      this.localStorageService.set(APP_KEY, appConfig); // 在本地内存中添加
    } else { // 不是第一次启动
      this.router.navigateByUrl('home'); // 路由到
    }
  }
  onSlideWillChange(event) {
    console.log(event);
    this.slides.isEnd().then((end) => {
      this.showSkip = !end;
    });
  }

3.4 Angular路由守护

上面的代码虽然实现了应用程序第一次运行和非第一次运行时页面的跳转,但是在测试时存在着一个Bug,非第一次运行时向导页面会一闪而过。接下来使用Angular路由守护解决这个问题。

通过Angular的路由守护当用户满足一定条件才被允许进入或者离开一个路由。

路由守卫场景:

  • 只有当用户登录并拥有某些权限的时候才能进入某些路由。
  • 当用户未执行保存操作而试图离开当前导航时提醒用户。

Angular提供了一些钩子帮助控制进入或离开路由。这些钩子就是路由守卫,可以通过这些钩子实现上面场景。

CanActivate: 处理导航到某路由的情况。
CanDeactivate: 处理从当前路由离开的情况。
Resolve: 在路由激活之前获取路由数据。

配置路由时候用到一些属性,path、component、outlet、 children,路由守卫也是路由属性。

3.4.1 创建守卫

参考之前的任务文档创建core module,在命令行中输入如下命令创建守卫:

ionic g guard core/StartApp

3.4.2 编写守卫逻辑

src\app\core\start-app.guard.ts

export class StartAppGuard implements CanActivate {
  constructor(private localStorageService: LocalStorageService, private router: Router) { }
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const appConfig: any = this.localStorageService.get(APP_KEY, {
      isLaunched: false,
      version: '1.0.0'
    });
    if ( appConfig.isLaunched === false ) {
      appConfig.isLaunched = true;
      this.localStorageService.set(APP_KEY, appConfig);
      return true;
    } else {
      this.router.navigateByUrl('folder/Inbox');
      return false;
    }
  }

}

3.4.3 配置路由守卫

参考之前的任务在AppModule中导入CoreModule。
在路由模块中配置路由守护。
src\app\app-routing.module.ts

{ path: 'welcome', loadChildren: './pages/welcome/welcome.module#WelcomePageModule', canActivate: [StartAppGuard] },

配置完路由守卫后应删除ngOnInit()中的相关代码

4 第四周 注册的实现

4.1创建passport模块

在pages文件夹下,在命令的后面添加参数--routing=true,会自动生成passport-routing.module.ts文件。

ionic g module passport --routing=true

在passport文件夹中创建注册页,使用了passport模块,因此删除创建注册页时自动生成的signup.routing.ts和signup.module.ts这两个文件。在src\app

ionic generate page pages/passport/signup

在Routes数组中定义注册页面的路由。
src\app\pages\passport\passport-routing.module.ts

const routes: Routes = [
  {
    path: 'signup',
    component: SignupPage
  }
];

这个数组中的每个路由都是一个包含两个属性的 JavaScript 对象。第一个属性 path 定义了该路由的 URL 路径。第二个属性 component 定义了要让 Angular 用作相应路径的组件。

把SignupPage添加到PassportModule中的declarations列表中。
src\app\pages\passport\passport.module.ts

@NgModule({
  declarations: [
    SignupPage
  ],
  imports: [
    PassportRoutingModule,
  ]
})
export class PassportModule { }

修改根路由模块。

 {
    path: 'passport',
    loadChildren: () => import('./pages/passport/passport.module').then( m => m.PassportModule)
  }

4.2 使用Shared模块

共享模块,指当你需要针对整个业务模块都需要使用的一些第三方模块、自定义组件、自定义指令,都应该存在这里。

  1. 在Shared模块中,修改imports属性导入CommonModule,FormsModule,IonicModule等第三方模块。
  2. 修改exports属性,导出其它模块都需要的模块。
  3. 修改providers属性,指定服务的提供者。 src\app\shared\shared.module.ts
@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    FormsModule,
    IonicModule
  ],
  exports: [
    CommonModule,
    FormsModule,
    IonicModule
  ],
  providers: [
    LocalStorageService
  ]
})

在PassportModule中导入SharedModule。src\app\pages\passport\passport.module.ts

@NgModule({
  declarations: [
    SignupPage
  ],
  imports: [
    SharedModule,
    CommonModule,
    PassportRoutingModule
  ]
})

最后,在WelcomeModule中导入SharedModule。src\app\pages\welcome\welcome.module.ts

@NgModule({
  imports: [
    SharedModule,
    CommonModule,
    FormsModule,
    IonicModule,
    WelcomePageRoutingModule
  ],
  declarations: [WelcomePage]
})

4.3 跳转到注册页面

4.3.1 在欢迎页中点击注册按钮进入注册页

为“注册”按钮添加href属性。
src\app\pages\welcome\welcome.page.html

          <ion-col>
            <!-- 为ion-button元素添加href属性 -->
            <ion-button color="primary" expand="block" href="/passport/signup">注册</ion-button>
          </ion-col>

4.3.2 在欢迎页中点击“跳过”按钮进入注册页

在WelcomePage组件类中添加onSkip方法。

src\app\pages\welcome\welcome.page.ts

  onSkip() {
    this.router.navigateByUrl('passport/signup');
  }

为“跳过”按钮添加click事件绑定,使用 Angular 事件绑定语法把click事件绑定到事件处理器。
src\app\pages\welcome\welcome.page.html

<ion-button color="primary" [hidden]="!showSkip" (click)="onSkip()">跳过</ion-button>

等号左边的click表示把按钮的点击事件作为绑定目标。 等号右边引号中的文本是模板语句,通过调用组件onSkip方法来响应这个点击事件。

4.4 实现注册界面

4.4.1

界面的顶部放一张图片居中,宽度33%。

通过样式表设置图片宽度。
src\app\pages\passport\signup\signup.page.scss

.logo {
  width: 33%;
}

图片居中可以使用类选择器ion-text-center,在旧版Ionic中使用的是属性选择器text-center。后面的任务中喷到类似的情况统一改成class="ion-xxx"
src\app\pages\passport\signup\signup.page.html

<div class="ion-text-center">
  <img class="logo" src="assets/img/logo.png" alt="">
</div>

放4张图片表示4个步骤。 使用Grid布局,1行7列,第2、4、6列中的内容垂直居中并添加一条水平线。
为注册页添加样式。
src\app\pages\passport\signup\signup.page.scss

hr {
  height: 1.5px;
  border: none;
  background-color: black; //要设置background-color的值不然不会显示
}
.full-width {
  width: 100%;
}

图片之间添加水平线。
src\app\pages\passport\signup\signup.page.html

<ion-col class="ion-align-self-center">
  <hr>
</ion-col>

网格的第1、3、5、7列,各放两张图片

<ion-grid class="fixed-bottom">
    <ion-row>
      <ion-col>
        <img src="assets/img/registered_one.png" alt="">
        <img src="assets/img/registered_one_one.png" alt="">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/registered_two.png" alt="">
        <img src="assets/img/registered_two_two.png" alt="">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/registered_three.png" alt="">
        <img src="assets/img/registered_three_three.png" alt="">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/register_four.png" alt="">
        <img src="assets/img/register_four_four.png" alt="">
      </ion-col>
    </ion-row>
  </ion-grid>

使用Slides,包含4个slide子元素,每个slide对应一个form标签,<ion-slides>元素不要加pager属性

  <ion-slides #slides style="height: 100%;">
    <ion-slide>
      <form>
        <ion-list>
          <ion-item>
          </ion-item>
          <!-- 根据需求添加若干ion-item -->
        </ion-list>
      </form>
    </ion-slide>
  </ion-slides>

4.4.2 通过4张图片表示注册进行到哪个步骤

4.4.2.1 通过索引来判断注册进行到哪一步

在注册组件类(对应的ts文件)中添加slideIndex属性,用来保存当前幻灯片的索引。通过Slides的事件来记录索引的值

src\app\pages\passport\signup\signup.page.ts

slideIndex = 0;

每个步骤分别对应两张图片,某一种状态下显示其中一张,另外一张隐藏。
src\app\pages\passport\signup\signup.page.html

<ion-col>
  <img src="assets/img/registered_one.png" alt="" *ngIf="slideIndex!==0">
  <img src="assets/img/registered_one_one.png" alt="" *ngIf="slideIndex===0">
</ion-col>

*ngIf后面的表达式的结果为true,ngIf会把img添加到DOM中,否则ngIf会从DOM中移除img。总共有4组图片,另外3组图片只要修改相应索引的值就可以了。

4.4.2.2 通过代码切换4个slide

声明引用变量signupSlides。
src\app\pages\passport\signup\signup.page.html

<ion-slides #signupSlides>

在组件类中通过@ViewChild声明对子组件元素的实例引用,意思是通过注入的方式将子组件注入到@ViewChild容器中,你可以想象成依赖注入的方式注入,只不过@ViewChild不能在构造器constructor中注入,因为@ViewChild会在ngAfterViewInit()回调函数之前执行。(这啥?-_-||听不懂)
src\app\pages\passport\signup\signup.ts

@ViewChild('signupSlides', {static: false}) signupSlides: IonSlides;
//字符串'signupSlides'和模板中的#signupSlides引用变量的名称一致
ngOnInit() {
  this.signupSlides.lockSwipeNext(true); // 不知道这个干嘛的
}
onNext(){
  this.slideIndex++;
  this.signupSlides.slideNext();
}
onPrevious() {
  this.slideIndex--;
  this.signupSlides.slidePrev()
}

在“上一步”按钮上绑定click事件,调用onPrevious()。在“下一步”按钮上绑定click事件,调用onNext()

src\app\pages\passport\signup\signup.page.html

    <ion-buttons slot="end">
      <ion-button color="primary" [hidden]="slideIndex===0" (click)="onPrevious()">上一步</ion-button>
      <ion-button color="primary" [hidden]="slideIndex===3" (click)="onNext()">下一步</ion-button>
    </ion-buttons>

4.4.3 客户端验证

需要验证用户输入的准确性和完整性,来增强整体数据质量。

4.4.3.1 创建注册模型类

在SignupPage组件类中添加signup属性,signup属性是一种视图模型(View Model)对象,模型中的属性与模板中的input元素通过ngModel实现双向绑定。
首先创建Signup。

ionic g class pages/passport/signup/signup

视图模型的名称与页面的名称一致,也可以在名称的后面加VO(View Object)这个后缀。
src\app\pages\passport\signup\signup.ts

export interface Signup {
  phone: string;
  email: string;
  shopName: string;
  password: string;
  confirmPassword: string;
  code: string;
}

然后在SignupPage组件类中添加signup属性。
src\app\pages\passport\signup\signup.page.ts

signup: Signup = {
  phone: '',
  email: '',
  shopName: '',
  password: '',
  confirmPassword: '',
  code: ''
};

4.4.3.2 模板驱动表单

使用表单之前,需要将FormsModule添加到应用模块的imports数组中。导入FormsModule。把FormsModule添加到ngModule装饰器的imports列表中,这样应用就能访问模板驱动表单的所有特性,包括ngModel。
src\app\shared\shared.module.ts

之前已完成

用ngModel创建双向数据绑定,以读取和写入输入控件的值。用户输入时,要求输入的数据类型应该和当前的键盘相匹配。例如要用户输入手机号码,弹出来应该是数字键盘,这样减少用户切换键盘的麻烦。
src\app\pages\passport\signup\signup.page.html

<ion-item>
  <ion-input name="phone" type="number" placeholder="请输入您的手机号码"  [(ngModel)]="signup.phone" #phone="ngModel">
  </ion-input>
</ion-item>

在表单中使用[(ngModel)]时,必须要定义name属性。

使用属性绑定禁用提交按钮。

声明phoneForm变量用于引用<form>元素
src\app\pages\passport\signup\signup.page.html

<form #phoneForm="ngForm">

表单中的数据如果没有通过验证,下一步按钮不可用。(phoneForm.invalid不懂是什么意思)
src\app\pages\passport\signup\signup.page.html

<div class="ion-padding-horizontal">
  <ion-button type="submit" expand="full" color="primary" [disabled]="phoneForm.invalid">下一步</ion-button>
</div>

4.4.3.3 模板驱动表单验证

注册时手机号码必填,且格式是正确的手机号码。

<ion-input>元素添加required和pattern属性。
src\app\pages\passport\signup\signup.page.html

<ion-item>
  <ion-input name="phone" type="number" placeholder="请输入您的手机号码" required  pattern="^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,3,5-9]))\d{8}$" [(ngModel)]="signup.phone" #phone="ngModel">
  </ion-input>
</ion-item>

在输入框的下方向用户显示验证错误提示。
src\app\pages\passport\signup\signup.page.html

<ion-item>
  <!-- 其他省略 -->
</ion-item>
<ion-text class="ion-text-left" color="danger" *ngIf="phone.invalid && phone.touched">
  <p [hidden]="!phone.errors?.required" class="padding-start">请输入手机号码</p>
  <p [hidden]="!phone.errors?.pattern" class="padding-start">您输入的手机号格式不正确</p>
</ion-text>

4.4.3.4 表单提交

填写完表单中的数据后,应提交表单。把ion-button元素的type属性值设置为submit,为form元素设置ngSubmit事件绑定。
src\app\pages\passport\signup\signup.page.html

<form (ngSubmit)="onSubmitPhone(phoneForm)" #phoneForm="ngForm">

根据需求请自行实现onSubmitPhone方法。

  onSubmitPhone(phoneForm) {
    if(phoneForm.valid) {
      // 已通过客户端验证
      this.signup.phone = phoneForm.value;
    }
  }

4.4.4 短信验证

参考之前的任务创建AuthenticationCode服务。AuthenticationCode服务提供随机生成验证码的方法,实际开发中随机生成验证码应放在服务器端实现。AuthenticationCode服务还提供判断用户输入的验证码是否正确且验证码是否过期。

在第二个slide标签中添加相关的元素。
src\app\pages\passport\signup\signup.page.html

<ion-item>
  <ion-input slot="start" placeholder="输入验证码"></ion-input>
  <ion-button color="primary" expand="full" slot="end" >发送验证码</ion-button>
</ion-item>

slot属性用于设置子元素的位置。

验证码功能基本上用于passport模块,注册或者忘记密码时会使用到。可以在passport文件夹下创建shared文件夹,或者直接在passport文件夹中创建。如果验证码服务在其他模块中也有用到,可以在ShareModule中创建。参考之前的任务创建验证码服务。

ionic g service pages/passport/shared/psssport/authenticationCode

src\app\passport\authentication-code.service.ts

export class AuthenticationCodeService {
  // 用于保存验证码
  private code: string;
  // 存放验证码的过期时间
  private deadline: number;
  constructor() {
    this.code = '';
  }
  // 获取验证码
  getCode() {
    return this.code;
  }
  // 生成指定长度的随机数字
  createCode(count: number): string{
    this.code = '';
    // 10分钟内有效
    this.deadline = Date.now() + 60 * 10 * 1000;
    for (let i = 0; i < count; i++) {
      this.code = this.code.concat(this.getRandomNumInt(0, 9).toString());
    }
    return this.code;
  }
  // 验证用户输入的短信验证码是否一致,是否过期
  validate(value: string): boolean{
    const now = Date.now();
    return value === this.code && now < this.deadline;
  }
  getRandomNumInt(min: number, max: number) {
    const Range = max - min;
    const Rand = Math.random(); // 获取[0-1)的随机数
    return (min + Math.round(Rand * Range)); // 放大取整
  }
}

参考之前的任务使用AuthenticationCode服务,并为注册组件添加onSendSMS方法和onValidateCode方法,为相关的按钮及表单添加事件绑定。

4.4.4.1 发短信

(直接在页面提示)src\app\pages\signup\signup.page.ts

  onSendSMS() {
    console.log(this.signup.phone); // 得到电话号码
    // 生成验证码
    this.code.createCode(4);
    // 发送短信
    window.alert(this.code.getCode());
  }

4.4.4.2 倒计时

点击“发送验证码”按钮后发送短信后,按钮不可用,倒计时60秒,按钮上显示“N秒后重新获取”。倒计时完了之后,按钮恢复可用,并显示“获取验证码”。

signup.page.html

<ion-button id="sendSMS" color="primary" expand="full" [disabled]="false" slot="end" (click)="onSendSMS();">发送验证码</ion-button>

signup.page.ts

  onSendSMS() {
    console.log(this.signup.phone); // 得到电话号码
    // 生成验证码
    this.code.createCode(4);
    const sendSMS = document.getElementById('sendSMS');
    sendSMS.setAttribute('disabled', 'true');
    // 发送短信
    window.alert(this.code.getCode());
    let second = 60;
    // tslint:disable-next-line: only-arrow-functions
    let secondInterval = setInterval(function() {
      if (second < 0) {
        // 关闭定时器
        clearInterval(secondInterval);
        secondInterval = undefined;
        sendSMS.innerHTML = '发送验证码';
        sendSMS.setAttribute('disabled', 'false');
      } else {
        // 继续计时
        sendSMS.innerHTML = '重新发送' + second;
        second--;
      }
    }, 1000); // 每一秒执行定时器
  }

4.4.4.3 检验验证码是否有效

  onValidateCode(codeForm) {
    if (this.code.getCode() !== this.signup.code) {
      this.slideIndex--;
      this.signupSlides.slidePrev();
      window.alert('验证码错误!');
    } else {
      window.alert('验证码正确!');
    }
  }  

4.4.4.4 邮件、密码

 onSubmitEmail(emailForm) {
    if (emailForm.valid) {
      // 已通过客户端验证
      this.signup.email = emailForm.value;
    }
  }
  onSubmitPassword(passwordForm) {
    if (this.signup.password !== this.signup.confirmPassword) {
      window.alert('两次密码不一致!');
    } else {
      window.alert('注册成功!');
    }
  }
    <ion-slide>
      <!--#xxx代表组件的变量名-->
      <form (ngSubmit)="onSubmitEmail(emailForm)" #emailForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="email" type="string" placeholder="请输入您的电子邮箱" required pattern="([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})" [(ngModel)]="signup.email" #email="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="email.invalid && email.touched">
            <p [hidden]="!email.errors?.required" class="padding-start">请输入电子邮箱</p>
            <p [hidden]="!email.errors?.pattern" class="padding-start">您输入的电子邮箱格式不正确</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="emailForm.invalid" (click)="onNext()">下一步</ion-button>
        </div>
      </form>
    </ion-slide>

    <ion-slide>
      <form (ngSubmit)="onSubmitPassword(passwordForm)" #passwordForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="password" type="string" placeholder="请输入您的密码" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$" [(ngModel)]="signup.password" #password="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="password.invalid && password.touched">
            <p [hidden]="!password.errors?.required" class="padding-start">请输入密码</p>
            <p [hidden]="!password.errors?.pattern" class="padding-start">您输入的密码格式不正确</p>
          </ion-text>
          <ion-item>
            <ion-input name="confirmPassword" type="string" placeholder="请再次输入您的密码" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$" [(ngModel)]="signup.confirmPassword" #confirmPassword="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="confirmPassword.invalid && confirmPassword.touched">
            <p [hidden]="!confirmPassword.errors?.required" class="padding-start">请再次输入密码</p>
            <p [hidden]="!confirmPassword.errors?.pattern" class="padding-start">您输入的密码格式不正确</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="passwordForm.invalid" (click)="onRegister()">注册</ion-button>
        </div>
      </form>
    </ion-slide>

4.4.5 设置用户信息

4.4.5.1 创建用户模型

本次任务中没有使用到服务器端以及数据库,数据直接保存到手机(本地存储)中,因此需要创建用户模型和登录账户模型,名称建议跟数据库中的表名一致。用户模型存储用户基本信息(不包括密码),登录账户模型存储登录账号和登录密码。

用户模型(User)

属性 类型 用途
id number 用户编号, 1、2、3......
phone string 手机号码
email string 邮箱
createTime Date 注册时间

登录账户模型(LoginAccount)

属性 类型 用途
userId number 用户编号
identifier string 身份唯一标识,手机号、E-Mail等
credential string password/token

一位用户可以使用手机号码或者email登录,所以用户和登录账户之间构成了一对多的关系。

创建用户模型类

ionic g class model/user

4.4.5.2 创建PassportService

ionic g service pages/passport/shared/psssport

参考之前的任务创建PassportService,该服务主要实现注册、登录验证、判断是否已登录等跟业务逻辑有关的方法,实际开发中这个服务还要负责跟服务器通讯。

方法 用途 说明
addUser insertUser 添加用户 从本地存储中获取User数据,默认为[],向数组中添加数据,把数组保存到本地存储中。同样的做法处理LoginAccount
isUniquePhone 判断手机号码是否唯一

最终的代码

signup.page.ts

import { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { IonSlides } from '@ionic/angular';
import { User } from 'src/app/model/user';
import { PassportServiceService } from 'src/app/shared/services/passport-service.service';
import { AuthenticationCodeService } from '../authentication-code.service';
import { Signup } from './signup';

export const UserList = 'UserList';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.page.html',
  styleUrls: ['./signup.page.scss'],
})
export class SignupPage implements OnInit {
  slideIndex = 0;
  code = new AuthenticationCodeService();
  signup: Signup = {
    phone: '',
    email: '',
    shopName: '',
    password: '',
    confirmPassword: '',
    code: ''
  };
  user: User = {
    id: null,
    phone: '',
    email: '',
    shopName: '',
    password: '',
    createTime: null
  };
  @ViewChild('signupSlides', {static: true}) signupSlides: IonSlides;
  // 字符串'signupSlides'和模板中的#signupSlides引用变量的名称一致
  constructor(private passportServiceService: PassportServiceService, private router: Router) {} // 在构造函数中依赖注入LocalStorageService

  ngOnInit() {
     this.signupSlides.lockSwipeToNext(true); // 不知道这个干嘛的
  }
  onNext(){
    this.slideIndex++;
    this.signupSlides.lockSwipeToNext(false);
    this.signupSlides.slideNext();
    this.signupSlides.lockSwipeToNext(true);
  }
  onPrevious() {
    this.slideIndex--;
    this.signupSlides.lockSwipeToNext(false);
    this.signupSlides.slidePrev();
    this.signupSlides.lockSwipeToNext(true);
  }
  onRegister() {
    console.log(this.signup);
    // 保存用户信息
    if (this.signup.password === this.signup.confirmPassword) {
      // tslint:disable-next-line: prefer-const
      let userList: User[] = this.passportServiceService.get(UserList, []);
      this.user.id = userList.length;
      this.user.phone = this.signup.phone;
      console.log(this.user.phone);
      this.user.email = this.signup.email;
      this.user.password = this.signup.password;
      console.log(this.user.password);
      this.user.shopName = '未命名';
      this.user.createTime = new Date(); // 获取当前系统时间
      for (const data of userList) {
        if (JSON.stringify(data.phone) === JSON.stringify(this.user.phone)) {
          alert('手机号已被注册!');
          return;
        } else if (JSON.stringify(data.email) === JSON.stringify(this.user.email)) {
          alert('邮箱已被注册!');
          return;
        }
      }
      console.log(userList);
      userList.push(this.user);
      this.passportServiceService.set(UserList, userList); // 在本地内存中添加
      alert('注册成功!');
      this.router.navigateByUrl('folder/Inbox');
    }
  }
  onSubmitPhone(phoneForm) {
  }
  onSubmitEmail(emailForm) {
  }
  onSubmitPassword(passwordForm) {
    if (this.signup.password !== this.signup.confirmPassword) {
      window.alert('两次密码不一致!');
    }
  }
  onSendSMS() {
    // 生成验证码
    this.code.createCode(4);
    const sendSMS = document.getElementById('sendSMS');
    sendSMS.setAttribute('disabled', 'true');
    // 发送短信
    window.alert(this.code.getCode());
    let second = 60;
    // tslint:disable-next-line: only-arrow-functions
    let secondInterval = setInterval(function() {
      if (second < 0) {
        // 关闭定时器
        clearInterval(secondInterval);
        secondInterval = undefined;
        sendSMS.innerHTML = '发送验证码';
        sendSMS.setAttribute('disabled', 'false');
      } else {
        // 继续计时
        sendSMS.innerHTML = '重新发送' + second;
        second--;
      }
    }, 1000); // 每一秒执行定时器
  }
  onValidateCode(codeForm) {
    if (this.code.getCode() !== this.signup.code) {
      this.slideIndex--;
      this.signupSlides.slidePrev();
      window.alert('验证码错误!');
    }
  }
}

signup.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>注册</ion-title>
    <ion-buttons slot="end">
      <ion-button color="primary" [hidden]="slideIndex===0" (click)="onPrevious()">上一步</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-text-center">
    <img class="logo" src="assets/img/logo.png" alt="">
  </div>
  <ion-grid class="fixed-bottom">
    <ion-row>
      <ion-col>
        <img src="assets/img/registered_one.png" alt="" *ngIf="slideIndex!==0">
        <img src="assets/img/registered_one_one.png" alt="" *ngIf="slideIndex===0">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/registered_two.png" alt="" *ngIf="slideIndex!==1">
        <img src="assets/img/registered_two_two.png" alt="" *ngIf="slideIndex===1">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/registered_three.png" alt="" *ngIf="slideIndex!==2">
        <img src="assets/img/registered_three_three.png" alt="" *ngIf="slideIndex===2">
      </ion-col>
      <ion-col class="ion-align-self-center">
        <hr>
      </ion-col>
      <ion-col>
        <img src="assets/img/register_four.png" alt="" *ngIf="slideIndex!==3">
        <img src="assets/img/register_four_four.png" alt="" *ngIf="slideIndex===3">
      </ion-col>
    </ion-row>
  </ion-grid>
  <ion-slides #signupSlides style="height: 100%;">
    <ion-slide>
      <!--#xxx代表组件的变量名-->
      <form (ngSubmit)="onSubmitPhone(phoneForm)" #phoneForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="phone" type="number" placeholder="请输入您的手机号码" required  pattern="^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,3,5-9]))\d{8}$" [(ngModel)]="signup.phone" #phone="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="phone.invalid && phone.touched">
            <p [hidden]="!phone.errors?.required" class="padding-start">请输入手机号码</p>
            <p [hidden]="!phone.errors?.pattern" class="padding-start">您输入的手机号格式不正确</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="phoneForm.invalid" (click)="onNext()">下一步</ion-button>
        </div>
      </form>
    </ion-slide>

    <ion-slide>
      <form (ngSubmit)="onValidateCode(codeForm)" #codeForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="code" slot="start" placeholder="输入验证码" required [(ngModel)]="signup.code" #code="ngModel"></ion-input>
            <ion-button id="sendSMS" color="primary" expand="full" slot="end" (click)="onSendSMS();">发送验证码</ion-button>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="code.invalid && code.touched">
            <p [hidden]="!code.errors?.required" class="padding-start">请输入验证码</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="codeForm.invalid" (click)="onNext()">下一步</ion-button>
        </div>
      </form>
    </ion-slide>

    <ion-slide>
      <!--#xxx代表组件的变量名-->
      <form (ngSubmit)="onSubmitEmail(emailForm)" #emailForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="email" type="string" placeholder="请输入您的电子邮箱" required pattern="([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})" [(ngModel)]="signup.email" #email="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="email.invalid && email.touched">
            <p [hidden]="!email.errors?.required" class="padding-start">请输入电子邮箱</p>
            <p [hidden]="!email.errors?.pattern" class="padding-start">您输入的电子邮箱格式不正确</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="emailForm.invalid" (click)="onNext()">下一步</ion-button>
        </div>
      </form>
    </ion-slide>

    <ion-slide>
      <form (ngSubmit)="onSubmitPassword(passwordForm)" #passwordForm="ngForm">
        <ion-list>
          <ion-item>
            <ion-input name="password" type="string" placeholder="请输入您的密码" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$" [(ngModel)]="signup.password" #password="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="password.invalid && password.touched">
            <p [hidden]="!password.errors?.required" class="padding-start">请输入密码</p>
            <p [hidden]="!password.errors?.pattern" class="padding-start">您输入的密码格式不正确</p>
          </ion-text>
          <ion-item>
            <ion-input name="confirmPassword" type="string" placeholder="请再次输入您的密码" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$" [(ngModel)]="signup.confirmPassword" #confirmPassword="ngModel">
            </ion-input>
          </ion-item>
          <ion-text class="ion-text-left" color="danger" *ngIf="confirmPassword.invalid && confirmPassword.touched">
            <p [hidden]="!confirmPassword.errors?.required" class="padding-start">请再次输入密码</p>
            <p [hidden]="!confirmPassword.errors?.pattern" class="padding-start">您输入的密码格式不正确</p>
          </ion-text>
        </ion-list>
        <div class="ion-padding-horizontal">
          <ion-button type="submit" expand="full" color="primary" [disabled]="passwordForm.invalid" (click)="onRegister()">注册</ion-button>
        </div>
      </form>
    </ion-slide>
  </ion-slides>

</ion-content>

5 第五周 登录的实现

5.1 建立用户类、账户类、登陆信息类

ionic g class model/User
ionic g class model/Account
ionic g class model/loginLog
export class User {
    id: number;
    phone: string;
    email: string;
    shopName: string;
    password: string;
    createTime: Date;
}
export class Account {
    phone: string;
    email: string;
    password: string;
    date: Date;
}
export class LoginLog {
    shopName: string;
    phone: string;
    email: string;
    loginTime: Date;
    expirationTime: Date;
}

5.2 AJAX请求结果

统一AJAX请求时从服务器端返回的JSON对象。AjaxResult在其他功能中都需要用到,因此把他放在SharedModule中

ionic g class shared/class/ajaxResult
export class AjaxResult {
    constructor(public success: boolean,
                public result: any,
                public error?: { message: string; details: string; },
                public targetUrl?: string,
                public unAuthorizedRequest?: boolean) {
    }
}

5.3 实现登陆方法

src\app\pages\passport\passport.service.ts

async onLogin(form: NgForm) {
    let toast: any;
    // 判断表单验证是否正确
    if (form.invalid) {
      toast = await this.toastController.create({
        duration: 3000
      });
    }
    if (this.username === '') {
      toast.message = '请输入您的手机号码或者邮箱';
      toast.present();
    } else if (this.password === '') {
      toast.message = '请输入您的密码';
      toast.present();
    } else {
      this.passportServiceService.login(this.username, this.password).then((result) => {
        if (result.success) {
          // 验证成功,自行完成页面跳转
          console.log('页面跳转');
        } else {
          this.alertController.create({
            header: '警告',
            buttons: ['确定']
          }).then((alert) => {
            alert.message = result.error.message;
            alert.present();
          });
        }
      });
    }
  }

5.4 创建登陆页面

src\app\pages\passport\login\login.page.ts

export class LoginPage implements OnInit {
  username = ''; // 视图模型的属性账号,双向绑定
  password = ''; // 视图模型的属性密码,双向绑定
  // tslint:disable-next-line: max-line-length
  constructor(private toastController: ToastController, private alertController: AlertController, private passportServiceService: PassportServiceService, private router: Router) {
  }

  ngOnInit() {
  }
  // 点击登录按钮时调用
  async onLogin(form: NgForm) {
    let toast: any;
    // 判断表单验证是否正确
    if (form.invalid) {
      toast = await this.toastController.create({
        duration: 3000
      });
    }
    if (this.username === '') {
      toast.message = '请输入您的手机号码或者邮箱';
      toast.present();
    } else if (this.password === '') {
      toast.message = '请输入您的密码';
      toast.present();
    } else {
      this.passportServiceService.login(this.username, this.password).then((result) => {
        if (result.success) {
          // 验证成功,自行完成页面跳转
          console.log('页面跳转');
        } else {
          this.alertController.create({
            header: '警告',
            buttons: ['确定']
          }).then((alert) => {
            alert.message = result.error.message;
            alert.present();
          });
        }
      });
    }
  }
  // 点击忘记密码时调用
  onForgotPassword() {
    // 进入找回密码页面
  }
}

修改登录组件的模板文件
src\app\pages\passport\login\login.page.html

<ion-content class="ion-no-padding">
  <img src="assets/img/logoin_title.jpg" alt="">
  <div class="ion-padding-horizontal">
    <form #loginForm="ngForm">
    <ion-list class="ion-no-margin ion-no-padding">
      <ion-item lines="none"></ion-item>
      <ion-item>
        <ion-label position="fixed">账号</ion-label>
        <ion-input name="username" type="text" placeholder="手机号或者电子邮箱" required [(ngModel)]="username"></ion-input>
      </ion-item>
      <ion-item class="ion-margin-top">
        <ion-label position="fixed">密码</ion-label>
        <ion-input name="password" type="password" placeholder="您的生意专家登录密码" required [(ngModel)]="password"></ion-input>
      </ion-item>
      <ion-item lines="none"></ion-item>
    </ion-list>
    <ion-grid>
      <ion-row>
        <ion-col>
          <ion-button expand="full" color="primary" (click)="onLogin(loginForm)">登录</ion-button>
        </ion-col>
        <ion-col>
          <ion-button expand="full" fill="outline" color="primary" href="/passport/signup">注册新账号</ion-button>
        </ion-col>
      </ion-row>
      <ion-row>
        <ion-col>
          <ion-button fill="clear" size="small" (click)="onForgotPassword()">忘记密码?</ion-button>
        </ion-col>
      </ion-row>
      <ion-row class="ion-text-center">
        <ion-col>查看演示</ion-col>
      </ion-row>
    </ion-grid>
    </form>
  </div>
</ion-content>

5.5 忘记密码功能

点击“忘记密码”,进入找回密码页面。

创建忘记密码页面

ionic g page pages/passport/forgotPassword

在passport-routing.module.ts中添加ForgotPasswordPage

  {
    path: 'forgot-password',
    component: ForgotPasswordPage
  }

在passport.module.ts中添加ForgotPasswordPage

  declarations: [
    SignupPage,
    LoginPage,
    ForgotPasswordPage
  ],

在login.page.ts中

  // 点击忘记密码时调用
  onForgotPassword() {
    // 进入找回密码页面
    this.router.navigateByUrl('passport/forgot-password');
  }

点击“重置密码”,进入重置密码页面。

创建重置密码页面

ionic g page pages/passport/resetPassword

在passport-routing.module.ts中添加ResetPasswordPage

  {
    path: 'reset-password',
    component: ResetPasswordPage
  }

在passport.module.ts中添加ResetPasswordPage

  declarations: [
    SignupPage,
    LoginPage,
    ResetPasswordPage
  ],

忘记密码和重置密码相关代码省略。

5.6 重新整理路由模块

登录成功后页面跳转到首页并把相关数据保存在本地存储中。已完成

在欢迎页组件中调整onSkip代码,如果程序是第一次运行,就跳转到注册页面。判断用户是否已登录,已登录过,跳转到首页。未登录过或者登录已经过期,跳转到登录页。

start-app.guard.ts (老师要求isLaunched改成launched)

if (appConfig.isLaunched === false) { // 如果是第一次启动
      appConfig.isLaunched = true;
      this.localStorageService.set(APP_KEY, appConfig); // 在本地内存中添加
      return true;
    } else {
      if (this.passportServiceService.isLoggedin() === false) {
        this.router.navigateByUrl('passport/login'); // 路由到
      } else {
        const loginlog: LoginLog = this.localStorageService.get('loginLog', []);
        loginlog.loginTime = new Date();
        loginlog.expirationTime = new Date(loginlog.loginTime.getTime() + 432000000);
        this.localStorageService.set('loginLog', loginlog); // 更新登陆时间
        this.router.navigateByUrl('folder/Inbox'); // 路由到
      }
      return false;
    }
    

passport-service.service.ts

  isLoggedin() {
    const loginlog: LoginLog = this.get('loginLog', {
      shopName: '',
      phone: '',
      email: '',
      loginTime: null,
      expirationTime: null,
    });
    if (loginlog.expirationTime === null) {
      return false;
    }
    if (new Date() > new Date(loginlog.expirationTime)) {
      return false;
    }
    return true;
  }

5.7 添加版权声明

把版权声明固定在程序的底部。
src\app\pages\passport\login\login.page.html

<div style="position: fixed; left: 0; right: 0; bottom: 10px;" class="ion-text-center">
  <span>&copy;2010-2020 生意专家</span>
</div>

在登录页面和注册页面都有版权的声明,考虑到复用性和可维护性,创建组件。实际上之前用到的页面也是组件的一种。

5.7.1 创建component。

在app\shared目录下创建components文件夹

ionic g component shared/components/Copyright

在shared.module.ts文件中,在declarations属性中添加CopyrightComponent,然后在exports属性中添加。 src\app\shared\shared.mudule.ts

@NgModule({
  declarations: [
    CopyrightComponent,
  ],
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
  ],
  exports: [
    CommonModule,
    FormsModule,
    IonicModule,
    CopyrightComponent,
  ],
  providers: [
    LocalStorageService,
  ]
})

5.7.2

修改copyright组件类,能够动态的设定版权距离底部的距离,自动获取当前时间的年份。

src\app\shared\componets\copyright\copyright.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-copyright',
  templateUrl: 'copyright.html'
})
export class CopyrightComponent {
  @Input() bottom: string;
  text: string;
  constructor() {
    let year = (new Date()).getFullYear();
    this.text = `2010-${year} 生意专家`;
    this.bottom = '10px';
  }
}

这种字符串是被反引号包围( `),并且以${ expr }这种形式嵌入表达式。

5.7.3

使用属性绑定组件类的bottom属性。
src\app\shared\componets\copyright\copyright.hml

<div style="position: fixed;left: 0; right: 0;" [style.bottom]="bottom" class="ion-text-center">
  <span>&copy;{{text}}</span>
</div>

5.7.4

在登录页中使用CopyrightComponent代替之前的div。
src\app\pages\passport\login\login.page.html

<!-- 其他省略 -->
  <app-copyright [bottom]="'20px'"></app-copyright>
</ion-content>

5.7.5

在注册页中使用CopyrightComponent,这样就能够做到组件的复用。

6 第六周 首页的实现

创建首页(Home)组件,导入ShareModule。

ionic g page pages/home

6.1 修改程序的主题颜色

修改应用程序的主色调。

  1. 访问Ionic官网提供的颜色生成器颜色生成器链接
  2. 在primary输入框中输入#FF6A3C。更新颜色的十六进制值,检查右侧的演示应用程序进行确认。
  3. 将生成的代码直接复制(primary)并粘贴到Ionic项目中。把--ion-color-primary-contrast的值改为#ffffff,把--ion-color-primary-contrast-rgb的值改为255,255,255。

CSS变量的修改参考下面的代码:
src\theme\variables.scss

:root {
  --ion-color-primary: #FF6A3C;
  --ion-color-primary-rgb: 255,106,60;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255,255,255;
  --ion-color-primary-shade: #e05d35;
  --ion-color-primary-tint: #ff7950;

  --ion-color-secondary: #0cd1e8;
  --ion-color-secondary-rgb: 12,209,232;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255,255,255;
  --ion-color-secondary-shade: #0bb8cc;
  --ion-color-secondary-tint: #24d6ea;

  --ion-color-tertiary: #7044ff;
  --ion-color-tertiary-rgb: 112,68,255;
  --ion-color-tertiary-contrast: #ffffff;
  --ion-color-tertiary-contrast-rgb: 255,255,255;
  --ion-color-tertiary-shade: #633ce0;
  --ion-color-tertiary-tint: #7e57ff;

  --ion-color-success: #10dc60;
  --ion-color-success-rgb: 16,220,96;
  --ion-color-success-contrast: #ffffff;
  --ion-color-success-contrast-rgb: 255,255,255;
  --ion-color-success-shade: #0ec254;
  --ion-color-success-tint: #28e070;

  --ion-color-warning: #ffce00;
  --ion-color-warning-rgb: 255,206,0;
  --ion-color-warning-contrast: #ffffff;
  --ion-color-warning-contrast-rgb: 255,255,255;
  --ion-color-warning-shade: #e0b500;
  --ion-color-warning-tint: #ffd31a;

  --ion-color-danger: #f04141;
  --ion-color-danger-rgb: 245,61,61;
  --ion-color-danger-contrast: #ffffff;
  --ion-color-danger-contrast-rgb: 255,255,255;
  --ion-color-danger-shade: #d33939;
  --ion-color-danger-tint: #f25454;

  --ion-color-dark: #222428;
  --ion-color-dark-rgb: 34,34,34;
  --ion-color-dark-contrast: #ffffff;
  --ion-color-dark-contrast-rgb: 255,255,255;
  --ion-color-dark-shade: #1e2023;
  --ion-color-dark-tint: #383a3e;

  --ion-color-medium: #989aa2;
  --ion-color-medium-rgb: 152,154,162;
  --ion-color-medium-contrast: #ffffff;
  --ion-color-medium-contrast-rgb: 255,255,255;
  --ion-color-medium-shade: #86888f;
  --ion-color-medium-tint: #a2a4ab;

  --ion-color-light: #f4f5f8;
  --ion-color-light-rgb: 244,244,244;
  --ion-color-light-contrast: #000000;
  --ion-color-light-contrast-rgb: 0,0,0;
  --ion-color-light-shade: #d7d8da;
  --ion-color-light-tint: #f5f6f9;
}

运行应用程序,检查登录页或者注册页中按钮的背景颜色是否发生改变。

6.2 左侧菜单

程序运行时先加载index.html文件,在body中显示app-rootapp-root就是AppComponent,应用程序的根组件。把页面共性的内容放在AppComponent中,这里放的就是菜单ion-menu。不同功能的个性化页面内容放在ion-router-outlet后面。

6.2.1 显示用户信息

从本地存储中获取之前保存过的用户基本信息,显示店铺名和手机号。请修改下面的代码,使用插值表达式{{}}展示相关数据。界面参考下面的代码:
src\app\app.component.html

        <ion-list>
          <ion-item color="medium">
            <ion-label>
              <ion-text>
                <h2>{{ LoginLog.shopName }}</h2>
              </ion-text>
              <p>{{ LoginLog.phone }}</p>
            </ion-label>
            <ion-badge slot="end" color="primary">高级版</ion-badge>
          </ion-item>
        </ion-list>

src\app\app.component.ts

  public LoginLog = '';
  ...
   this.LoginLog = this.passportServiceService.get('loginLog', {
      email: 'null',
      phone: 'null',
      shopName: '未命名',
      loginTime: null,
      expirationTime: null
    });

6.2.2 实现左侧菜单的界面

在组件类中修改appPages数组,数组成员中添加icon属性,用来表示图标的名字。
src\app\app.component.ts

  public appPages: Array<{title: string, url: string, icon: string}>;

在构造函数中修改pages的初始化代码。
src\app\app.component.ts

initializeApp() {
    this.LoginLog = this.passportServiceService.get('loginLog', {
      email: 'null',
      phone: 'null',
      shopName: '未命名',
      loginTime: null,
      expirationTime: null
    });
    this.appPages = [
      { title: '开店论坛', url: '/home', icon: 'chatbox' },
      { title: '手机橱窗', url: '/home', icon: 'create' },
      { title: '邀请有礼', url: '/home', icon: 'git-merge' },
      { title: '资金账户', url: '/home', icon: 'cash' },
      { title: '反馈建议', url: '/home', icon: 'cash' },
      { title: '帮助中心', url: '/home', icon: 'cash' },
    ];
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });
  }

使用ngFor显示数组属性。
以下代码可以从原有的AppComponent中获得,无需调整。
src\app\app.component.html

		<ion-list>
          <ion-menu-toggle auto-hide="false" *ngFor="let p of appPages">
            <ion-item [routerDirection]="'root'" [routerLink]="[p.url]">
              <ion-icon slot="start" [name]="p.icon"></ion-icon>
              <ion-label>
                {{p.title}}
              </ion-label>
            </ion-item>
          </ion-menu-toggle>
        </ion-list>

点击左下角的设置按钮,页面跳转到系统设置页。参考之前的任务使用样式表设置按钮的位置,规定在界面的左下角。
src\app\app.component.html

<ion-menu-toggle auto-hide="false">
  <ion-button color="dark" fill="clear">
    <ion-icon slot="start" name='settings'></ion-icon>设置
  </ion-button>
</ion-menu-toggle>

页面跳转代码未完成

6.2.3 禁用菜单

有些页面是不能带菜单的,例如登录页、注册页等,但目前所有的页面都是带了菜单。要限制用户使用菜单,首先工具栏上面不放ion-menu-button。但这么做还不够,因为用户可以通过向右滑动的操作,显示出菜单。可以通过MenuController的enable方法禁用菜单。在组件类的构造函数中依赖注入MenuController,添加下面两个方法:

  ionViewWillEnter() {
    this.menuController.enable(false);
  }

  ionViewDidLeave() {
    this.menuController.enable(true);
  }

要在每个页面的ts文件中添加

6.3 首页

参考之前的任务在界面上部添加一张图片。

6.3.1 头部右侧添加两个图标

在ion-toolbar元素中设置颜色,并添加以ion-buttons子元素。
src\pages\home\home.html

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      首页
    </ion-title>
    <ion-buttons slot="end">
      <ion-button>
        <ion-icon slot="icon-only" name="calendar"></ion-icon>
      </ion-button>
      <ion-button>
        <ion-icon slot="icon-only" name="notifications"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

6.3.2 展示销售统计数据

在home组件类中,添加一个类型为数组的属性(sales),数组中的成员是个对象(含title、content、previous、current)。

public sales: Array<{title: string, content: string, previous: string, current: string}>;

添加sale.service,

ionic g service shared/services/Sale

在服务中添加getSales方法,随机生成6个数字,分别表示昨天、今天、7天、去年同期7天、本月和去年同期月份的销售数据。

src\app\shared\services\sale.services.ts

  // 随机生成6个数字,分别表示昨天、今天、7天、去年同期7天、本月和去年同期月份的销售数据
  getSales() {
    // tslint:disable-next-line: prefer-const
    let Sales: Array<{title: string, content: string, previous: string, current: string}> = new Array();
    for (let i = 0; i < 6; i++) {
      const sales = {title: '', content: '', previous: '', current: ''};
      sales.title = this.titles[i] + '的销售数据';
      sales.content = 'xxxxxxxxxxxxxxxxxxxxxxxxxx';
      sales.current = this.getRandomNumInt(0, 1000) + '';
      sales.previous = this.getRandomNumInt(0, 1000) + '';
      Sales.push(sales);
    }
    return Sales;
  }

  getRandomNumInt(min: number, max: number) {
    const Range = max - min;
    const Rand = Math.random(); // 获取[0-1)的随机数
    return (min + Math.round(Rand * Range)); // 放大取整
  }

根据需求使用对应的颜色和图标表示数据的变化。

使用grid布局,1行3列,并使用ngFor。
src\app\pages\home\home.page.html

<ion-grid>
  <ion-row>
    <ion-col *ngFor="let s of sales">
      <h6>{{s.title}}</h6>
      <h4><span>{{s.current | number:'1.2-2'}}元</span></h4>
      <p>
        {{s.content}}
        <span>
          {{s.current - s.previous | number:'1.2-2'}}
        </span>&nbsp;
        <ion-icon name="arrow-round-up"></ion-icon>
      </p>
    </ion-col>
  </ion-row>
</ion-grid>

在样式中使用success和danger这两种颜色。虽然在工程中可以找到success和danger这两种颜色的16进制或者rgb,但是下面这种写法不建议使用,虽然也可以达到要求。后期开发中如果修改了success或者danger的颜色,还需要回来修改样式,可维护性较差。

  .less-equal{
    color: #10dc60;
  }
  .greater{
    color: #f04141;
  }

建议使用下面这种写法,var() CSS函数可以用于获得CSS变量的值。
src\app\pages\home\home.page.scss

  .less-equal{
    color: var(--ion-color-success, #10dc60);
  }
  .greater{
    color: var(--ion-color-danger, #f04141);
  }

第一个参数表示变量的名称,如果有声明--ion-color-success变量,则使用--ion-color-success变量的值。如果没有声明--ion-color-success变量,将使用第二个参数的值。也可以省略第二个参数。

使用CSS变量修改之前注册任务中水平线的颜色。

hr {
    height: 1.5px;
    border: none;
    background-color: black;
}

差额的值大于0,应用.greater样式。差额的值小于等于0,应用.less-equal样式。
src\app\pages\home\home.page.html

<ion-grid>
  <ion-row>
    <ion-col *ngFor="let s of sales">
      <h6>{{s.title}}</h6>
      <h4><span>{{564.678 | number:'1.2-2'}}元</span></h4>
      <p>
        {{s.content}}
        <span [ngClass]="{'less-equal': s.current - s.previous <= 0,'greater': s.current - s.previous > 0}">
          {{s.current - s.previous | number:'1.2-2'}}
        </span>&nbsp;
        <ion-icon name="arrow-round-up"></ion-icon>
      </p>
    </ion-col>
  </ion-row>
</ion-grid>

请调整相关文字字体(font-size)的大小。

  1. 使用三个方向箭头图标(arrow-up、arrow-forward、arrow-down)表示数据的变化。可以使用多种方式实现,例如,在组件类中添加一个方法用于拼接出图标的名称。也可以使用之前任务中用到的ngIf。为了学习ngSwitch指令的用法,这里用NgSwitch、NgSwitchCase 和 NgSwitchDefault实现,当然解决起来麻烦了点。在组件类中添加minus方法。
    src\app\pages\home\home.page.ts
  /**
   *
   *
   * @param {number} current 当前销售数据
   * @param {number} previous 前期销售数据
   * @returns {number} 1 增长 0 持平 -1 减少
   * @memberof HomePage
   */
minus(current: number, previous: number): number {
  const result = current - previous;
  if (result > 0) {
    return 1;
  } else if (result === 0) {
    return 0;
  } else {
    return -1;
  }
}

展示销售数据的界面实现,参考下面的代码:
src\app\pages\home\home.page.html

<ion-grid>
  <ion-row>
    <ion-col *ngFor="let s of sales">
      <h6>{{s.title}}</h6>
      <h4><span>{{564.678 | number:'1.2-2'}}元</span></h4>
      <p>
        {{s.content}}
        <span [ngClass]="{'less-equal':s.current - s.previous <= 0,'greater':s.current - s.previous > 0}">
          {{s.current - s.previous}}
        </span>&nbsp;
        <ng-container [ngSwitch]="minus(s.current, s.previous)">
          <ion-icon name="arrow-up" color="danger" *ngSwitchCase="1"></ion-icon>
          <ion-icon name="arrow-forward" color="success" *ngSwitchCase="0"></ion-icon>
          <ion-icon name="arrow-down" color="success" *ngSwitchCase="-1"></ion-icon>
        </ng-container>
      </p>
    </ion-col>
  </ion-row>
</ion-grid>

6.3.3 添加常用功能的快捷图标

添加相关的样式。
src\app\pages\home\home.scss

  .grid {
  border-right: 1px solid #ececec;
  border-bottom: 1px solid #ececec;
  .grid-item {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 25vw;
    border-top: 1px solid #ececec;
    border-left: 1px solid #ececec;
    div {
      margin-top: 9px;
      text-align: center;
    }
  }
}

调整界面。
src\app\pages\home\home.page.html

<!-- 其他省略 -->
<ion-row class="grid">
  <ion-col size="3">
    <div class="grid-item">
      <ion-icon size="large" name="apps"></ion-icon>
      <div>新增商品</div>
    </div>
  </ion-col>
</ion-row>

在首页的组件类中添加一个数组用来保存常用功能的快捷方式,参考之前的任务初始化数组,在首页的模板文件中使用ngFor

最终html

<ion-content>
  <ion-slides #imgSlides>
    <ion-slide>
      <img src="assets/img/androidbanner.png" alt="">
    </ion-slide>
    <ion-slide>
      <img src="assets/img/androidbanner.png" alt="">
    </ion-slide>
  </ion-slides>
  <ion-grid>
    <ion-row style="background: rgb(240, 239, 239);">
      <ion-col *ngFor="let s of sales">
        <div class="grid-item">
          <h6 style="font-size: 20px;color:rgb(112, 111, 111);">{{s.title}}</h6>
          <h4 style="font-size: 18px;"><span>{{s.current | number:'1.2-2'}}元</span></h4>
          <p style="font-size: 8px;">
            {{s.content}}
            <span [ngClass]="{'less-equal': s.current - s.previous <= 0,'greater': s.current - s.previous > 0}">
              {{s.current - s.previous | number:'1.2-2'}}
            </span>&nbsp;
            <ng-container [ngSwitch]="minus(s.current, s.previous)">
              <ion-icon name="arrow-up" color="danger" *ngSwitchCase="1"></ion-icon>
              <ion-icon name="arrow-forward" color="success" *ngSwitchCase="0"></ion-icon>
              <ion-icon name="arrow-down" color="success" *ngSwitchCase="-1"></ion-icon>
            </ng-container>
          </p>
        </div>
      </ion-col>
    </ion-row>

    <ion-row class="grid">
      <ion-col size="3" *ngFor="let f of funcs">
        <div class="grid-item">
          <a href=""> <img src={{f.url}}></a>
          <div>{{f.title}}</div>
        </div>
      </ion-col>
    </ion-row>
  </ion-grid>
  <yxy-copyright [bottom]="'20px'"></yxy-copyright>
</ion-content>

最终ts

export class HomePage implements OnInit {
  public sales: Array<{title: string, content: string, previous: string, current: string}>;
  public funcs: Array<{title: string, url: string}> = [{
    title: '新增商品',
    url: 'assets/img/add_salse.png'
  }, {
    title: '新增会员',
    url: 'assets/img/add_user.png'
  }, {
    title: '收银记账',
    url: 'assets/img/sales_account.png'
  }, {
    title: '支出管理',
    url: 'assets/img/a_note.png'
  }, {
    title: '商品管理',
    url: 'assets/img/sales_management.png'
  }, {
    title: '会员管理',
    url: 'assets/img/user_management.png'
  }, {
    title: '查询销售',
    url: 'assets/img/shop_management.png'
  }, {
    title: '智能分析',
    url: 'assets/img/analysis.png'
  }, {
    title: '供应商管理',
    url: 'assets/img/gongying_more.png'
  }, {
    title: '挂单',
    url: 'assets/img/guandan_more.png'
  }, {
    title: '高级功能',
    url: 'assets/img/image_addsales.png'
  }];
  constructor(private menuController: MenuController, private saleService: SaleService) { }

  ngOnInit() {
    this.sales = this.saleService.getSales();
  }

  /*
   * @param current 当前销售数据
   * @param previous 前期销售数据
   * @returns 1 增长 0 持平 -1 减少
   */
  minus(current: number, previous: number): number {
    const result = current - previous;
    if (result > 0) {
      return 1;
    } else if (result === 0) {
      return 0;
    } else {
      return -1;
    }
  }
}

6.4 其他

使用之前任务中创建的CopyrightComponent。略

参考之前的任务,点击图标进入相关页面。目前相关页面还未创建,后面做到相关任务时记得回来补全代码。

app.component.ts

{ title: '资金账户', url: '/home', icon: 'cash' },

修改之前的守护路由StartAppGuard,根据登录时间是否过期,如果没有过期跳转到首页,如果登录过期或者用户没有登录过跳转到登录页。上次任务已完成

后面的懒得写了。。。

posted @ 2020-10-19 15:43  s1beria  阅读(524)  评论(0编辑  收藏  举报