陈松 5 月之前
父節點
當前提交
8f04c72a12
共有 44 個文件被更改,包括 1035 次插入46 次删除
  1. 109 2
      package-lock.json
  2. 2 0
      package.json
  3. 2 2
      src/app/app-routing.module.ts
  4. 7 1
      src/app/app.component.ts
  5. 58 10
      src/app/tab-mine/tab-mine.page.html
  6. 43 0
      src/app/tab-mine/tab-mine.page.scss
  7. 40 4
      src/app/tab-mine/tab-mine.page.ts
  8. 5 3
      src/app/tab1/tab1.page.html
  9. 4 0
      src/app/tab1/tab1.page.scss
  10. 8 1
      src/app/tab1/tab1.page.ts
  11. 2 6
      src/app/tabs/tabs-routing.module.ts
  12. 8 17
      src/app/tabs/tabs.page.html
  13. 17 0
      src/modules/contact/contact-detail/contact-detail-routing.module.ts
  14. 20 0
      src/modules/contact/contact-detail/contact-detail.module.ts
  15. 13 0
      src/modules/contact/contact-detail/contact-detail.page.html
  16. 0 0
      src/modules/contact/contact-detail/contact-detail.page.scss
  17. 17 0
      src/modules/contact/contact-detail/contact-detail.page.spec.ts
  18. 15 0
      src/modules/contact/contact-detail/contact-detail.page.ts
  19. 17 0
      src/modules/contact/contact-list/contact-list-routing.module.ts
  20. 20 0
      src/modules/contact/contact-list/contact-list.module.ts
  21. 34 0
      src/modules/contact/contact-list/contact-list.page.html
  22. 0 0
      src/modules/contact/contact-list/contact-list.page.scss
  23. 17 0
      src/modules/contact/contact-list/contact-list.page.spec.ts
  24. 42 0
      src/modules/contact/contact-list/contact-list.page.ts
  25. 12 0
      src/modules/contact/contact-routing.module.ts
  26. 14 0
      src/modules/contact/contact.module.ts
  27. 18 0
      src/modules/user/guard-auth/auth.guard.ts
  28. 15 0
      src/modules/user/guard-auth/auth.local.guard.ts
  29. 49 0
      src/modules/user/page-info/page-info.component.html
  30. 0 0
      src/modules/user/page-info/page-info.component.scss
  31. 21 0
      src/modules/user/page-info/page-info.component.spec.ts
  32. 56 0
      src/modules/user/page-info/page-info.component.ts
  33. 45 0
      src/modules/user/page-login/page-login.component.html
  34. 8 0
      src/modules/user/page-login/page-login.component.scss
  35. 21 0
      src/modules/user/page-login/page-login.component.spec.ts
  36. 51 0
      src/modules/user/page-login/page-login.component.ts
  37. 45 0
      src/modules/user/page-register/page-register.component.html
  38. 8 0
      src/modules/user/page-register/page-register.component.scss
  39. 21 0
      src/modules/user/page-register/page-register.component.spec.ts
  40. 56 0
      src/modules/user/page-register/page-register.component.ts
  41. 16 0
      src/modules/user/service-user/user.service.spec.ts
  42. 29 0
      src/modules/user/service-user/user.service.ts
  43. 23 0
      src/modules/user/user-routing.module.ts
  44. 27 0
      src/modules/user/user.module.ts

+ 109 - 2
package-lock.json

@@ -18,6 +18,7 @@
         "@angular/router": "^17.0.2",
         "@ionic/angular": "^8.0.0",
         "ionicons": "^7.0.0",
+        "parse": "^5.1.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.14.2"
@@ -34,6 +35,7 @@
         "@angular/language-service": "^17.0.2",
         "@ionic/angular-toolkit": "^11.0.1",
         "@types/jasmine": "~5.1.0",
+        "@types/parse": "^3.0.9",
         "@typescript-eslint/eslint-plugin": "^6.0.0",
         "@typescript-eslint/parser": "^6.0.0",
         "eslint": "^8.57.0",
@@ -2401,6 +2403,18 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/runtime-corejs3": {
+      "version": "7.23.2",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.23.2.tgz",
+      "integrity": "sha512-54cIh74Z1rp4oIjsHjqN+WM4fMyCBYe+LpZ9jWm51CZ1fbH3SkAzQD/3XLoNkjbJ7YEmjobLXyvQrFypRHOrXw==",
+      "dependencies": {
+        "core-js-pure": "^3.30.2",
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.24.6",
       "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.6.tgz",
@@ -4391,6 +4405,15 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/parse": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmmirror.com/@types/parse/-/parse-3.0.9.tgz",
+      "integrity": "sha512-DGTHygc7krgmNAK8h42giwmAofCd9uv2++RD+zw6OmWI7AEnlTYZwEuWsx22SA2CSMQrZW8P2INHLpQbnQFUng==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/qs": {
       "version": "6.9.15",
       "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.15.tgz",
@@ -6438,6 +6461,16 @@
         "browserslist": "^4.23.0"
       }
     },
+    "node_modules/core-js-pure": {
+      "version": "3.37.1",
+      "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.37.1.tgz",
+      "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==",
+      "hasInstallScript": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -6597,6 +6630,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "optional": true
+    },
     "node_modules/css-loader": {
       "version": "6.10.0",
       "resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.10.0.tgz",
@@ -9032,6 +9071,11 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/idb-keyval": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz",
+      "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
+    },
     "node_modules/ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
@@ -12054,6 +12098,25 @@
         "node": ">=6"
       }
     },
+    "node_modules/parse": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/parse/-/parse-5.1.0.tgz",
+      "integrity": "sha512-46gVRe1JHsh21Ht0/Ko6PeMDl6wELLMYxnZPFD6iZm2EWsWnzi2txNGE6PvnIv+G7yOufZIOD0BCZLYOFl3toA==",
+      "dependencies": {
+        "@babel/runtime-corejs3": "7.23.2",
+        "idb-keyval": "6.2.1",
+        "react-native-crypto-js": "1.0.0",
+        "uuid": "9.0.1",
+        "ws": "8.16.0",
+        "xmlhttprequest": "1.8.0"
+      },
+      "engines": {
+        "node": ">=18 <21"
+      },
+      "optionalDependencies": {
+        "crypto-js": "4.2.0"
+      }
+    },
     "node_modules/parse-json": {
       "version": "5.2.0",
       "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
@@ -12093,6 +12156,38 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/parse/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/parse/node_modules/ws": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.16.0.tgz",
+      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/parse5": {
       "version": "7.1.2",
       "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.1.2.tgz",
@@ -12657,6 +12752,11 @@
       "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
       "dev": true
     },
+    "node_modules/react-native-crypto-js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz",
+      "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA=="
+    },
     "node_modules/read-package-json": {
       "version": "7.0.1",
       "resolved": "https://registry.npmmirror.com/read-package-json/-/read-package-json-7.0.1.tgz",
@@ -12787,8 +12887,7 @@
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
-      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
-      "dev": true
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
     },
     "node_modules/regenerator-transform": {
       "version": "0.15.2",
@@ -15689,6 +15788,14 @@
         }
       }
     },
+    "node_modules/xmlhttprequest": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
+      "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",

+ 2 - 0
package.json

@@ -23,6 +23,7 @@
     "@angular/router": "^17.0.2",
     "@ionic/angular": "^8.0.0",
     "ionicons": "^7.0.0",
+    "parse": "^5.1.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.14.2"
@@ -39,6 +40,7 @@
     "@angular/language-service": "^17.0.2",
     "@ionic/angular-toolkit": "^11.0.1",
     "@types/jasmine": "~5.1.0",
+    "@types/parse": "^3.0.9",
     "@typescript-eslint/eslint-plugin": "^6.0.0",
     "@typescript-eslint/parser": "^6.0.0",
     "eslint": "^8.57.0",

+ 2 - 2
src/app/app-routing.module.ts

@@ -7,8 +7,8 @@ const routes: Routes = [
     loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
   },
   {
-    path: 'tab-mine',
-    loadChildren: () => import('./tab-mine/tab-mine.module').then( m => m.TabMinePageModule)
+    path: 'user',
+    loadChildren: () => import('../modules/user/user.module').then(m => m.UserModule)
   }
 ];
 @NgModule({

+ 7 - 1
src/app/app.component.ts

@@ -1,4 +1,7 @@
 import { Component } from '@angular/core';
+import * as Parse from "parse";
+Parse.initialize("dev");
+(Parse as any).serverURL = 'http://web2023.fmode.cn:9999/parse'
 
 @Component({
   selector: 'app-root',
@@ -6,5 +9,8 @@ import { Component } from '@angular/core';
   styleUrls: ['app.component.scss'],
 })
 export class AppComponent {
-  constructor() {}
+  constructor() {
+
+
+  }
 }

+ 58 - 10
src/app/tab-mine/tab-mine.page.html

@@ -1,13 +1,61 @@
-<ion-header [translucent]="true">
-  <ion-toolbar>
-    <ion-title>tab-mine</ion-title>
-  </ion-toolbar>
-</ion-header>
-
-<ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
+<ion-content class="ion-padding">
+  <ion-header>
     <ion-toolbar>
-      <ion-title size="large">tab-mine</ion-title>
+      <ion-title>我的</ion-title>
     </ion-toolbar>
   </ion-header>
-</ion-content>
+  
+  <ion-content color="light">
+    <ion-card>
+      <img alt="User Background" src="https://tse3-mm.cn.bing.net/th/id/OIP-C.ZSxe_CD8y2gVb9P_-mZVpAHaEG?w=305&h=180&c=7&r=0&o=5&dpr=1.3&pid=1.7" />
+      <ion-card-header>
+        <ion-card-title>{{ name || '尊贵的用户' }}</ion-card-title>
+        <ion-card-subtitle>{{ userName || '未知' }}</ion-card-subtitle>
+      </ion-card-header>
+      <ion-card-content>
+        {{ errorMessage }}
+      </ion-card-content>
+    </ion-card>
+  
+    <ion-list [inset]="true">
+      <ion-button *ngIf="!isLoggedIn" expand="block" routerLink="../../user/login">登录</ion-button>
+      <ion-button *ngIf="!isLoggedIn" expand="block" routerLink="../../user/register">注册</ion-button>
+      <ion-button *ngIf="isLoggedIn" fill="clear" expand="block" (click)="logout()">登出</ion-button>
+    </ion-list>
+ 
+  
+
+      <ion-list [inset]="true">
+        <ion-item [button]="true" routerLink="/user/info">
+          <ion-icon color="tertiary" slot="start" name="create-outline" size="large"></ion-icon>
+          <ion-label>编辑资料</ion-label>
+          <ion-note slot="end">丰富您的信息</ion-note>
+        </ion-item>
+       
+      </ion-list>
+
+     
+    <ion-list [inset]="true" style="margin-bottom: 160px;">
+      <ion-item [button]="true">
+        <ion-icon color="danger" slot="start" name="book-outline" size="large"></ion-icon>
+        <ion-label>我的课程</ion-label>
+        <ion-note slot="end">6</ion-note>
+      </ion-item>
+      <ion-item [button]="true">
+        <ion-icon color="tertiary" slot="start" name="star-outline" size="large"></ion-icon>
+        <ion-label>我的收藏</ion-label>
+        <ion-note slot="end">15</ion-note>
+      </ion-item>
+      <ion-item [button]="true">
+        <ion-icon color="success" slot="start" name="eye-outline" size="large"></ion-icon>
+        <ion-label>我的浏览</ion-label>
+        <ion-note slot="end">3</ion-note>
+      </ion-item>
+    </ion-list>
+
+  </ion-content>
+
+  </ion-content>
+
+
+  

+ 43 - 0
src/app/tab-mine/tab-mine.page.scss

@@ -0,0 +1,43 @@
+.unread-indicator {
+    background: var(--ion-color-primary);
+  
+    width: 10px;
+    height: 10px;
+  
+    border-radius: 100%;
+  
+    position: absolute;
+  
+    inset-inline-start: 12px;
+    top: 12px;
+  }
+  
+  .metadata-end-wrapper {
+    position: absolute;
+  
+    top: 10px;
+    inset-inline-end: 10px;
+  
+    font-size: 0.8rem;
+  
+    display: flex;
+    align-items: center;
+  }
+  
+  ion-label strong {
+    display: block;
+  
+    max-width: calc(100% - 60px);
+  
+    overflow: hidden;
+  
+    text-overflow: ellipsis;
+  }
+  
+  ion-label ion-note {
+    font-size: 0.9rem;
+  }
+
+/*   .ion-padding{
+    padding-bottom: 100px;
+  } */

+ 40 - 4
src/app/tab-mine/tab-mine.page.ts

@@ -1,15 +1,51 @@
 import { Component, OnInit } from '@angular/core';
-
+import { AlertController } from '@ionic/angular';
+import * as Parse from 'parse';
 @Component({
   selector: 'app-tab-mine',
   templateUrl: './tab-mine.page.html',
   styleUrls: ['./tab-mine.page.scss'],
 })
 export class TabMinePage implements OnInit {
+  isLoggedIn: boolean = false;
+  currentUser: Parse.User<Parse.Attributes> | undefined;
+  userName: string = '';
+  name: string = '';
+  errorMessage: string = '';
+
+  constructor(
+    private alertController: AlertController,
+  ) {
+    this.loadUserInfo();
+  }
 
-  constructor() { }
+  ngOnInit(): void {
+  }
 
-  ngOnInit() {
+  loadUserInfo(): void {
+    this.currentUser = Parse.User.current();
+    if (this.currentUser) {
+      this.isLoggedIn = true;
+      this.userName = this.currentUser.get('username');
+      this.name = this.currentUser.get('name');
+    }
   }
 
-}
+  logout(): void {
+    Parse.User.logOut().then(async () => {
+      this.name = ''
+      this.userName = ''
+      this.errorMessage = ''
+      // this.errorMessage = '登出成功';
+      this.isLoggedIn = false;
+      const alert = await this.alertController.create({
+        header: '退出成功',
+        message: '您已退出账号。',
+        buttons: ['确定']
+      });
+      await alert.present();
+
+       // location.reload(); // 刷新页面
+    });
+  }
+}

+ 5 - 3
src/app/tab1/tab1.page.html

@@ -1,7 +1,7 @@
 <ion-header [translucent]="true">
   <ion-toolbar>
     <ion-title>
-      Tab 1
+      使用说明
     </ion-title>
   </ion-toolbar>
 </ion-header>
@@ -9,9 +9,11 @@
 <ion-content [fullscreen]="true">
   <ion-header collapse="condense">
     <ion-toolbar>
-      <ion-title size="large">Tab 1</ion-title>
+      <ion-title size="large">使用说明</ion-title>
     </ion-toolbar>
   </ion-header>
 
-  <app-explore-container name="Tab 1 page"></app-explore-container>
+  <div *ngIf="safeHTML" [innerHTML]="safeHTML"></div>
+  <ion-button expand="block" routerLink="/tabs/tab2">示例:通讯录列表</ion-button>
+  <ion-button expand="block" (click)="openGit()">代码:study-ng-contact</ion-button>
 </ion-content>

+ 4 - 0
src/app/tab1/tab1.page.scss

@@ -0,0 +1,4 @@
+iframe{
+    width:100%;
+    height:100%;
+}

+ 8 - 1
src/app/tab1/tab1.page.ts

@@ -1,4 +1,5 @@
 import { Component } from '@angular/core';
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
 
 @Component({
   selector: 'app-tab1',
@@ -7,6 +8,12 @@ import { Component } from '@angular/core';
 })
 export class Tab1Page {
 
-  constructor() {}
+  safeHTML: SafeResourceUrl|undefined
+  constructor(private domSan:DomSanitizer) {
+    this.safeHTML = domSan.bypassSecurityTrustHtml(``)
+  }
+  openGit(){
+    window.open(`http://git.fmode.cn:3000/nkkj/study-ng-contact`,"_blank")
+  }
 
 }

+ 2 - 6
src/app/tabs/tabs-routing.module.ts

@@ -13,15 +13,11 @@ const routes: Routes = [
       },
       {
         path: 'tab2',
-        loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
+        loadChildren: () => import('../../modules/contact/contact-list/contact-list.module').then(mod => mod.ContactListPageModule)
       },
       {
         path: 'tab3',
-        loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule)
-      },
-      {
-        path: 'tab-mine',
-        loadChildren: () => import('../tab-mine/tab-mine.module').then( m => m.TabMinePageModule)
+        loadChildren: () => import('../tab-mine/tab-mine.module').then(m => m.TabMinePageModule)
       },
 
       {

+ 8 - 17
src/app/tabs/tabs.page.html

@@ -2,28 +2,19 @@
 
   <ion-tab-bar slot="bottom">
     <ion-tab-button tab="tab1" href="/tabs/tab1">
-      <ion-icon aria-hidden="true" name="home"></ion-icon>
-      <ion-label>首页</ion-label>
+      <ion-icon aria-hidden="true" name="document"></ion-icon>
+      <ion-label>说明</ion-label>
     </ion-tab-button>
 
     <ion-tab-button tab="tab2" href="/tabs/tab2">
-      <ion-icon aria-hidden="true" name="cart"></ion-icon>
-      <ion-label>市集</ion-label>
-    </ion-tab-button>
-
-    <ion-tab-button disabled>
-      <ion-icon aria-hidden="true" name="add-circle"></ion-icon>
+      <ion-icon aria-hidden="true" name="ellipse"></ion-icon>
+      <ion-label>通讯录</ion-label>
     </ion-tab-button>
 
     <ion-tab-button tab="tab3" href="/tabs/tab3">
-      <ion-icon aria-hidden="true" name="compass"></ion-icon>
-      <ion-label>发现</ion-label>
-    </ion-tab-button>
-
-  <ion-tab-button tab="tab-mine" href="/tabs/tab-mine">
-    <ion-icon aria-hidden="true"  name="person"></ion-icon>
+      <ion-icon aria-hidden="true" name="person"></ion-icon>
       <ion-label>我的</ion-label>
-  </ion-tab-button>
-</ion-tab-bar>
+    </ion-tab-button>
+  </ion-tab-bar>
 
-</ion-tabs>
+</ion-tabs>

+ 17 - 0
src/modules/contact/contact-detail/contact-detail-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ContactDetailPage } from './contact-detail.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: ContactDetailPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class ContactDetailPageRoutingModule {}

+ 20 - 0
src/modules/contact/contact-detail/contact-detail.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { ContactDetailPageRoutingModule } from './contact-detail-routing.module';
+
+import { ContactDetailPage } from './contact-detail.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    ContactDetailPageRoutingModule
+  ],
+  declarations: [ContactDetailPage]
+})
+export class ContactDetailPageModule {}

+ 13 - 0
src/modules/contact/contact-detail/contact-detail.page.html

@@ -0,0 +1,13 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>contact-detail</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">contact-detail</ion-title>
+    </ion-toolbar>
+  </ion-header>
+</ion-content>

+ 0 - 0
src/modules/contact/contact-detail/contact-detail.page.scss


+ 17 - 0
src/modules/contact/contact-detail/contact-detail.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ContactDetailPage } from './contact-detail.page';
+
+describe('ContactDetailPage', () => {
+  let component: ContactDetailPage;
+  let fixture: ComponentFixture<ContactDetailPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ContactDetailPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 15 - 0
src/modules/contact/contact-detail/contact-detail.page.ts

@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-contact-detail',
+  templateUrl: './contact-detail.page.html',
+  styleUrls: ['./contact-detail.page.scss'],
+})
+export class ContactDetailPage implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}

+ 17 - 0
src/modules/contact/contact-list/contact-list-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ContactListPage } from './contact-list.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: ContactListPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class ContactListPageRoutingModule {}

+ 20 - 0
src/modules/contact/contact-list/contact-list.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { ContactListPageRoutingModule } from './contact-list-routing.module';
+
+import { ContactListPage } from './contact-list.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    ContactListPageRoutingModule
+  ],
+  declarations: [ContactListPage]
+})
+export class ContactListPageModule {}

+ 34 - 0
src/modules/contact/contact-list/contact-list.page.html

@@ -0,0 +1,34 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>通讯录</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">通讯录</ion-title>
+    </ion-toolbar>
+  </ion-header>
+  
+  <ion-searchbar [(ngModel)]="searchName" (ionInput)="search()"></ion-searchbar>
+
+  <ion-list>
+    <ng-container *ngFor="let contact of contactList;let index = index;">
+      <!-- 分组:根据下标首个出现的元素显示分组分隔符 -->
+      <ion-item-divider *ngIf="charGroupIndex[contact.get('firstChar')] == index">
+        <ion-label>{{contact.get('firstChar')}}</ion-label>
+      </ion-item-divider>
+      <ion-item lines="none">
+        <ion-avatar slot="start">
+          <img [src]="contact.get('avatarUrl') || 'https://ionicframework.com/docs/img/demos/avatar.svg'">
+        </ion-avatar>
+        <ion-label>
+          <h2>{{ contact.get('name') }}</h2>
+          <p>性别: {{ contact.get('gender') }}</p>
+          <p>手机: {{ contact.get('mobile') }}</p>
+        </ion-label>
+      </ion-item>
+    </ng-container>
+  </ion-list>
+</ion-content>

+ 0 - 0
src/modules/contact/contact-list/contact-list.page.scss


+ 17 - 0
src/modules/contact/contact-list/contact-list.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ContactListPage } from './contact-list.page';
+
+describe('ContactListPage', () => {
+  let component: ContactListPage;
+  let fixture: ComponentFixture<ContactListPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ContactListPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 42 - 0
src/modules/contact/contact-list/contact-list.page.ts

@@ -0,0 +1,42 @@
+import { Component, OnInit } from '@angular/core';
+import * as Parse from "parse";
+@Component({
+  selector: 'app-contact-list',
+  templateUrl: './contact-list.page.html',
+  styleUrls: ['./contact-list.page.scss'],
+})
+export class ContactListPage implements OnInit {
+  searchName: string = '';
+  contactList: Array<Parse.Object> = [];
+
+  ngOnInit() {
+    this.loadContact();
+  }
+
+  charGroupIndex:any = {}
+  loadContact() {
+    const Contact = Parse.Object.extend('Contact');
+    const query = new Parse.Query(Contact);
+    query.ascending('firstChar');
+
+    if (this.searchName) {
+      query.contains('name', this.searchName);
+    }
+
+    query.find().then((results) => {
+      this.contactList = results;
+      this.contactList.forEach((contact,index)=>{
+        if(this.charGroupIndex[contact.get("firstChar")] == undefined){
+          this.charGroupIndex[contact.get("firstChar")] = index
+        }
+      })
+    }, (error) => {
+      console.error('Error while fetching contacts', error);
+    });
+  }
+
+  search() {
+    this.loadContact();
+  }
+
+}

+ 12 - 0
src/modules/contact/contact-routing.module.ts

@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+const routes: Routes = [
+  {path: 'list', loadChildren: () => import('./contact-list/contact-list.module').then(mod => mod.ContactListPageModule)},
+  {path: 'detail/:id', loadChildren: () => import('./contact-detail/contact-detail.module').then(mod => mod.ContactDetailPageModule)},
+];
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class ContactRoutingModule { }

+ 14 - 0
src/modules/contact/contact.module.ts

@@ -0,0 +1,14 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ContactRoutingModule } from './contact-routing.module';
+
+
+@NgModule({
+  declarations: [],
+  imports: [
+    CommonModule,
+    ContactRoutingModule
+  ]
+})
+export class ContactModule { }

+ 18 - 0
src/modules/user/guard-auth/auth.guard.ts

@@ -0,0 +1,18 @@
+import { CanActivateFn } from '@angular/router';
+import * as Parse from "parse";
+
+export const authGuard: CanActivateFn = (route, state) => {
+  // 检查当前本地存储中,是否有用户验证信息
+  let userAuth = Parse.User.current();
+  if(userAuth?.id){
+    return true;
+  }else{
+    // 暂时存储登陆前用户所在页面
+    let REDIRECT_URL = location.pathname;
+    localStorage.setItem("REDIRECT_URL",REDIRECT_URL);
+   
+        location.href = "/user/login"
+    
+    return false;
+  }
+};

+ 15 - 0
src/modules/user/guard-auth/auth.local.guard.ts

@@ -0,0 +1,15 @@
+import { CanActivateFn } from '@angular/router';
+
+export const authLocalGuard: CanActivateFn = (route, state) => {
+  // 检查当前本地存储中,是否有用户验证信息
+  let userAuth = localStorage.getItem("USER_AUTH")
+  if(userAuth){
+    return true;
+  }else{
+    // 暂时存储登陆前用户所在页面
+    let REDIRECT_URL = location.pathname;
+    localStorage.setItem("REDIRECT_URL",REDIRECT_URL);
+    location.href = "/user/login"
+    return false;
+  }
+};

+ 49 - 0
src/modules/user/page-info/page-info.component.html

@@ -0,0 +1,49 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>资料编辑</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">资料编辑</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <ion-card>
+    <ion-card-header>
+      <ion-card-title>{{currentUser?.get('username')}} - {{currentUser?.id}}</ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <ion-list>
+        <ion-item>
+          <ion-input label="姓名" type="text" [(ngModel)]="userInfo.name"></ion-input>
+        </ion-item>
+        <ion-item>
+          <ion-input label="手机" type="tel" [(ngModel)]="userInfo.mobile"></ion-input>
+        </ion-item>
+        <ion-item>
+          <ion-select label="性别" [(ngModel)]="userInfo.gender">
+            <ion-select-option value="男">男</ion-select-option>
+            <ion-select-option value="女">女</ion-select-option>
+          </ion-select>
+        </ion-item>
+        <ion-item>
+          <ion-label>生日</ion-label>
+          <ion-datetime-button datetime="birthday"></ion-datetime-button>
+          <ion-modal [keepContentsMounted]="true">
+            <ng-template>
+              <ion-datetime id="birthday" displayFormat="MM/DD/YYYY" [(ngModel)]="userInfo.birthday"></ion-datetime>
+            </ng-template>
+          </ion-modal>
+        </ion-item>
+      </ion-list>
+    </ion-card-content>
+  </ion-card>
+
+  <ion-button expand="block" (click)="save()">保存</ion-button>
+  <ion-button expand="block" (click)="cancel()">取消</ion-button>
+</ion-content>
+
+

+ 0 - 0
src/modules/user/page-info/page-info.component.scss


+ 21 - 0
src/modules/user/page-info/page-info.component.spec.ts

@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageInfoComponent } from './page-info.component';
+
+describe('PageInfoComponent', () => {
+  let component: PageInfoComponent;
+  let fixture: ComponentFixture<PageInfoComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [PageInfoComponent]
+    });
+    fixture = TestBed.createComponent(PageInfoComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 56 - 0
src/modules/user/page-info/page-info.component.ts

@@ -0,0 +1,56 @@
+import { Component, OnInit } from '@angular/core';
+import { AlertController, NavController } from '@ionic/angular';
+import * as Parse from 'parse';
+
+@Component({
+  selector: 'app-page-info',
+  templateUrl: './page-info.component.html',
+  styleUrls: ['./page-info.component.scss']
+})
+export class PageInfoComponent implements OnInit {
+
+  userInfo: any = {
+    name: '',
+    mobile: '',
+    gender: '',
+    birthday: ''
+  };
+  currentUser: Parse.User | undefined;
+
+  constructor(private navController: NavController) {}
+
+  ngOnInit() {
+    this.currentUser = Parse.User.current();
+    if (this.currentUser) {
+      let json = this.currentUser.toJSON();
+      for (const key in json) {
+        if (this.userInfo.hasOwnProperty(key)) {
+          this.userInfo[key] = json[key];
+        }
+      }
+    }
+    console.log(this.userInfo);
+  }
+
+  save() {
+    this.currentUser = Parse.User.current();
+    if (this.currentUser) {
+      console.log(this.userInfo);
+      for (const key in this.userInfo) {
+        if (this.userInfo.hasOwnProperty(key)) {
+          this.currentUser.set(key, this.userInfo[key]);
+        }
+      }
+      this.currentUser.save().then(() => {
+        this.navController.back();
+      }).catch((error) => {
+        console.error('Error saving user data: ', error);
+      });
+    }
+  }
+
+  cancel() {
+    this.navController.back();
+  }
+
+}

+ 45 - 0
src/modules/user/page-login/page-login.component.html

@@ -0,0 +1,45 @@
+
+  <!-- IonicModule + FormsModule实现,默认好看 -->
+<div class="login-modal">
+    <ion-card>
+        <ion-card-header>
+        <ion-card-title>登录</ion-card-title>
+        <ion-card-subtitle>该页面需登陆可继续使用</ion-card-subtitle>
+        </ion-card-header>
+    
+        <ion-card-content>
+        <ion-input [(ngModel)]="userData.username"
+            label="账号/手机" 
+            labelPlacement="floating" 
+            [counter]="true" 
+            maxlength="20">
+        </ion-input>
+
+        <ion-input [(ngModel)]="userData.password"
+            label="密码"
+            labelPlacement="floating"
+            [counter]="true"
+            maxlength="20"
+        ></ion-input>
+            
+        </ion-card-content>
+    
+        <ion-button expand="block" (click)="login()">登录</ion-button>
+        <ion-button expand="block" fill="clear"routerLink="../../tabs/tab3" >返回</ion-button>
+    </ion-card>
+</div>
+
+
+  <!-- 纯HTML + FormsModule实现,可任意定制样式 -->
+<ng-container *ngIf="false">
+
+    请输入用户名:{{userData.username}} <br/>
+    
+    <input [(ngModel)]="userData.username" type="text"/><br/>
+    
+    请输入密码:{{userData.password}}<br/>
+    <input [(ngModel)]="userData.password" type="password"/><br/>
+    
+    <button (click)="login()">登录</button>
+
+</ng-container>

+ 8 - 0
src/modules/user/page-login/page-login.component.scss

@@ -0,0 +1,8 @@
+.login-modal{
+    height:100vh;
+    width: 100vw;
+    display: flex;
+    flex-direction: column;
+    vertical-align: middle;
+    justify-content: center;
+}

+ 21 - 0
src/modules/user/page-login/page-login.component.spec.ts

@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageLoginComponent } from './page-login.component';
+
+describe('PageLoginComponent', () => {
+  let component: PageLoginComponent;
+  let fixture: ComponentFixture<PageLoginComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [PageLoginComponent]
+    });
+    fixture = TestBed.createComponent(PageLoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 51 - 0
src/modules/user/page-login/page-login.component.ts

@@ -0,0 +1,51 @@
+import * as Parse from 'parse';
+import { AlertController } from '@ionic/angular';
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'app-page-login',
+  templateUrl: './page-login.component.html',
+  styleUrls: ['./page-login.component.scss']
+})
+export class PageLoginComponent {
+  userData = {
+    username: '',
+    password: ''
+  };
+
+  constructor(
+    private alertController: AlertController,
+    private router: Router
+  ) {
+    (Parse as any).serverURL = 'https://web2023.fmode.cn/parse';
+    Parse.initialize('dev');
+  }
+
+  async login() {
+    const { username, password } = this.userData;
+
+    try {
+      const user = await Parse.User.logIn(username, password);
+
+      const alert = await this.alertController.create({
+        header: '登录成功',
+        message: '您已成功登录。',
+        buttons: ['确定']
+      });
+
+      await alert.present();
+
+      // 导航到目标页面
+      this.router.navigateByUrl('tabs/tab3');
+    } catch (error: any) {
+      const alert = await this.alertController.create({
+        header: '登录失败',
+        message: error.message,
+        buttons: ['确定']
+      });
+
+      await alert.present();
+    }
+  }
+}

+ 45 - 0
src/modules/user/page-register/page-register.component.html

@@ -0,0 +1,45 @@
+
+<div class="register-modal">
+    <ion-card>
+        <ion-card-header>
+            <ion-card-title>注册</ion-card-title>
+            <ion-card-subtitle>创建新账号</ion-card-subtitle>
+        </ion-card-header>
+
+        <ion-card-content>
+            <ion-input [(ngModel)]="userData.username"
+                       label="用户名"
+                       labelPlacement="floating"
+                       [counter]="true"
+                       maxlength="20">
+            </ion-input>
+
+            <ion-input [(ngModel)]="userData.password"
+                       label="密码"
+                       labelPlacement="floating"
+                       [counter]="true"
+                       maxlength="20"
+            ></ion-input>
+
+        </ion-card-content>
+
+        <ion-button expand="block" (click)="register()">注册</ion-button>
+        <ion-button expand="block" fill="clear" routerLink="../../tabs/tab3">返回</ion-button>
+    </ion-card>
+</div>
+
+
+<!-- Pure HTML + FormsModule implementation, customizable styling -->
+<ng-container *ngIf="false">
+
+    请输入用户名:{{userData.username}} <br/>
+
+    <input [(ngModel)]="userData.username" type="text"/><br/>
+
+    请输入密码:{{userData.password}}<br/>
+    <input [(ngModel)]="userData.password" type="password"/><br/>
+
+    <button (click)="register()">注册</button>
+
+</ng-container>
+

+ 8 - 0
src/modules/user/page-register/page-register.component.scss

@@ -0,0 +1,8 @@
+.register-modal{
+    height:100vh;
+    width: 100vw;
+    display: flex;
+    flex-direction: column;
+    vertical-align: middle;
+    justify-content: center;
+}

+ 21 - 0
src/modules/user/page-register/page-register.component.spec.ts

@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageRegisterComponent } from './page-register.component';
+
+describe('PageRegisterComponent', () => {
+  let component: PageRegisterComponent;
+  let fixture: ComponentFixture<PageRegisterComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [PageRegisterComponent]
+    });
+    fixture = TestBed.createComponent(PageRegisterComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 56 - 0
src/modules/user/page-register/page-register.component.ts

@@ -0,0 +1,56 @@
+import * as Parse from 'parse';
+import { AlertController } from '@ionic/angular';
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'app-page-register',
+  templateUrl: './page-register.component.html',
+  styleUrls: ['./page-register.component.scss']
+})
+export class PageRegisterComponent {
+  userData = {
+    username: '',
+    password: ''
+  };
+
+  constructor(
+    private alertController: AlertController,
+    private router: Router
+  ) {
+    (Parse as any).serverURL = 'https://web2023.fmode.cn/parse';
+    Parse.initialize('dev');
+  }
+
+  async register() {
+    const { username, password } = this.userData;
+
+    try {
+      const user = new Parse.User();
+      user.set('username', username);
+      user.set('password', password);
+
+      await user.signUp();
+
+      const alert = await this.alertController.create({
+        header: '注册成功',
+        message: '您已成功注册账号。',
+        buttons: ['确定']
+      });
+
+      await alert.present();
+
+      // 导航到目标页面
+      this.router.navigateByUrl('tabs/tab3');
+    } catch (error: any) {
+      const alert = await this.alertController.create({
+        header: '注册失败',
+        message: error.message,
+        buttons: ['确定']
+      });
+
+      await alert.present();
+    }
+  }
+}
+

+ 16 - 0
src/modules/user/service-user/user.service.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+  let service: UserService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(UserService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});

+ 29 - 0
src/modules/user/service-user/user.service.ts

@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import * as Parse from "parse";
+@Injectable({
+  providedIn: 'root'
+})
+export class UserService {
+  constructor() {
+    
+  }
+
+  /**
+   * 检查用户密码是否正确
+   */
+  async loginUserPassword(userData:{
+    username:string
+    password:string
+  }){
+    try{
+      let user = await Parse.User.logIn(userData.username,userData.password)
+      if(user?.id){
+        return true
+      }
+      return false
+    }catch(err){
+      console.error(err)
+      return false
+    }
+  }
+}

+ 23 - 0
src/modules/user/user-routing.module.ts

@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { authGuard } from './guard-auth/auth.guard';
+import { PageInfoComponent } from './page-info/page-info.component';
+import { PageLoginComponent } from './page-login/page-login.component';
+import { PageRegisterComponent } from './page-register/page-register.component';
+
+
+const routes: Routes = [
+  {path:"login",component:PageLoginComponent},
+  {path:"register",component:PageRegisterComponent},
+  {
+    path:"info",component:PageInfoComponent,
+    canActivate:[authGuard]
+  },
+
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class UserRoutingModule { }

+ 27 - 0
src/modules/user/user.module.ts

@@ -0,0 +1,27 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { UserRoutingModule } from './user-routing.module';
+import { PageLoginComponent } from './page-login/page-login.component';
+import { PageRegisterComponent } from './page-register/page-register.component';
+import { PageInfoComponent } from './page-info/page-info.component';
+import { FormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+
+
+
+@NgModule({
+  declarations: [
+    PageLoginComponent,
+    PageRegisterComponent,
+    PageInfoComponent,
+    
+  ],
+  imports: [
+    CommonModule,
+    UserRoutingModule,
+    FormsModule,
+    IonicModule,
+  ]
+})
+export class UserModule { }