0225138 4 bulan lalu
induk
melakukan
5391f04dc4
39 mengubah file dengan 1227 tambahan dan 6 penghapusan
  1. 2 1
      angular.json
  2. 26 2
      src/app/app-routing.module.ts
  3. 17 0
      src/app/contact-list/contact-list-routing.module.ts
  4. 20 0
      src/app/contact-list/contact-list.module.ts
  5. 70 0
      src/app/contact-list/contact-list.page.html
  6. 0 0
      src/app/contact-list/contact-list.page.scss
  7. 26 0
      src/app/contact-list/contact-list.page.spec.ts
  8. 81 0
      src/app/contact-list/contact-list.page.ts
  9. 2 1
      src/app/tab3/tab3.page.ts
  10. 79 0
      src/modules/contact/README.md
  11. 13 0
      src/modules/contact/chat/chat-routing.module.ts
  12. 18 0
      src/modules/contact/chat/chat.module.ts
  13. 66 0
      src/modules/contact/chat/chat.page.html
  14. 79 0
      src/modules/contact/chat/chat.page.scss
  15. 24 0
      src/modules/contact/chat/chat.page.spec.ts
  16. 120 0
      src/modules/contact/chat/chat.page.ts
  17. 17 0
      src/modules/contact/contact-detail/contact-detail-routing.module.ts
  18. 20 0
      src/modules/contact/contact-detail/contact-detail.module.ts
  19. 13 0
      src/modules/contact/contact-detail/contact-detail.page.html
  20. 0 0
      src/modules/contact/contact-detail/contact-detail.page.scss
  21. 17 0
      src/modules/contact/contact-detail/contact-detail.page.spec.ts
  22. 15 0
      src/modules/contact/contact-detail/contact-detail.page.ts
  23. 17 0
      src/modules/contact/contact-list/contact-list-routing.module.ts
  24. 20 0
      src/modules/contact/contact-list/contact-list.module.ts
  25. 70 0
      src/modules/contact/contact-list/contact-list.page.html
  26. 0 0
      src/modules/contact/contact-list/contact-list.page.scss
  27. 26 0
      src/modules/contact/contact-list/contact-list.page.spec.ts
  28. 81 0
      src/modules/contact/contact-list/contact-list.page.ts
  29. 12 0
      src/modules/contact/contact-routing.module.ts
  30. 14 0
      src/modules/contact/contact.module.ts
  31. 14 0
      src/modules/contact/session/session-routing.module.ts
  32. 20 0
      src/modules/contact/session/session.module.ts
  33. 37 0
      src/modules/contact/session/session.page.html
  34. 69 0
      src/modules/contact/session/session.page.scss
  35. 24 0
      src/modules/contact/session/session.page.spec.ts
  36. 69 0
      src/modules/contact/session/session.page.ts
  37. 8 0
      src/modules/contact/user-name.pipe.spec.ts
  38. 19 0
      src/modules/contact/user-name.pipe.ts
  39. 2 2
      src/modules/user/login/login.page.ts

+ 2 - 1
angular.json

@@ -136,7 +136,8 @@
   "cli": {
     "schematicCollections": [
       "@ionic/angular-toolkit"
-    ]
+    ],
+    "analytics": "a44e3817-b82c-40d8-ad1f-b72caa960b41"
   },
   "schematics": {
     "@ionic/angular-toolkit:component": {

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

@@ -1,7 +1,12 @@
 import { NgModule } from '@angular/core';
 import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
-import { LoginPage } from '../modules/user/login/login.page';
+//import { LoginPage } from '../modules/user/login/login.page';
 const routes: Routes = [
+  {
+    path: '',
+    redirectTo: 'home',
+    pathMatch: 'full'
+  },
   {
     path: '',
     loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
@@ -25,7 +30,26 @@ const routes: Routes = [
   {
     path: 'edit-info',
     loadChildren: () => import('../modules/user/edit-info/edit-info.module').then(m => m.EditInfoPageModule)
-  }
+  },
+  // {
+  //   path: 'contact',
+  //   children:[
+  //     {
+  //       path: 'chat',
+  //       loadChildren: () => import('../modules/contact/chat/chat.module').then(m => m.ChatPageModule)
+  //     },
+  //     {
+  //       path: 'chat/:userid',
+  //       loadChildren: () => import('../modules/contact/chat/chat.module').then(m => m.ChatPageModule)
+  //     },
+  //   ]
+  // },
+  {
+    path: 'contact-list',
+    loadChildren: () => import('./contact-list/contact-list.module').then( m => m.ContactListPageModule)
+  },
+
+
 ];
 @NgModule({
   imports: [

+ 17 - 0
src/app/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/app/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 {}

+ 70 - 0
src/app/contact-list/contact-list.page.html

@@ -0,0 +1,70 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>通讯录</ion-title>
+    <ion-buttons slot="end">
+      <ion-button id="add-contact">
+        添加
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-modal trigger="add-contact" #modal>
+  <ng-template>
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-button>取消</ion-button>
+        </ion-buttons>
+        <ion-title>添加联系人</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="addContact()" [strong]="true">确认</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+    <ion-content class="ion-padding">
+      <ion-item>
+        <ion-input  label="联系人" placeholder="输入用户名" [(ngModel)]="usernameInput" type="text"></ion-input>
+      </ion-item>
+    </ion-content>
+   
+  </ng-template>
+</ion-modal>
+
+<ion-content [fullscreen]="true">
+  
+  
+  <ion-searchbar [(ngModel)]="searchName" (ionInput)="search()"></ion-searchbar>
+
+  
+  <ion-segment [(ngModel)]="segment" (click)="loadContact()">
+    <ion-segment-button value="me">
+      我的
+    </ion-segment-button>
+    <ion-segment-button value="all">
+      全部
+    </ion-segment-button>
+  </ion-segment>
+
+  <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') || contact?.get('to')?.get("nickname") ||contact?.get('to')?.get('username') }}</h2>
+          <p>性别: {{ contact.get('gender') || contact?.get('to')?.get("gender") }}</p>
+          <p>手机: {{ contact.get('mobile') || contact?.get('to')?.get("mobile")}}</p>
+        </ion-label>
+        <ion-button slot="end" routerLink="/contact/chat/{{contact?.get('to')?.id}}">聊天</ion-button>
+      </ion-item>
+    </ng-container>
+  </ion-list>
+  <ion-button type="button" (click)="clicked()">Click me!</ion-button>
+    <span>{{ clickMsg }}</span>
+</ion-content>

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


+ 26 - 0
src/app/contact-list/contact-list.page.spec.ts

@@ -0,0 +1,26 @@
+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();
+  });
+
+  it('#clicked() should toggle #isOn', () => {
+    const comp = new ContactListPage();
+    expect(comp.isOn).withContext('off at first').toBe(false);
+    comp.clicked();
+    expect(comp.isOn).withContext('on after click').toBe(true);
+    comp.clicked();
+    expect(comp.isOn).withContext('off after second click').toBe(false);
+  });
+});

+ 81 - 0
src/app/contact-list/contact-list.page.ts

@@ -0,0 +1,81 @@
+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> = [];
+  segment:string = "me";
+  ngOnInit() {
+    this.loadContact();
+  }
+
+  usernameInput:string = ""
+  async checkIfExists(toUser:Parse.Object){
+    let query = new Parse.Query("Contact");
+    query.equalTo("from",Parse.User.current()?.toPointer())
+    query.equalTo("to",toUser.toPointer())
+    return await query.first();
+  }
+  async addContact(){
+    if(this.usernameInput){
+      let query = new Parse.Query("_User");
+      query.equalTo("username",this.usernameInput);
+      let user = await query.first()
+      if(user?.id){
+        let exists = await this.checkIfExists(user)
+        if(exists?.id) return
+        let Contact = Parse.Object.extend("Contact");
+        let contact = new Contact()
+        contact.set("from",Parse.User.current()?.toPointer())
+        contact.set("to",user.toPointer())
+        await contact.save();
+        this.loadContact();
+      }else{
+        // 提示找不到用户
+      }
+    }
+  }
+  charGroupIndex:any = {}
+  loadContact() {
+    const Contact = Parse.Object.extend('Contact');
+    const query = new Parse.Query(Contact);
+    query.include("to")
+    query.ascending('firstChar');
+    if(this.segment=="me"){
+      if(!Parse.User.current()?.id) return
+      query.equalTo("from",Parse.User.current()?.id)
+    }
+
+    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();
+  }
+
+  isOn:boolean = false;
+  clicked(){
+    this.isOn = !this.isOn;
+  }
+  get clickMsg(){
+      return `The light is ${this.isOn ? 'On' : 'Off'}`;
+  }
+
+}

+ 2 - 1
src/app/tab3/tab3.page.ts

@@ -42,7 +42,8 @@ export class Tab3Page {
   }
    
   goToLogin() {
+    
+    this.navCtrl.navigateForward('/login');
     console.log('Navigating to Login page');
-    this.navCtrl.navigateForward('/src/app/modules/user/login');
   }
 }

+ 79 - 0
src/modules/contact/README.md

@@ -0,0 +1,79 @@
+# 通讯模块
+
+# 项目结构
+- chat 对话页面
+- session 历史会话
+- contact-list 通讯录列表
+
+
+# 对话模块
+- 功能简介:两个用户互相发消息,接收消息,查看消息历史记录
+
+## 数据范式
+- _User 用户表
+    - objectId
+    - username 用户名
+    - mobile 手机号
+    - nickname 昵称
+- Contact 通讯好友的表
+    - from Pointer<_User>
+    - to Pointer<_User>
+- Message 消息表
+    - sendUser Pointer<_User>
+    - receiveUser Pointer<_User>
+    - contentJson 消息对象 符合各类消息格式
+    - isRead Boolean 消息已读
+    - isCancel Boolean 消息撤回
+
+思考:图片链接和图片消息的区别,和表达方式?
+图片链接,本质上是文本消息(基于字符串)
+图片消息,本质上是结构化消息 {type:"image",imgUrl:"https://file-cloud.fmode.cn/E4KpGvTEto/20230822/3mkf41033623275.png"}
+
+### 消息内容规范(参考GPT多模态响应)
+- 参考文档:https://ai.fmode.cn/chat/share/nxZMG3CZrd
+
+普通文本消息:
+
+{
+      "role": "user",
+      "content": "Here is the text: 'The quick brown fox jumps over the lazy dog.'"
+}
+
+ 图片消息
+{
+    "role": "user",
+    "content": {
+    "type": "image",
+    "data": {
+        "url": "https://example.com/image.jpg",
+        "alt_text": "A quick brown fox jumping over a lazy dog."
+    }
+    }
+}
+
+音频消息
+
+{
+    "role": "user",
+    "content": {
+    "type": "audio",
+    "data": {
+        "url": "https://example.com/audio.mp3",
+        "alt_text": "An audio clip of a quick brown fox jumping over a lazy dog."
+    }
+    }
+}
+
+### 业务逻辑
+
+#### 发送消息
+- 进入会话页面
+    - 无记录:输入用户昵称添加好友,再进行对话
+    - 有记录:点击历史会话进入
+- 向微服务创建一个Message,sendUser为自身,receiveUser为联系人
+
+#### 接收消息
+- 查询,receiveUser为自身,sendUser为会话窗口联系人的所有Message
+
+#### 会话列表
+- 查询,receiveUser为自身,所有Message,并每个receiver只显示最新一条

+ 13 - 0
src/modules/contact/chat/chat-routing.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ChatPage } from './chat.page';
+// import { Chat1Page } from './tab1 copy/tab11.page';
+const routes: Routes = [
+  { path: '', component: ChatPage },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class ChatPageRoutingModule {}

+ 18 - 0
src/modules/contact/chat/chat.module.ts

@@ -0,0 +1,18 @@
+import { IonicModule } from '@ionic/angular';
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ChatPage } from './chat.page';
+
+import { ChatPageRoutingModule } from './chat-routing.module';
+
+@NgModule({
+  imports: [
+    IonicModule,
+    CommonModule,
+    FormsModule,
+    ChatPageRoutingModule
+  ],
+  declarations: [ChatPage]
+})
+export class ChatPageModule {}

+ 66 - 0
src/modules/contact/chat/chat.page.html

@@ -0,0 +1,66 @@
+<ion-header>
+  <ion-toolbar color="primary">
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/session"></ion-back-button>
+    </ion-buttons>
+    <ion-title>{{ contact?.get('name') || contact?.get('to')?.get("nickname") ||contact?.get('to')?.get('username') }}</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="chat-content">
+  <ion-list lines="none">
+    <ion-item *ngFor="let message of messageList" class="message-item">
+      <ion-label class="message-container"
+                 [class.sent-message-container]="message?.get('sendUser')?.id === user?.id"
+                 [class.received-message-container]="message?.get('receiveUser')?.id === targetId">
+        <div class="message-details">
+          <p [class.sent-message]="message?.get('sendUser')?.id === user?.id"
+             [class.received-message]="message?.get('receiveUser')?.id === targetId"
+             class="message-text">{{ message?.get("contentJson")?.content }}</p>
+          <div class="message-time">
+            <p>{{ message.createdAt|date:'HH:mm:ss' }}</p>
+          </div>
+        </div>
+      </ion-label>
+      <ion-avatar slot="end" *ngIf="message?.get('sendUser')?.id === user?.id" class="message-avatar sent-avatar">
+        <img [src]="'/assets/img/clock.png'">
+      </ion-avatar>
+      <ion-avatar slot="start" *ngIf="message?.get('receiveUser')?.id === targetId" class="message-avatar received-avatar">
+        <img [src]="'/assets/img/clock.png'">
+      </ion-avatar>
+    </ion-item>
+  </ion-list>
+
+  <ion-list lines="none">
+    <ion-item *ngFor="let message of messages" class="message-item">
+      <ion-label class="message-container"
+                 [class.sent-message-container]="message.type === 'sent'"
+                 [class.received-message-container]="message.type === 'received'">
+        <div class="message-details">
+          <p [class.sent-message]="message.type === 'sent'"
+             [class.received-message]="message.type === 'received'"
+             class="message-text">{{ message.text }}</p>
+          <div class="message-time">
+            <p>{{ message.time }}</p>
+          </div>
+        </div>
+      </ion-label>
+      <ion-avatar slot="end" *ngIf="message.type === 'sent'" class="message-avatar sent-avatar">
+        <img [src]="message.avatar">
+      </ion-avatar>
+      <ion-avatar slot="start" *ngIf="message.type === 'received'" class="message-avatar received-avatar">
+        <img [src]="message.avatar">
+      </ion-avatar>
+    </ion-item>
+  </ion-list>
+</ion-content>
+
+<ion-footer>
+  <ion-toolbar>
+    <ion-input placeholder="Type your message..." [(ngModel)]="newMessage" clearInput></ion-input>
+    <ion-buttons slot="end">
+      <ion-button (click)="sendMessage()" color="primary" [disabled]="!newMessage">Send</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-footer>
+

+ 79 - 0
src/modules/contact/chat/chat.page.scss

@@ -0,0 +1,79 @@
+.chat-content {
+  padding: 10px;
+}
+
+ion-item.message-item {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  margin-bottom: 10px;
+  max-width: 80%; /* 控制消息框的最大宽度 */
+}
+
+.message-container {
+  padding: 8px 12px;
+  display: inline-block;
+  max-width: 80%; /* 控制消息内容的最大宽度 */
+}
+
+.sent {
+  justify-content: flex-end; /* 将发送的消息右对齐 */
+}
+
+.received {
+  justify-content: flex-start; /* 将接收的消息左对齐 */
+}
+
+.sent-message-container {
+  background-color: #dcf8c6; /* 发送消息的背景颜色 */
+  align-self: flex-end;
+  border-radius: 10px;
+  padding: 10px;
+  margin-bottom: 4px;
+  max-width: 70%; /* 适当调整最大宽度 */
+  text-align: right; /* 文本右对齐 */
+}
+
+.received-message-container {
+  background-color: #f0f0f0; /* 接收消息的背景颜色 */
+  align-self: flex-start;
+  border-radius: 10px;
+  padding: 10px;
+  margin-bottom: 4px;
+  max-width: 70%; /* 适当调整最大宽度 */
+  text-align: left; /* 文本左对齐 */
+}
+.sent-message-container[_ngcontent-ng-c914708361]{
+  max-width: 100%;
+}
+ion-avatar[_ngcontent-ng-c914708361]{
+  margin-top:28px;
+}
+.list-md{
+  width: 123%;
+}
+.received-message-container[_ngcontent-ng-c914708361]{
+  max-width: 100%;
+}
+
+.sent-message, .received-message {
+  color: #333; /* 消息文本颜色 */
+}
+
+.message-time {
+  font-size: 12px;
+  color: #888; /* 时间文本颜色 */
+  margin-top: 4px;
+}
+
+.message-details {
+  display: flex;
+  flex-direction: column;
+}
+
+ion-avatar {
+  width: 32px;
+  height: 32px;
+  margin-left: 8px;
+  margin-right: 8px;
+}

+ 24 - 0
src/modules/contact/chat/chat.page.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { ChatPage } from './chat.page';
+
+describe('ChatPage', () => {
+  let component: ChatPage;
+  let fixture: ComponentFixture<ChatPage>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ChatPage],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ChatPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 120 - 0
src/modules/contact/chat/chat.page.ts

@@ -0,0 +1,120 @@
+import { Component, OnInit } from '@angular/core';
+import { NavController } from '@ionic/angular';
+import { Router } from '@angular/router';
+import { ActivatedRoute } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+// import { Parse } from 'parse';
+
+@Component({
+  selector: 'app-chat',
+  templateUrl: 'chat.page.html',
+  styleUrls: ['chat.page.scss']
+})
+export class ChatPage implements OnInit{
+  userid: string;
+  messageList:any = []
+  messages = [
+    { type: 'received', avatar: '/assets/img/female.png', time: '10:30 AM', text: 'Hi there!' },
+    { type: 'sent', avatar: '/assets/img/female.png', time: '10:31 AM', text: 'Hey! How are you?' },
+    // Add more messages as needed
+  ];
+  
+  newMessage: string = '';
+
+  constructor(private route: ActivatedRoute) {
+    this.userid = this.route.snapshot.paramMap.get('userid') as string;
+    this.targetId = this.userid
+  }
+
+  contact:Parse.Object|undefined
+  user:Parse.User|undefined
+  targetId:string |undefined
+  async ngOnInit() {
+    this.user =  Parse.User.current()
+    let fromId = Parse.User.current()?.id
+    if(this.userid && fromId){
+      let query = new Parse.Query("Contact");
+      query.include("to")
+      query.equalTo("from",fromId);
+      query.equalTo("to",this.userid);
+      this.contact = await query.first();
+    }
+    this.loadHistory();
+    return
+  }
+  async sendMessage() {
+    if (this.newMessage.trim() === '') {
+      return;
+    }
+    let sendUser = Parse.User?.current()?.toPointer()
+    let receiveUser = {__type:"Pointer",className:"_User",objectId:this.userid}
+    let Message = Parse.Object.extend("Message");
+    let message = new Message();
+    message.set("sendUser",sendUser)
+    message.set("receiveUser",receiveUser)
+    message.set("contentJson",{
+      role:"user",
+      content: this.newMessage
+    })
+    message = await message.save()
+    if(message?.id){
+      this.messageList.push(message)
+      this.syncSession(message,sendUser,receiveUser);
+    }
+    this.newMessage = '';
+
+    this.loadHistory();
+  }
+  async syncSession(message:any,sendUser:any,receiveUser:any){
+    // 根据当前聊天,更新最新的会话状态表 MessageSession 记录最新的会话
+    let session = await this.checkSessionExists();
+    if(!session?.id){
+      let MessageSession = Parse.Object.extend("MessageSession")
+      session = new MessageSession()
+    }
+    session?.set("message",message?.toPointer())
+    session?.set("sendUser",sendUser)
+    session?.set("receiveUser",receiveUser)
+    session?.save();
+  }
+  async checkSessionExists(){
+    let query = Parse.Query.fromJSON('MessageSession',{where: {
+      $or: [
+        {
+          sendUser: this.user?.id,
+          receiveUser: this.targetId
+        },
+        {
+          sendUser: this.targetId,
+          receiveUser: this.user?.id
+        }
+      ]
+    }})
+    return await query.first()
+  }
+
+  getCurrentTime() {
+    const now = new Date();
+    return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+  }
+
+  async loadHistory(){
+    let query = Parse.Query.fromJSON('Message',{where: {
+      $or: [
+        {
+          sendUser: this.user?.id,
+          receiveUser: this.targetId
+        },
+        {
+          sendUser: this.targetId,
+          receiveUser: this.user?.id
+        }
+      ]
+    }})
+
+    let list = await query.find();
+    if(list?.length){
+      this.messageList = list
+    }
+  }
+}

+ 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 {}

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

@@ -0,0 +1,70 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>通讯录</ion-title>
+    <ion-buttons slot="end">
+      <ion-button id="add-contact">
+        添加
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-modal trigger="add-contact" #modal>
+  <ng-template>
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-button>取消</ion-button>
+        </ion-buttons>
+        <ion-title>添加联系人</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="addContact()" [strong]="true">确认</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+    <ion-content class="ion-padding">
+      <ion-item>
+        <ion-input  label="联系人" placeholder="输入用户名" [(ngModel)]="usernameInput" type="text"></ion-input>
+      </ion-item>
+    </ion-content>
+   
+  </ng-template>
+</ion-modal>
+
+<ion-content [fullscreen]="true">
+  
+  
+  <ion-searchbar [(ngModel)]="searchName" (ionInput)="search()"></ion-searchbar>
+
+  
+  <ion-segment [(ngModel)]="segment" (click)="loadContact()">
+    <ion-segment-button value="me">
+      我的
+    </ion-segment-button>
+    <ion-segment-button value="all">
+      全部
+    </ion-segment-button>
+  </ion-segment>
+
+  <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') || contact?.get('to')?.get("nickname") ||contact?.get('to')?.get('username') }}</h2>
+          <p>性别: {{ contact.get('gender') || contact?.get('to')?.get("gender") }}</p>
+          <p>手机: {{ contact.get('mobile') || contact?.get('to')?.get("mobile")}}</p>
+        </ion-label>
+        <ion-button slot="end" routerLink="/contact/chat/{{contact?.get('to')?.id}}">聊天</ion-button>
+      </ion-item>
+    </ng-container>
+  </ion-list>
+  <ion-button type="button" (click)="clicked()">Click me!</ion-button>
+    <span>{{ clickMsg }}</span>
+</ion-content>

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


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

@@ -0,0 +1,26 @@
+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();
+  });
+
+  it('#clicked() should toggle #isOn', () => {
+    const comp = new ContactListPage();
+    expect(comp.isOn).withContext('off at first').toBe(false);
+    comp.clicked();
+    expect(comp.isOn).withContext('on after click').toBe(true);
+    comp.clicked();
+    expect(comp.isOn).withContext('off after second click').toBe(false);
+  });
+});

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

@@ -0,0 +1,81 @@
+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> = [];
+  segment:string = "me";
+  ngOnInit() {
+    this.loadContact();
+  }
+
+  usernameInput:string = ""
+  async checkIfExists(toUser:Parse.Object){
+    let query = new Parse.Query("Contact");
+    query.equalTo("from",Parse.User.current()?.toPointer())
+    query.equalTo("to",toUser.toPointer())
+    return await query.first();
+  }
+  async addContact(){
+    if(this.usernameInput){
+      let query = new Parse.Query("_User");
+      query.equalTo("username",this.usernameInput);
+      let user = await query.first()
+      if(user?.id){
+        let exists = await this.checkIfExists(user)
+        if(exists?.id) return
+        let Contact = Parse.Object.extend("Contact");
+        let contact = new Contact()
+        contact.set("from",Parse.User.current()?.toPointer())
+        contact.set("to",user.toPointer())
+        await contact.save();
+        this.loadContact();
+      }else{
+        // 提示找不到用户
+      }
+    }
+  }
+  charGroupIndex:any = {}
+  loadContact() {
+    const Contact = Parse.Object.extend('Contact');
+    const query = new Parse.Query(Contact);
+    query.include("to")
+    query.ascending('firstChar');
+    if(this.segment=="me"){
+      if(!Parse.User.current()?.id) return
+      query.equalTo("from",Parse.User.current()?.id)
+    }
+
+    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();
+  }
+
+  isOn:boolean = false;
+  clicked(){
+    this.isOn = !this.isOn;
+  }
+  get clickMsg(){
+      return `The light is ${this.isOn ? 'On' : 'Off'}`;
+  }
+
+}

+ 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 { }

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

@@ -0,0 +1,14 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { SessionPage } from './session.page';
+// import { Session1Page } from './session copy/session1.page';
+const routes: Routes = [
+  { path: '', component: SessionPage },
+  // { path: 'chat/:username', component: Session1Page },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class SessionPageRoutingModule {}

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

@@ -0,0 +1,20 @@
+import { IonicModule } from '@ionic/angular';
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { SessionPage } from './session.page';
+
+import { SessionPageRoutingModule } from './session-routing.module';
+import { UserNamePipe } from '../user-name.pipe';
+
+@NgModule({
+  imports: [
+    IonicModule,
+    CommonModule,
+    FormsModule,
+    SessionPageRoutingModule,
+    UserNamePipe,
+  ],
+  declarations: [SessionPage]
+})
+export class SessionPageModule {}

+ 37 - 0
src/modules/contact/session/session.page.html

@@ -0,0 +1,37 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title style="text-align: center;">联系人</ion-title>
+  </ion-toolbar>
+  <ion-toolbar>
+    <ion-searchbar class="custom-searchbar" placeholder="搜索联系人"></ion-searchbar>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-list lines="full">
+    <ion-item *ngFor="let session of sessionList" (click)="goSession(session)">
+      <ion-avatar slot="start">
+        <img [src]="'/assets/img/clock.png'">
+      </ion-avatar>
+      <ion-label>
+        <h2>{{targetUser(session) | userName}}</h2>
+        <p>{{session?.get('message')?.get("contentJson")?.content}}</p>
+        <p>{{session?.createdAt | date:"MM-dd HH:mm"}}</p>
+      </ion-label>
+    </ion-item>
+  </ion-list>
+
+
+  <ion-list lines="full" *ngIf="!sessionList?.length">
+    <ion-item *ngFor="let item of skeletonList">
+      <ion-avatar slot="start">
+        <img [src]="item.avatar">
+      </ion-avatar>
+      <ion-label>
+        <h2>{{ item.username }}</h2>
+        <p>{{ item.lastMessageTime }}</p>
+        <p>{{ item.lastMessage }}</p>
+      </ion-label>
+    </ion-item>
+  </ion-list>
+</ion-content>

+ 69 - 0
src/modules/contact/session/session.page.scss

@@ -0,0 +1,69 @@
+
+.custom-searchbar {
+  --background: #f2f2f2; /* 设置搜索栏的背景色为灰色 */
+  --border-width: 1px; /* 设置边框宽度 */
+  --border-color: #ccc; /* 设置边框颜色 */
+  --border-radius: 10px; /* 设置圆角半径 */
+  --box-shadow: none; /* 去掉阴影 */
+}
+
+.custom-searchbar .searchbar-input {
+  font-size: 14px; /* 设置字体大小 */
+}
+
+
+
+ion-avatar {
+  width: 56px;
+  height: 56px;
+}
+
+ion-item {
+  --ion-item-background: transparent;
+  --ion-item-text-transform: none;
+}
+
+ion-label h2 {
+  font-size: 18px;
+  font-weight: bold;
+}
+
+ion-label p {
+  font-size: 14px;
+  color: #888;
+}
+.header-md{
+  box-shadow: none;
+  border-bottom: 1px rgba(0,0,0,0.2) solid;
+}
+.custom-searchbar {
+  --border-radius: 20px; /* 设置圆角大小 */
+  --height: 10px; /* 设置搜索框高度 */
+  // --box-shadow: none; /* 去掉阴影 */
+  // border:#f0f0f0 8px solid;
+
+  /* 可选:调整输入框内部的样式 */
+  .searchbar-input {
+    border-radius: var(--border-radius);
+    height: var(--height);
+    box-shadow: var(--box-shadow);
+  }
+
+  /* 可选:调整输入框外部的样式 */
+  .searchbar-input-container {
+    border-radius: var(--border-radius);
+    height: var(--height);
+    box-shadow: var(--box-shadow);
+  }
+}
+ion-content {
+  background-color: #f0f0f0; /* 设置整体背景色为白灰色 */
+}
+
+/* 如果想突出显示聊天框,可以为ion-item添加自定义样式 */
+ion-item.chat-item {
+  background-color: #ffffff; /* 设置聊天框的背景色为白色 */
+  border-radius: 10px; /* 可选:设置聊天框的圆角 */
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 可选:添加轻微阴影增强立体感 */
+}
+// .toolbar-con

+ 24 - 0
src/modules/contact/session/session.page.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { SessionPage } from './session.page';
+
+describe('SessionPage', () => {
+  let component: SessionPage;
+  let fixture: ComponentFixture<SessionPage>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [SessionPage],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SessionPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 69 - 0
src/modules/contact/session/session.page.ts

@@ -0,0 +1,69 @@
+import { Component } from '@angular/core';
+import { NavController } from '@ionic/angular';
+import { Router } from '@angular/router';
+import Parse from "parse";
+@Component({
+  selector: 'app-session',
+  templateUrl: 'session.page.html',
+  styleUrls: ['session.page.scss']
+})
+export class SessionPage {
+  skeletonList: { username: string, avatar: string, lastMessageTime: string, lastMessage: string }[] = [
+    {
+      username: 'Alice',
+      avatar: '/assets/img/female.png',
+      lastMessageTime: '10:30 AM',
+      lastMessage: 'Hey, how are you?',
+    },
+    {
+      username: 'Bob',
+      avatar: '/assets/img/male.png',
+      lastMessageTime: 'Yesterday',
+      lastMessage: 'Let\'s meet tomorrow.',
+    },
+  ];
+
+  constructor(private navCtrl: NavController) {
+    this.loadSessions()
+  }
+
+  targetUser(session:Parse.Object){
+    let user = Parse.User.current();
+    let target = null
+    if(session?.get("sendUser")?.id==user?.id){
+      target = session?.get("receiveUser")
+    }
+    if(session?.get("receiveUser")?.id==user?.id){
+      target = session?.get("sendUser")
+    }
+    return target
+  }
+  goSession(session:Parse.Object){
+    let user = Parse.User.current();
+    if(session?.get("sendUser")?.id==user?.id){
+      this.navCtrl.navigateForward(`/contact/chat/${session?.get("receiveUser")?.id}`);
+    }
+    if(session?.get("receiveUser")?.id==user?.id){
+      this.navCtrl.navigateForward(`/contact/chat/${session?.get("sendUser")?.id}`);
+    }
+  }
+  sessionList:Parse.Object[] = []
+  async loadSessions(){
+    console.log("loadSessions")
+    let user = Parse.User.current();
+    let query = Parse.Query.fromJSON('MessageSession',{where: {
+      $or: [
+        {
+          sendUser: user?.id,
+        },
+        {
+          receiveUser: user?.id
+        }
+      ]
+    }})
+    query.include("sendUser","receiveUser");
+    this.sessionList = await query.find();
+  }
+
+
+}

+ 8 - 0
src/modules/contact/user-name.pipe.spec.ts

@@ -0,0 +1,8 @@
+import { UserNamePipe } from './user-name.pipe';
+
+describe('UserNamePipe', () => {
+  it('create an instance', () => {
+    const pipe = new UserNamePipe();
+    expect(pipe).toBeTruthy();
+  });
+});

+ 19 - 0
src/modules/contact/user-name.pipe.ts

@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'userName',
+  pure:true,
+  standalone: true
+})
+export class UserNamePipe implements PipeTransform {
+
+  transform(user: Parse.Object, ...args: unknown[]): unknown {
+    let nickname = user?.get("nickname");
+    let username = user?.get("username");
+    let name = user?.get("name");
+    let mobile = user?.get("mobile");
+    console.log(user?.toJSON())
+    return nickname || name || mobile  || username;
+  }
+
+}

+ 2 - 2
src/modules/user/login/login.page.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
 import { AlertController, NavController } from '@ionic/angular';
 import * as Parse from "parse"
 // 引用Router服务
@@ -7,7 +7,7 @@ import * as Parse from "parse"
   templateUrl: './login.page.html',
   styleUrls: ['./login.page.scss'],
 })
-export class LoginPage implements OnInit {
+export class LoginPage {
 
   username:string = ""
   password:string = ""