工程實踐作業——生意專家


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,根據登錄時間是否過期,如果沒有過期跳轉到首頁,如果登錄過期或者用戶沒有登錄過跳轉到登錄頁。上次任務已完成

后面的懶得寫了。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM