4 달 전
부모
커밋
253b35e46e
100개의 변경된 파일3119개의 추가작업 그리고 4개의 파일을 삭제
  1. 15 1
      src/app/app-routing.module.ts
  2. 2 2
      src/app/tab1/tab1.page.html
  3. 1 1
      src/app/tabs/tabs-routing.module.ts
  4. 17 0
      src/modules/aigc/agent/agent-shige/agent-shige-routing.module.ts
  5. 20 0
      src/modules/aigc/agent/agent-shige/agent-shige.module.ts
  6. 41 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.html
  7. 0 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.scss
  8. 17 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.spec.ts
  9. 91 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.ts
  10. 16 0
      src/modules/aigc/aigc-routing.module.ts
  11. 13 0
      src/modules/aigc/aigc.module.ts
  12. 17 0
      src/modules/aigc/chat/chat-routing.module.ts
  13. 22 0
      src/modules/aigc/chat/chat.data.md
  14. 20 0
      src/modules/aigc/chat/chat.module.ts
  15. 24 0
      src/modules/aigc/chat/chat.page.html
  16. 0 0
      src/modules/aigc/chat/chat.page.scss
  17. 17 0
      src/modules/aigc/chat/chat.page.spec.ts
  18. 119 0
      src/modules/aigc/chat/chat.page.ts
  19. 149 0
      src/modules/aigc/chat/class-rxjs-chat-completion.ts
  20. 103 0
      src/modules/aigc/chat/class-test-chat-completion.ts
  21. 30 0
      src/modules/babylon/README.md
  22. 12 0
      src/modules/babylon/pages/case-babylon/README.md
  23. 17 0
      src/modules/babylon/pages/case-babylon/case-babylon-routing.module.ts
  24. 20 0
      src/modules/babylon/pages/case-babylon/case-babylon.module.ts
  25. 6 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.html
  26. 43 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.scss
  27. 17 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.spec.ts
  28. 171 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.ts
  29. 79 0
      src/modules/contact/README.md
  30. 13 0
      src/modules/contact/chat/chat-routing.module.ts
  31. 18 0
      src/modules/contact/chat/chat.module.ts
  32. 66 0
      src/modules/contact/chat/chat.page.html
  33. 79 0
      src/modules/contact/chat/chat.page.scss
  34. 26 0
      src/modules/contact/chat/chat.page.spec.ts
  35. 120 0
      src/modules/contact/chat/chat.page.ts
  36. 17 0
      src/modules/contact/contact-detail/contact-detail-routing.module.ts
  37. 20 0
      src/modules/contact/contact-detail/contact-detail.module.ts
  38. 13 0
      src/modules/contact/contact-detail/contact-detail.page.html
  39. 0 0
      src/modules/contact/contact-detail/contact-detail.page.scss
  40. 17 0
      src/modules/contact/contact-detail/contact-detail.page.spec.ts
  41. 15 0
      src/modules/contact/contact-detail/contact-detail.page.ts
  42. 17 0
      src/modules/contact/contact-list/contact-list-routing.module.ts
  43. 20 0
      src/modules/contact/contact-list/contact-list.module.ts
  44. 68 0
      src/modules/contact/contact-list/contact-list.page.html
  45. 0 0
      src/modules/contact/contact-list/contact-list.page.scss
  46. 17 0
      src/modules/contact/contact-list/contact-list.page.spec.ts
  47. 73 0
      src/modules/contact/contact-list/contact-list.page.ts
  48. 12 0
      src/modules/contact/contact-routing.module.ts
  49. 14 0
      src/modules/contact/contact.module.ts
  50. 14 0
      src/modules/contact/session/session-routing.module.ts
  51. 20 0
      src/modules/contact/session/session.module.ts
  52. 37 0
      src/modules/contact/session/session.page.html
  53. 69 0
      src/modules/contact/session/session.page.scss
  54. 24 0
      src/modules/contact/session/session.page.spec.ts
  55. 69 0
      src/modules/contact/session/session.page.ts
  56. 8 0
      src/modules/contact/user-name.pipe.spec.ts
  57. 19 0
      src/modules/contact/user-name.pipe.ts
  58. 17 0
      src/modules/study/camera-page/camera-page-routing.module.ts
  59. 20 0
      src/modules/study/camera-page/camera-page.module.ts
  60. 13 0
      src/modules/study/camera-page/camera-page.page.html
  61. 0 0
      src/modules/study/camera-page/camera-page.page.scss
  62. 17 0
      src/modules/study/camera-page/camera-page.page.spec.ts
  63. 15 0
      src/modules/study/camera-page/camera-page.page.ts
  64. 32 0
      src/modules/study/case-angular/comps/edit-ratio-star/README.md
  65. 3 0
      src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.html
  66. 0 0
      src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.scss
  67. 22 0
      src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.spec.ts
  68. 61 0
      src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.ts
  69. 17 0
      src/modules/study/case-angular/pages/case-edit/case-edit-routing.module.ts
  70. 22 0
      src/modules/study/case-angular/pages/case-edit/case-edit.module.ts
  71. 59 0
      src/modules/study/case-angular/pages/case-edit/case-edit.page.html
  72. 0 0
      src/modules/study/case-angular/pages/case-edit/case-edit.page.scss
  73. 17 0
      src/modules/study/case-angular/pages/case-edit/case-edit.page.spec.ts
  74. 27 0
      src/modules/study/case-angular/pages/case-edit/case-edit.page.ts
  75. 17 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype-routing.module.ts
  76. 20 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype.module.ts
  77. 83 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.html
  78. 0 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.scss
  79. 17 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.spec.ts
  80. 53 0
      src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.ts
  81. 22 0
      src/modules/study/case-js-module/case-edit/case-edit.module.ts
  82. 163 0
      src/modules/study/case-js-module/case-parse-petmarket/README.md
  83. 17 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket-routing.module.ts
  84. 20 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.module.ts
  85. 31 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.html
  86. 0 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.scss
  87. 17 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.spec.ts
  88. 36 0
      src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.ts
  89. 28 0
      src/modules/study/case-js-module/case-parse-petmarket/pet.service.ts
  90. 49 0
      src/modules/study/case-js-module/case-parse/README.md
  91. 17 0
      src/modules/study/case-js-module/case-parse/case-parse-routing.module.ts
  92. 20 0
      src/modules/study/case-js-module/case-parse/case-parse.module.ts
  93. 17 0
      src/modules/study/case-js-module/case-parse/case-parse.page.html
  94. 0 0
      src/modules/study/case-js-module/case-parse/case-parse.page.scss
  95. 17 0
      src/modules/study/case-js-module/case-parse/case-parse.page.spec.ts
  96. 28 0
      src/modules/study/case-js-module/case-parse/case-parse.page.ts
  97. 71 0
      src/modules/study/case-js-module/case-pet-detail/README.md
  98. 17 0
      src/modules/study/case-js-module/case-pet-detail/case-pet-detail-routing.module.ts
  99. 20 0
      src/modules/study/case-js-module/case-pet-detail/case-pet-detail.module.ts
  100. 13 0
      src/modules/study/case-js-module/case-pet-detail/case-pet-detail.page.html

+ 15 - 1
src/app/app-routing.module.ts

@@ -14,7 +14,7 @@ const routes: Routes = [
   },
   {
     path:'study',
-    canActivate:[authGuard],//路由守卫
+    //canActivate:[authGuard],//路由守卫
     children:[
       {
           path: 'parse',
@@ -27,7 +27,21 @@ const routes: Routes = [
 
     ]
   },
+  {
+    path:'aigc',
+    canActivate:[authGuard],//路由守卫
+    children:[
+      {
+          path: 'chat',
+          loadChildren: () => import('../modules/aigc/chat/chat.module').then( m => m.ChatPageModule)
+      },
+      {
+          path: 'agent',
+          loadChildren: () => import('../modules/aigc/agent/agent-shige/agent-shige.module').then( m => m.AgentShigePageModule)
+      },
 
+    ]
+  },
   
 ];
 @NgModule({

+ 2 - 2
src/app/tab1/tab1.page.html

@@ -13,11 +13,11 @@
   <div class="container">
     <ion-card>
       <ion-card-content>
-        <img id="face" src="/assets/images/home.png" class="captured-image" />
+        <img id="face" src="https://web2023.fmode.cn/jxufe/0225067/assets/images/home.png" class="captured-image" />
       </ion-card-content>
     </ion-card>
     <ion-button class="capture-button" expand="block" routerLink="/study/results">
-      <ion-icon src="/assets/icon/camera.svg" class="button-icon"></ion-icon>
+      <ion-icon src="https://web2023.fmode.cn/jxufe/0225067/assets/icon/camera.svg" class="button-icon"></ion-icon>
     </ion-button>
   </div>
 </ion-content>

+ 1 - 1
src/app/tabs/tabs-routing.module.ts

@@ -14,7 +14,7 @@ const routes: Routes = [
       },
       {
         path: 'tab2',
-        canActivate:[authGuard],//路由守卫
+        //canActivate:[authGuard],//路由守卫
         loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
       },
       {

+ 17 - 0
src/modules/aigc/agent/agent-shige/agent-shige-routing.module.ts

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

+ 20 - 0
src/modules/aigc/agent/agent-shige/agent-shige.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 { AgentShigePageRoutingModule } from './agent-shige-routing.module';
+
+import { AgentShigePage } from './agent-shige.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    AgentShigePageRoutingModule
+  ],
+  declarations: [AgentShigePage]
+})
+export class AgentShigePageModule {}

+ 41 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.html

@@ -0,0 +1,41 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>
+      诗词创作
+    </ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-item>
+    <ion-input placeholder="关键词" [(ngModel)]="shigeOptions.keywords"></ion-input>
+  </ion-item>
+  <ion-item>
+    <ion-input placeholder="创作灵感" [(ngModel)]="shigeOptions.content"></ion-input>
+  </ion-item>
+  <ion-item>
+    <ion-select label="诗歌格律" placeholder="格律" [(ngModel)]="shigeOptions.type">
+      <ion-select-option value="不限制">不限制</ion-select-option>
+      <ion-select-option value="七言绝句">七言绝句</ion-select-option>
+      <ion-select-option value="五言律诗">五言律诗</ion-select-option>
+      <ion-select-option value="藏头诗">藏头诗</ion-select-option>
+      <ion-select-option value="宋词">藏头诗</ion-select-option>
+    </ion-select>
+  </ion-item>
+  <ion-item>
+    <ion-select label="诗歌主题" placeholder="主题" [(ngModel)]="shigeOptions.theme">
+      <ion-select-option value="不限制">不限制</ion-select-option>
+      <ion-select-option [value]="item" *ngFor="let item of themeList">{{item}}</ion-select-option>
+    </ion-select>
+  </ion-item>
+  <ion-button expand="block" (click)="sendMessage()">发送</ion-button>
+
+  <ion-card *ngFor="let message of messageList">
+    <ion-card-header>
+      {{message?.role}}
+    </ion-card-header>
+    <ion-card-content>
+      {{ message?.content }}
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 0 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.scss


+ 17 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.spec.ts

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

+ 91 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.ts

@@ -0,0 +1,91 @@
+import { Component, OnInit } from '@angular/core';
+// 引用FmodeChatCompletion类
+import { TestRxjsChatCompletion,TestRxjsChatMessage } from '../../chat/class-rxjs-chat-completion';
+import Parse from "parse";
+@Component({
+  selector: 'app-agent-shige',
+  templateUrl: './agent-shige.page.html',
+  styleUrls: ['./agent-shige.page.scss'],
+})
+export class AgentShigePage implements OnInit {
+  messageList:Array<TestRxjsChatMessage> = []
+  shigeOptions:any ={
+    content:"",
+    type:"不限制",
+    theme:"不限制",
+  }
+  themeList = ['羁旅思乡诗','爱情闺怨诗','咏史怀古诗','咏物言志诗','送别怀人诗','边塞征战诗','山水田园诗']
+
+  constructor() { 
+  }
+
+  ngOnInit() {
+  }
+  sendMessage(){
+    /*
+4. 风格:选择风格,例如“豪放”或“婉约”。
+5. 情感:描述诗中应表达的情感,例如“喜悦”、“忧愁”、“思念”等。
+
+    */
+    let GuhiPromoptTemplate = `
+你是一位中国的古代诗人,擅长文言文,和现代词汇转古词语经验和技巧,并且精通各类主题、格律的诗词。请根据以下要求创作一首诗:
+1. 主题:${this.shigeOptions?.theme}。
+2. 格律:${this.shigeOptions?.type}。
+3. 关键词:${this.shigeOptions?.keywords}。
+4. 创意灵感:${this.shigeOptions?.content}。
+请根据以上要求创作一首古代诗词,并且格式要严格,有必要的情况下,可以牺牲一些关键词原意。
+
+请开始创作,并按照以下格式返回
+题目:
+内容:
+简介:
+    `
+    this.messageList.push({
+      role:"user",
+      content: GuhiPromoptTemplate
+    })
+    
+    // messageList在competion内部,已经赋予了完整的message
+    // 下方暴露出来的可订阅内容,主要是用于关键字过滤,或者其他开发逻辑的续写
+    let resultStr = ""
+    let testChatCompletion = new TestRxjsChatCompletion(this.messageList);
+    testChatCompletion.createCompletionByStream({model:"fmode-3.6-16k"}).subscribe({
+        next: ({ content, cumulativeContent, done }) => {
+          resultStr = cumulativeContent
+            console.log(`Content: ${content}`);
+            console.log(`Cumulative Content: ${cumulativeContent}`);
+            if (done) {
+                console.log('Stream completed');
+            }
+        },
+        error: err => console.error(err),
+        complete: () => {
+          // 诗歌创建完成:正则表达式,匹配诗歌json内容
+          console.log("原文",resultStr)
+                    
+          let pattern = /题目:\s*(.*?)\s*内容:\s*(.*?)\s*简介:\s*(.*)/;
+          let match = resultStr.match(pattern);
+
+          if (match) {
+            let gushi:any = {}
+              gushi.title = match[1];
+              gushi.content = match[2];
+              gushi.intro = match[3];
+              gushi.source = "AI创作";
+              console.log(`题目: ${gushi.title}`);
+              console.log(`内容: ${gushi.content}`);
+              console.log(`简介: ${gushi.intro}`);
+              console.log(gushi);
+              let Shige = Parse.Object.extend("Shige");
+              let sg = new Shige();
+              sg.set(gushi);
+              sg.save();
+          } else {
+              console.log("未能匹配到任何内容");
+          }
+        }
+    });
+
+  }
+
+}

+ 16 - 0
src/modules/aigc/aigc-routing.module.ts

@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+const routes: Routes = [
+  {path: 'chat', loadChildren: () => import('./chat/chat.module').then(mod => mod.ChatPageModule)},
+  {
+    path: 'agent/shige',
+    loadChildren: () => import('./agent/agent-shige/agent-shige.module').then( m => m.AgentShigePageModule)
+  },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class AigcRoutingModule { }

+ 13 - 0
src/modules/aigc/aigc.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AigcRoutingModule } from './aigc-routing.module';
+
+
+@NgModule({
+  declarations: [],
+  imports: [
+    CommonModule,
+    AigcRoutingModule
+  ]
+})
+export class AigcModule { }

+ 17 - 0
src/modules/aigc/chat/chat-routing.module.ts

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

+ 22 - 0
src/modules/aigc/chat/chat.data.md

@@ -0,0 +1,22 @@
+# 原始接收数据
+
+``` sh
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"从"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"前"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"有"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"一个"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"小"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"村"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"庄"},"logprobs":null,"finish_reason":null}]}
+# ...............
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"充"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"实"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"和"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"幸"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"福"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"。"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}
+data: [DONE]
+```

+ 20 - 0
src/modules/aigc/chat/chat.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 { ChatPageRoutingModule } from './chat-routing.module';
+
+import { ChatPage } from './chat.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    ChatPageRoutingModule
+  ],
+  declarations: [ChatPage]
+})
+export class ChatPageModule {}

+ 24 - 0
src/modules/aigc/chat/chat.page.html

@@ -0,0 +1,24 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>
+      AIGC 消息发送
+    </ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-item>
+    <ion-input placeholder="输入消息" [(ngModel)]="userInput"></ion-input>
+  </ion-item>
+
+  <ion-button expand="block" (click)="sendMessage()">发送</ion-button>
+
+  <ion-card *ngFor="let message of messageList">
+    <ion-card-header>
+      {{message?.role}}
+    </ion-card-header>
+    <ion-card-content>
+      {{ message?.content }}
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 0 - 0
src/modules/aigc/chat/chat.page.scss


+ 17 - 0
src/modules/aigc/chat/chat.page.spec.ts

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

+ 119 - 0
src/modules/aigc/chat/chat.page.ts

@@ -0,0 +1,119 @@
+import { Component, OnInit } from '@angular/core';
+// 引用FmodeChatCompletion类
+import { TestChatCompletion, TestChatMessage } from './class-test-chat-completion';
+import { TestRxjsChatCompletion } from './class-rxjs-chat-completion';
+
+@Component({
+  selector: 'app-chat',
+  templateUrl: './chat.page.html',
+  styleUrls: ['./chat.page.scss'],
+})
+export class ChatPage implements OnInit {
+  messageList:Array<TestChatMessage> = []
+  userInput:string = ""
+
+  Postjson = {
+    "面部特征分析": {
+      "三庭比例": {
+        "前庭": "145.88",
+        "中庭": "173.07",
+        "下庭": "63.76",
+        "分析": "前庭较短,说明额头相对较窄;中庭较长,显示脸中部较长,鼻子和颧骨区域比较突出;下庭较短,表明下巴较短。",
+        "形象设计建议": "可以选择有刘海的发型,如空气刘海,增加额头的宽度。同时选择有层次感的发型,平衡脸部中庭的长度。使用高光在额头部位,增加视觉上的额头长度。同时在鼻子两侧使用阴影,缩短中庭的长度感。"
+      },
+      "五眼比例": {
+        "比例": "1035.03",
+        "分析": "五眼比例较大,说明眼睛之间的间距相对较宽。",
+        "形象设计建议": "可以选择猫眼妆,通过延长眼线和使用深色眼影在眼角部分,增加眼睛的聚拢感。眉毛可以修剪得靠近眼睛的位置,增加眼睛的集中度。"
+      },
+      "脸型": {
+        "类型": "长形脸",
+        "分析": "长形脸说明脸部长度较大,相对宽度较窄。",
+        "形象设计建议": "选择中长发或短发,卷发或大波浪可以增加脸部的宽度,减少脸部长度的视觉效果。侧分刘海也有助于缩短脸部长度。在脸颊中央使用腮红,增加脸部的饱满感,平衡脸部长度。"
+      },
+      "眉形": {
+        "类型": "浓眉",
+        "分析": "浓眉说明眉毛较为浓密,需要适当修饰使其更加整洁有型。",
+        "形象设计建议": "保持自然的浓眉,适当修饰眉毛的形状,避免过于锐利的眉形。可以使用眉刷和眉笔进行细致的修整,使其更加有型。"
+      },
+      "唇形": {
+        "类型": "薄唇",
+        "分析": "薄唇说明嘴唇较为纤薄,需要增加唇部的丰满感。",
+        "形象设计建议": "使用唇线笔勾勒出唇部轮廓,稍微超出自然唇线,使唇形显得更加丰满。选择光泽感强的唇膏或唇蜜,增加唇部的立体感。"
+      },
+      "眼睛形状": {
+        "类型": "圆形眼",
+        "分析": "圆形眼说明眼睛较为圆润,有一定的可爱感。",
+        "形象设计建议": "可以尝试猫眼妆或烟熏妆,增加眼部的深邃感。使用眼线笔或眼线液拉长眼尾,适当加粗眼线,增强眼部轮廓感。选择哑光和珠光结合的眼影,可以增加眼部的层次感。"
+      },
+      "鼻形": {
+        "类型": "鹰钩鼻",
+        "分析": "鹰钩鼻说明鼻子的弧度较大,有一定的锐利感。",
+        "形象设计建议": "通过高光和修容来调整鼻子的视觉效果。在鼻梁和鼻头部位使用高光,在鼻翼两侧使用阴影,能够柔化鼻子的线条。"
+      },
+      "颧骨": {
+        "类型": "颧骨平",
+        "分析": "颧骨平说明颧骨不突出,面部轮廓较为平坦。",
+        "形象设计建议": "在颧骨部位使用阴影,增加颧骨的立体感,使面部轮廓更加分明。"
+      },
+      "下巴": {
+        "类型": "短下巴",
+        "分析": "短下巴说明下巴较为短小,需要增加下巴的长度感。",
+        "形象设计建议": "在下巴部位使用高光,增加视觉上的下巴长度感,使面部比例更加和谐。"
+      },
+      "额头": {
+        "类型": "窄额头",
+        "分析": "窄额头说明额头较为窄小,需要增加额头的宽度感。",
+        "形象设计建议": "选择有刘海的发型,如空气刘海,增加额头的宽度感。"
+      },
+      "脸颊": {
+        "类型": "瘦削脸颊",
+        "分析": "瘦削脸颊说明脸颊较为瘦削,需要增加脸颊的饱满感。",
+        "形象设计建议": "使用腮红增加脸颊的饱满度,选择暖色调的腮红(如桃粉色、珊瑚色),从笑肌处轻扫至太阳穴,制造出健康红润的感觉。"
+      }
+    }
+  };
+
+  Postprompt={
+    "现在你是一个形象设计大师,我的脸部特征是这些:" : ",三庭五眼:"+"145.88,173.07, 63.76"+",五眼比例:"+"1035.03"+",脸型:"+"长形脸"+",眉形: "+"浓眉"+",唇形: "+"薄唇"+",眼睛形状: "+"圆形眼"+",鼻形:"+"鹰钩鼻"+",颧骨:"+"颧骨平"+",下巴: "+"短下巴"+",额头: "+"窄额头"+",脸颊: "+"瘦削脸颊"+"。请你先分析说明每个特征分类的特点,然后给出形象设计意见,并将结果转换成json的格式。例如"+JSON.stringify(this.Postjson)
+  }
+  PostpromptString = JSON.stringify(this.Postprompt);
+  completion:TestChatCompletion
+
+  
+  constructor() { 
+    // 测试类:纯fetch读取http event stream数据
+    this.completion = new TestChatCompletion(this.messageList)
+
+    // 测试类:rxjs封装的可观察对象
+  }
+
+  ngOnInit() {
+  }
+  sendMessage(){
+    this.messageList.push({
+      role:"user",
+      content:this.PostpromptString,
+    })
+    this.userInput = ""
+    
+    // this.completion.createCompletionByStream()
+
+    // messageList在competion内部,已经赋予了完整的message
+    // 下方暴露出来的可订阅内容,主要是用于关键字过滤,或者其他开发逻辑的续写
+    let testChatCompletion = new TestRxjsChatCompletion(this.messageList);
+    testChatCompletion.createCompletionByStream().subscribe({
+        next: ({ content, cumulativeContent, done }) => {
+            console.log(`Content: ${content}`);
+            console.log(`Cumulative Content: ${cumulativeContent}`);
+            if (done) {
+                console.log('Stream completed');
+            }
+        },
+        error: err => console.error(err),
+        complete: () => console.log('Observable completed')
+    });
+
+  }
+
+}

+ 149 - 0
src/modules/aigc/chat/class-rxjs-chat-completion.ts

@@ -0,0 +1,149 @@
+import { Observable, from, of } from 'rxjs';
+import { switchMap, map, catchError, finalize } from 'rxjs/operators';
+import { ResultsPage } from '../../study/results/results.page';
+import{ParseObject} from'../../study/results/results-post'
+import Parse from "parse"
+Parse.initialize("dev");
+Parse.serverURL="http://web2023.fmode.cn:9999/parse";
+
+export interface TestRxjsChatMessage {
+    role: string;
+    content: string;
+}
+
+export class TestRxjsChatCompletion {
+    messageList: Array<TestRxjsChatMessage>;
+    messageAiReply = "";
+    lastCompletionContent:string = ""; // 新增属性以保存最后的对话结果
+
+
+    async SaveMessage()
+    {
+        let savemessage = new ResultsPage();
+        let chatResult=this.lastCompletionContent;
+        console.log("2222222222222222222222222222222222")
+        console.log(this.lastCompletionContent)
+        async  function  SaveChatResult() 
+        {
+          console.log("11111111111111111111111111111111111111111111111111");
+          let YCX:any = new ParseObject("MagicMirror")
+             YCX = await YCX.get(savemessage.currentObjectId)
+             
+             YCX.set(
+              {
+                result:chatResult,    
+               }    )
+               YCX.save();
+            // console.log(YCX);
+        }
+        SaveChatResult();
+    }
+    constructor(messageList: Array<TestRxjsChatMessage>) {
+        this.messageList = messageList;
+    }
+
+    createCompletionByStream(options?:{
+        model?:string
+    }): Observable<{ content: string, cumulativeContent: string, done: boolean }> {
+        const token = localStorage.getItem("token");
+        const bodyJson = {
+            "token": `Bearer ${token}`,
+            "messages": this.messageList,
+            "model": options?.model || "fmode-3.6-16k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "top_p": 1,
+            "stream": true
+        };
+
+        return from(fetch("https://test.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+                "accept": "text/event-stream",
+                "sec-fetch-dest": "empty",
+                "sec-fetch-mode": "cors",
+                "sec-fetch-site": "same-site"
+            },
+            "referrer": "https://ai.fmode.cn/",
+            "referrerPolicy": "strict-origin-when-cross-origin",
+            "body": JSON.stringify(bodyJson),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        })).pipe(
+            switchMap(response => {
+                const reader = response.body?.getReader();
+                if (!reader) {
+                    throw new Error("Failed to get the response reader.");
+                }
+                const decoder = new TextDecoder();
+                let buffer = "";
+                let messageAiReply = "";
+                let messageIndex = this.messageList.length;
+
+                return new Observable<{ content: string, cumulativeContent: string, done: boolean }>(observer => {
+                    const read = () => {
+                        reader.read().then(({ done, value }) => {
+                            if (done) {
+                                observer.next({ content: "", cumulativeContent: messageAiReply, done: true });
+                                observer.complete();
+                                return;
+                            }
+
+                            buffer += decoder.decode(value);
+                            let messages = buffer.split("\n");
+
+                            for (let i = 0; i < messages.length - 1; i++) {
+                                let message = messages[i];
+                                let dataText = message.replace("data: ", "");
+
+                                if (dataText.startsWith("{")) {
+                                    try {
+                                        let dataJson = JSON.parse(dataText);
+                                        let content = dataJson?.choices?.[0]?.delta?.content || "";
+                                        messageAiReply += content;
+                                        this.lastCompletionContent = messageAiReply;
+                                        this.messageList[messageIndex] = {
+                                            role: "assistant",
+                                            content: messageAiReply
+                                        };
+                                        observer.next({ content, cumulativeContent: messageAiReply, done: false });
+                                    } catch (err) { }
+                                }
+
+                                if (dataText.startsWith("[")) {
+                                    this.messageList[messageIndex] = {
+                                        role: "assistant",
+                                        content: messageAiReply
+                                    };
+                                    observer.next({ content: "", cumulativeContent: messageAiReply, done: true });
+
+                                }
+
+                                buffer = buffer.slice(message.length + 1);
+                            }
+
+                            read();
+                        }).catch(err => observer.error(err));
+                    };
+
+                    read();
+                });
+            }),
+            catchError(err => {
+                console.error(err);
+                return of({ content: "", cumulativeContent: "", done: true });
+            }),
+            finalize(() => {
+                
+                console.log("Stream completed");
+                console.log(this.lastCompletionContent)
+                this.SaveMessage()
+                
+                                
+            })
+            
+        );
+    }
+    
+}

+ 103 - 0
src/modules/aigc/chat/class-test-chat-completion.ts

@@ -0,0 +1,103 @@
+/*
+  案例:纯fetch读取http event stream数据
+*/
+export interface TestChatMessage{
+    role:string
+    content:string
+}
+export class TestChatCompletion{
+messageList:Array<TestChatMessage>
+constructor(messageList:Array<TestChatMessage>){
+    this.messageList = messageList
+}
+async createCompletionByStream() {
+
+let token = localStorage.getItem("token");
+let bodyJson = {
+  "token": `Bearer ${token}`,
+  "messages": this.messageList,
+  "model": "fmode-3.6-16k",
+  "temperature": 0.5,
+  "presence_penalty": 0,
+  "frequency_penalty": 0,
+  "top_p": 1,
+  "stream":false
+};
+
+let response = await fetch("https://test.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+  "headers": {
+    "accept": "text/event-stream",
+    "sec-fetch-dest": "empty",
+    "sec-fetch-mode": "cors",
+    "sec-fetch-site": "same-site"
+  },
+  "referrer": "https://ai.fmode.cn/",
+  "referrerPolicy": "strict-origin-when-cross-origin",
+  "body": JSON.stringify(bodyJson),
+  "method": "POST",
+  "mode": "cors",
+  "credentials": "omit"
+});
+
+let messageAiReply = ""
+let messageIndex = this.messageList.length
+let reader = response.body?.getReader();
+if (!reader) {
+  throw new Error("Failed to get the response reader.");
+}
+
+let decoder = new TextDecoder();
+let buffer = "";
+
+while (true) {
+  let { done, value } = await reader.read();
+  if (done) {
+    break;
+  }
+
+  buffer += decoder.decode(value);
+
+  // Split the buffer by newlines to get individual messages
+  let messages = buffer.split("\n");
+
+  // Process each message
+  for (let i = 0; i < messages.length - 1; i++) {
+    let message = messages[i];
+
+    // Process the message as needed
+    /**
+     * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
+     * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
+     * data: [DONE]
+     */
+    let dataText = message.replace("data:\ ","")
+    console.log(dataText)
+    if(dataText.startsWith("{")){
+      try{
+        let dataJson = JSON.parse(dataText)
+        console.log(dataJson)
+        messageAiReply += dataJson?.choices?.[0]?.delta?.content || ""
+        this.messageList[messageIndex] = {
+          role:"assistant",
+          content:messageAiReply
+        }
+      }catch(err){}
+    }
+    if(dataText.startsWith("[")){
+      console.log(message)
+      console.log("完成")
+      this.messageList[messageIndex] = {
+        role:"assistant",
+        content:messageAiReply
+      }
+      messageAiReply = ""
+    }
+    // Parse the message as JSON
+    // let data = JSON.parse(message);
+
+    // Clear the processed message from the buffer
+    buffer = buffer.slice(message.length + 1);
+  }
+}
+}
+}

+ 30 - 0
src/modules/babylon/README.md

@@ -0,0 +1,30 @@
+# WebGPU Babylon教学案例库
+
+
+
+# 模型素材
+- 中国城市lowpoly
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-city-lowpoly/gltf/
+    - FILE scene.gltf
+- 中国象棋
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-chess/gltf/
+    - FILE scene.gltf
+- 古风少女
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-woman/gltf/
+    - FILE scene.gltf
+- 古风少年
+    - https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-man/README.md
+- 卡通异瞳少女
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/vtuber-selen/gltf/
+    - FILE scene.gltf
+- 中国老杯子
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-iron-cup/
+    - FILE scene.gltf
+
+## 上传格式
+- OBS对象KEY
+    - /storage/3d/model/<model_name>/gltf/scene.gltf
+- OBS命令行
+    - obsutil sync ../model/ obs://nova-cloud/storage/3d/model/
+- 本地临时存储
+    - ~/Downloads/model/<model_name>/gltf/scene.gltf

+ 12 - 0
src/modules/babylon/pages/case-babylon/README.md

@@ -0,0 +1,12 @@
+# Babylon.js使用示例
+
+- 官方网站 https://doc.babylonjs.com/
+
+# 依赖安装
+- npm安装 https://doc.babylonjs.com/setup/frameworkPackages/es6Support
+
+``` bash
+npm install @babylonjs/core -S
+npm install @babylonjs/materials -S
+npm install @babylonjs/loaders -S
+```

+ 17 - 0
src/modules/babylon/pages/case-babylon/case-babylon-routing.module.ts

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

+ 20 - 0
src/modules/babylon/pages/case-babylon/case-babylon.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 { CaseBabylonPageRoutingModule } from './case-babylon-routing.module';
+
+import { CaseBabylonPage } from './case-babylon.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseBabylonPageRoutingModule
+  ],
+  declarations: [CaseBabylonPage]
+})
+export class CaseBabylonPageModule {}

+ 6 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.html

@@ -0,0 +1,6 @@
+<div class="list">
+  <div (click)="showCharacter(item?.name)" class="character" *ngFor="let item of CharacterMeshList">
+    <h1>{{item?.name}}</h1>
+  </div>
+</div>
+<canvas id="renderCanvas" touch-action="none"></canvas>

+ 43 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.scss

@@ -0,0 +1,43 @@
+// 调试面板
+#scene-explorer-host {
+    position: absolute !important;
+}
+#sceneExplorer{
+    position:absolute!important;
+    height:100vh!important;
+}
+.babylonDebugLayer {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    width: 300px; /* 调试层的宽度 */
+    background-color: rgba(255, 255, 255, 0.8); /* 背景颜色和透明度 */
+    z-index: 20; /* 确保调试层在最顶层 */
+    overflow: auto; /* 如果内容过多,添加滚动条 */
+  }
+  
+  .babylonDebugLayer .content {
+    padding: 10px;
+  }
+
+// 展示区
+#renderCanvas {
+    width: 100vw;
+    height: 100vh;
+    display: block;
+    font-size: 0;
+    z-index: 1;
+}
+
+.list{
+    position: absolute;
+    top:20px;
+    z-index: 10;
+    width: 100vw;
+    display: flex;
+    justify-content: center;
+    .character{
+        
+    }
+}

+ 17 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.spec.ts

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

+ 171 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.ts

@@ -0,0 +1,171 @@
+import { Component, OnInit } from '@angular/core';
+import * as BABYLON from "@babylonjs/core/Legacy/legacy";
+import { Engine, Scene } from "@babylonjs/core";
+import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera";
+import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight";
+import { Vector3 } from "@babylonjs/core/Maths/math.vector";
+import { CreateGround } from "@babylonjs/core/Meshes/Builders/groundBuilder";
+import { CreateSphere } from "@babylonjs/core/Meshes/Builders/sphereBuilder";
+import { GridMaterial } from "@babylonjs/materials/grid/gridMaterial";
+
+import "@babylonjs/loaders/glTF";
+
+@Component({
+  selector: 'app-case-babylon',
+  templateUrl: './case-babylon.page.html',
+  styleUrls: ['./case-babylon.page.scss'],
+})
+export class CaseBabylonPage implements OnInit {
+  private engine: BABYLON.Engine|undefined;
+  private scene: BABYLON.Scene|undefined;
+
+  CharacterMap:any = {}
+  CharacterMeshList = [
+    {name:"古风少女",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-woman/gltf/",
+    filePath:"scene.gltf"},
+    {name:"中国象棋",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-chess/gltf/",
+    filePath:"scene.gltf"},
+    {name:"卡通异瞳少女",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/vtuber-selen/gltf/",
+    filePath:"scene.gltf"},
+    {name:"中国老杯子",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-iron-cup/",
+    filePath:"scene.gltf"},
+    {name:"中国城市lowpoly",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-city-lowpoly/gltf/",
+    filePath:"scene.gltf"},
+
+  ]
+  constructor() { }
+
+  ngOnInit() {
+    this.initBabylon()
+  }
+
+  initBabylon(){
+    // Get the canvas element from the DOM.
+    const canvas:any = document.getElementById("renderCanvas");
+    // Set the canvas size to match the device pixel ratio
+    const devicePixelRatio = window.devicePixelRatio || 1;
+    canvas.width = window.innerWidth * devicePixelRatio;
+    canvas.height = window.innerHeight * devicePixelRatio;
+    this.engine = new BABYLON.Engine(canvas, true);
+    this.scene = new BABYLON.Scene(this.engine);
+
+    // Create a basic light, aiming 0,1,0 - meaning, to the sky.
+    const light = new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 1, 0), this.scene);
+
+    // 加载粒子效果覆盖地面
+    this.loadGroundParticle()
+    
+    // 设置初始相机
+    // Add an ArcRotateCamera to the scene and attach it to the canvas
+    // let initPositon = new BABYLON.Vector3(0,12,0)
+    let initPositon = new BABYLON.Vector3(0,0,0)
+    let beta = Math.PI / 2.5 // 镜头初始 纬度 Math.PI / 3 斜下45度
+    let radius = 20 // 镜头初始半径
+    const camera = new BABYLON.ArcRotateCamera('camera1', Math.PI / 2, beta, radius, initPositon, this.scene);
+    camera.attachControl(canvas, true);
+
+    // Load the FBX model
+
+    this.CharacterMeshList.forEach(character=>{
+      BABYLON.SceneLoader.ImportMesh('', character.dirPath, character.filePath, this.scene, (meshes, particleSystems, skeletons) => {
+        console.log(meshes)
+        // characterMesh.isVisible
+        meshes.forEach(mesh=>mesh.isVisible=false)
+        this.CharacterMap[character?.name] = {
+          meshes:meshes,
+          particleSystems:particleSystems,
+          skeletons:skeletons
+        }
+        if (skeletons.length > 0) {
+          this.scene?.beginAnimation(skeletons[0], 0, 100, true);
+        }
+      });
+    })
+    setTimeout(() => {
+      this.showCharacter("机器人")
+    }, 3000);
+    
+
+    // Register a render loop to repeatedly render the scene.
+    this.engine.runRenderLoop(() => {
+      this.scene?.render();
+    });
+
+    // 开启调试层,用于抓取角度
+    this.scene.debugLayer.show();
+
+    // Watch for browser/canvas resize events
+    window.addEventListener('resize', () => {
+      this.engine?.resize();
+    });
+  }
+
+  showCharacter(name?:string){
+    if(!name) return
+
+    Object.keys(this.CharacterMap).forEach(tempname=>{
+      let character = this.CharacterMap[tempname];
+      if(tempname==name){
+        if(character.meshes?.length){
+          character.meshes.forEach((mesh:any)=>mesh.isVisible=true)
+        }
+      }else{
+        if(character.meshes?.length){
+          character.meshes.forEach((mesh:any)=>mesh.isVisible=false)
+        }
+      }
+    })
+
+   
+  }
+  loadGroundParticle(){
+      if(!this.scene) return
+      // 创建粒子系统
+      const particleSystem = new BABYLON.ParticleSystem("particles", 2000, this.scene);
+
+      // 纹理
+      particleSystem.particleTexture = new BABYLON.Texture("textures/flare.png", this.scene);
+
+      // 发射器
+      particleSystem.emitter = new BABYLON.Vector3(0, 0, 0); // 发射器位置
+      particleSystem.minEmitBox = new BABYLON.Vector3(-300, 0, -300); // 最小发射范围
+      particleSystem.maxEmitBox = new BABYLON.Vector3(300, 0, 300); // 最大发射范围
+
+      // 粒子颜色
+      particleSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
+      particleSystem.color2 = new BABYLON.Color4(0.5, 0.5, 1, 1);
+      particleSystem.colorDead = new BABYLON.Color4(0, 0, 0.2, 0.5);
+
+      // 粒子大小
+      particleSystem.minSize = 0.1;
+      particleSystem.maxSize = 0.5;
+
+      // 粒子生命周期
+      particleSystem.minLifeTime = 0.3;
+      particleSystem.maxLifeTime = 1.5;
+
+      // 发射速率
+      particleSystem.emitRate = 1000;
+
+      // 粒子方向
+      particleSystem.direction1 = new BABYLON.Vector3(-1, 1, -1);
+      particleSystem.direction2 = new BABYLON.Vector3(1, 1, 1);
+
+      // 重力
+      particleSystem.gravity = new BABYLON.Vector3(0, -9.81, 0);
+
+      // 粒子速度
+      particleSystem.minEmitPower = 0.5;
+      particleSystem.maxEmitPower = 1.5;
+      particleSystem.updateSpeed = 0.01;
+
+      // 开始粒子系统
+      particleSystem.start();
+  }
+
+}

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

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

@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
+
+import { ChatPage } from './chat.page';
+
+describe('ChatPage', () => {
+  let component: ChatPage;
+  let fixture: ComponentFixture<ChatPage>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ChatPage],
+      imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
+    }).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 {}

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

@@ -0,0 +1,68 @@
+<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-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();
+  });
+});

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

@@ -0,0 +1,73 @@
+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();
+  }
+
+}

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

+ 17 - 0
src/modules/study/camera-page/camera-page-routing.module.ts

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

+ 20 - 0
src/modules/study/camera-page/camera-page.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 { CameraPagePageRoutingModule } from './camera-page-routing.module';
+
+import { CameraPagePage } from './camera-page.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CameraPagePageRoutingModule
+  ],
+  declarations: [CameraPagePage]
+})
+export class CameraPagePageModule {}

+ 13 - 0
src/modules/study/camera-page/camera-page.page.html

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

+ 0 - 0
src/modules/study/camera-page/camera-page.page.scss


+ 17 - 0
src/modules/study/camera-page/camera-page.page.spec.ts

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

+ 15 - 0
src/modules/study/camera-page/camera-page.page.ts

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

+ 32 - 0
src/modules/study/case-angular/comps/edit-ratio-star/README.md

@@ -0,0 +1,32 @@
+# 组件:星星打分组件
+
+# 组件结构描述
+- 基本功能:用户可以引入该组件,绑定在number类型的属性上,实现用户选择星星,就能返回对应星级的数字.
+- 页面模板
+    - 横向的多颗行星列表
+        - 根据starCheckList进行循环,显示满星和空星
+    - 星星用ion-icon字体图标表示
+- 常量属性
+    - starCheckList:Array<boolean> 星星选择的数组
+        - [true,true,false,false,falce] 表示两颗满星 三颗空星
+- 输入属性
+    - value:number 默认 0
+    - maxValue:number default 5 默认5颗星
+- 输出事件
+    - onValueChange 事件 返回number类型 组件内选择的星星分数
+- 逻辑函数
+    - 初始化函数
+        - 根据value和maxValue的值,分别计算空星星数量和满星星数量,生成starCheckList
+    - 星星选择函数
+        - 根据星星所在位置,给value分数赋值,并且重新生成星星数组.
+        - 将value的值通过onValueChange事件进行发送
+
+# 组件开发:人工实现
+``` bash
+ionic g component edit-ratio-star --standalone
+```
+
+# 思考题目
+- 问题1:如何实现当满星被再次点击时候,分值设置为0?
+- 问题2:如何实现不用星星做标识,换成笑脸?
+- 问题3:如何实现图标颜色的用户自定义?

+ 3 - 0
src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.html

@@ -0,0 +1,3 @@
+<div class="list">
+  <ion-icon [style.color]="color" (click)="onStarClick(index)" [name]="checked?fillIcon:emptyIcon" *ngFor="let checked of starCheckList;let index=index;"></ion-icon>
+</div>

+ 0 - 0
src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.scss


+ 22 - 0
src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { EditRatioStarComponent } from './edit-ratio-star.component';
+
+describe('EditRatioStarComponent', () => {
+  let component: EditRatioStarComponent;
+  let fixture: ComponentFixture<EditRatioStarComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [EditRatioStarComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(EditRatioStarComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 61 - 0
src/modules/study/case-angular/comps/edit-ratio-star/edit-ratio-star.component.ts

@@ -0,0 +1,61 @@
+import { NgForOf } from '@angular/common';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { IonIcon } from '@ionic/angular/standalone';
+
+@Component({
+  selector: 'app-edit-ratio-star',
+  templateUrl: './edit-ratio-star.component.html',
+  styleUrls: ['./edit-ratio-star.component.scss'],
+  standalone: true,
+  imports:[
+    IonIcon,NgForOf
+  ]
+})
+export class EditRatioStarComponent  implements OnInit {
+
+  @Input()
+  fillIcon:string = "star"
+  @Input()
+  emptyIcon:string = "star-outline"
+  @Input()
+  color:string = "#FFFF00"
+
+  @Input()
+  value:number = 2;
+  @Output()
+  onValueChange:EventEmitter<number> = new EventEmitter<number>
+
+  maxValue:number = 10;
+  starCheckList:Array<boolean> = []
+  constructor() {
+   }
+
+  ngOnInit() {
+    this.makeStarList()
+  }
+
+  onStarClick(index:number){
+    let score = index + 1
+
+    if(score==this.value){ // 点原星星,设置0分
+      this.value = 0
+    }else{ // 根据星星位置,设置分数
+      this.value = score;
+    }
+
+    this.makeStarList();
+    this.onValueChange.emit(this.value)
+  }
+  makeStarList(){
+    let starList = []
+    for (let index = 0; index < this.value; index++) {
+      starList.push(true)
+    }
+    let leftCount = this.maxValue - this.value
+    for (let index = 0; index < leftCount; index++) {
+      starList.push(false)
+    }
+    this.starCheckList = starList
+  }
+
+}

+ 17 - 0
src/modules/study/case-angular/pages/case-edit/case-edit-routing.module.ts

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

+ 22 - 0
src/modules/study/case-angular/pages/case-edit/case-edit.module.ts

@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { CaseEditPageRoutingModule } from './case-edit-routing.module';
+
+import { CaseEditPage } from './case-edit.page';
+import { EditRatioStarComponent } from '../../comps/edit-ratio-star/edit-ratio-star.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseEditPageRoutingModule,
+    EditRatioStarComponent
+  ],
+  declarations: [CaseEditPage]
+})
+export class CaseEditPageModule {}

+ 59 - 0
src/modules/study/case-angular/pages/case-edit/case-edit.page.html

@@ -0,0 +1,59 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>case-edit</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">case-edit</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <ion-card>
+    <ion-item>
+      星星打分:<app-edit-ratio-star [value]="score" (onValueChange)="score = $event"></app-edit-ratio-star>
+    </ion-item>
+    <ion-item>
+      图标打分:<app-edit-ratio-star color="blue" fillIcon="home" emptyIcon="home-outline" [value]="score" (onValueChange)="score = $event"></app-edit-ratio-star>
+    </ion-item>
+    <ion-item>
+      <ion-input type="number" [(ngModel)]="score"></ion-input>
+    </ion-item>
+  </ion-card>
+
+  <ion-card>
+    <ion-item>
+      已输入用户名:{{username}}
+    </ion-item>
+    <ion-item>
+      <ion-label>Input事件</ion-label>
+      <ion-input type="text" (input)="onUserNameInput($event)"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-label>ngModel双向绑定</ion-label>
+      <ion-input type="text" [value]="username" (input)="onUserNameInput($event)"></ion-input>
+      <ion-input type="text" [(ngModel)]="username"></ion-input>
+    </ion-item>
+  </ion-card>
+
+  <ion-card>
+    <ion-item>
+      用户资料:各种输入类型
+    </ion-item>
+    <ion-item>
+      <ion-label>姓名</ion-label>
+      <ion-input type="text" [(ngModel)]="userInfo.name"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-label>年龄</ion-label>
+      <ion-input type="number" [(ngModel)]="userInfo.age"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-label>生日</ion-label>
+      <ion-datetime [(ngModel)]="userInfo.birthday"></ion-datetime>
+    </ion-item>
+  </ion-card>
+
+</ion-content>

+ 0 - 0
src/modules/study/case-angular/pages/case-edit/case-edit.page.scss


+ 17 - 0
src/modules/study/case-angular/pages/case-edit/case-edit.page.spec.ts

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

+ 27 - 0
src/modules/study/case-angular/pages/case-edit/case-edit.page.ts

@@ -0,0 +1,27 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-case-edit',
+  templateUrl: './case-edit.page.html',
+  styleUrls: ['./case-edit.page.scss'],
+})
+export class CaseEditPage implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+  username:string = ""
+  userInfo:any = {
+    name:"姓名",
+    age:18,
+    birthday:new Date()
+  }
+  score:number = 4;
+  onUserNameInput(ev:Event|any){
+    console.log(ev)
+    this.username = ev?.target?.value;
+  }
+
+}

+ 17 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype-routing.module.ts

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

+ 20 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype.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 { TsdatatypePageRoutingModule } from './tsdatatype-routing.module';
+
+import { TsdatatypePage } from './tsdatatype.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    TsdatatypePageRoutingModule
+  ],
+  declarations: [TsdatatypePage]
+})
+export class TsdatatypePageModule {}

+ 83 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.html

@@ -0,0 +1,83 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>{{title}} - 点赞数{{likeCount}}</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">tsdatatype</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <ion-item>
+    常见的数据类型:
+    {{dataTypeList}}
+  </ion-item>
+
+  <ion-item>
+    日期类型:
+    {{now}}
+    日期管道(DatePipe的使用)
+    {{now | date:"yyyy-MM-dd"}}
+  </ion-item>
+
+  <ion-item>
+    Object类型:
+    {{student}}
+    名称:{{student.name}}
+    年龄:{{student.age}}
+  </ion-item>
+
+  <ion-item>
+    Boolean类型的模板(*ngIf)
+    {{isCollection}}
+    <ion-icon *ngIf="isCollection" name="star"></ion-icon>
+    <ion-icon *ngIf="!isCollection" name="star-outline"></ion-icon>
+  </ion-item>
+
+  <ion-list>
+    接口描述的Object作为数组元素的显示
+    <ion-item>
+      直接显示:{{studentList}}
+    </ion-item>
+    <ion-item>
+      下标取元素显示:
+      {{studentList[0].name}}
+      {{studentList[1].idcard}}
+      未定义变量的预取值(需要用?.表明可能不存在的父级Object)
+      {{studentList[2]?.name}}
+    </ion-item>
+    <ion-item>
+      数组Object的循环显示(*ngFor)
+      <ion-card *ngFor="let student of studentList">
+        <ion-card-title>
+          {{student?.name}}
+        </ion-card-title>
+        <ion-card-subtitle>
+          {{student?.age}}
+        </ion-card-subtitle>
+      </ion-card>
+    </ion-item>
+  </ion-list>
+
+  <ion-list>
+    管道:https://angular.cn/guide/pipes
+    <ion-item>
+      日期格式化
+      DatePipe {{now | date:"HH:mm:SS"}}
+    </ion-item>
+    <ion-item>
+      大写转换
+      UpperCasePipe {{studentList[0]?.name | uppercase}}
+    </ion-item>
+    <ion-item>
+      JSON调试
+      JsonPipe {{studentList[0] | json}}
+    </ion-item>
+    <ion-item>
+      PercentPipe {{0.125 | percent}}
+    </ion-item>
+  </ion-list>
+</ion-content>

+ 0 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.scss


+ 17 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.spec.ts

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

+ 53 - 0
src/modules/study/case-angular/pages/tsdatatype/tsdatatype.page.ts

@@ -0,0 +1,53 @@
+import { Component, OnInit } from '@angular/core';
+
+
+interface Student{
+  name:string
+  age:number
+  idcard?:string
+}
+
+@Component({
+  selector: 'app-tsdatatype',
+  templateUrl: './tsdatatype.page.html',
+  styleUrls: ['./tsdatatype.page.scss'],
+})
+export class TsdatatypePage implements OnInit {
+
+  title:string = "TS数据类型与NG模板指令"
+  likeCount:number = 99
+  isCollection:boolean = false
+  now:Date = new Date();
+  dataTypeList:Array<string> = ["string","number","boolean"]
+  dataTypeList2:string[] = []
+
+  // 接口:Student
+  student:Student = {
+    name:"XiaoMing",
+    age:18,
+    idcard:"322xxxxxxxxxxxxxx"
+  }
+  studentList:Array<Student> = []
+  
+
+  constructor() { 
+    // 接口数组:Array<Student>
+    let studentList:Array<Student> = []
+    // 必填属性
+    let xiaoming = {name:"XiaoMing",age:18,idcard:"322*0000",lev:3}
+    // 可选属性
+    let xiaoli = {name:"XiaoLi",age:17}
+    studentList.push(xiaoming)
+    studentList.push(xiaoli)
+    this.studentList = studentList
+    // 未定义变量的预先取值
+    setTimeout(() => {
+      this.studentList.push({name:"小王",age:19})
+    }, 2000);
+
+  }
+
+  ngOnInit() {
+  }
+
+}

+ 22 - 0
src/modules/study/case-js-module/case-edit/case-edit.module.ts

@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { CaseEditPageRoutingModule } from './case-edit-routing.module';
+
+import { CaseEditPage } from './case-edit.page';
+import { EditRatioStarComponent } from '../../case-angular/comps/edit-ratio-star/edit-ratio-star.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseEditPageRoutingModule,
+    EditRatioStarComponent
+  ],
+  declarations: [CaseEditPage]
+})
+export class CaseEditPageModule {}

+ 163 - 0
src/modules/study/case-js-module/case-parse-petmarket/README.md

@@ -0,0 +1,163 @@
+
+# 示例:AI编写列表页
+> 我在编写一个宠物市场的页面,前端框架使用ionic7的angular版本,数据请求使用Parse JS SDK。
+> 其中宠物的Schema名称为Pet,字段name:String名称、type:String类型,price:Number价格。
+> 请帮我实现该页面,通过卡片列表展示宠物,需要支持搜索条、宠物类型(Pet的type字段)标签切换、价格(Pet的price字段)点击排序等功能。
+
+好的,我可以帮你实现这个宠物市场页面。我们将使用Ionic 7和Angular来构建前端,并通过Parse JS SDK来处理数据请求。以下是实现这个页面的详细步骤:
+
+1. **初始化项目**
+   首先,确保你已经安装了Ionic CLI。如果没有,使用以下命令安装:
+   ```bash
+   npm install -g @ionic/cli
+   ```
+
+   然后,创建一个新的Ionic Angular项目:
+   ```bash
+   ionic start pet-market blank --type=angular
+   cd pet-market
+   ```
+
+2. **安装Parse JS SDK**
+   在项目目录下安装Parse JS SDK:
+   ```bash
+   npm install parse
+   ```
+
+3. **配置Parse**
+   在`src/environments/environment.ts`中添加Parse配置:
+   ```typescript
+   export const environment = {
+     production: false,
+     parseAppId: 'YOUR_PARSE_APP_ID',
+     parseServerUrl: 'YOUR_PARSE_SERVER_URL'
+   };
+   ```
+
+   在`src/main.ts`中初始化Parse:
+   ```typescript
+   import { environment } from './environments/environment';
+   import { Parse } from 'parse';
+
+   Parse.initialize(environment.parseAppId);
+   Parse.serverURL = environment.parseServerUrl;
+   ```
+
+4. **创建服务来处理Parse请求**
+   创建一个新的服务来处理与Parse的交互。在`src/app`目录下创建一个`services`文件夹,并在其中创建`pet.service.ts`:
+   ```typescript
+   import { Injectable } from '@angular/core';
+   import { Parse } from 'parse';
+
+   @Injectable({
+     providedIn: 'root'
+   })
+   export class PetService {
+     private Pet = Parse.Object.extend('Pet');
+
+     constructor() {}
+
+     async getPets(queryParams: any): Promise<Parse.Object[]> {
+       const query = new Parse.Query(this.Pet);
+       if (queryParams.name) {
+         query.contains('name', queryParams.name);
+       }
+       if (queryParams.type) {
+         query.equalTo('type', queryParams.type);
+       }
+       if (queryParams.sortByPrice) {
+         query.ascending('price');
+       } else {
+         query.descending('price');
+       }
+       return await query.find();
+     }
+   }
+   ```
+
+5. **创建页面组件**
+   生成页面组件:
+   ```bash
+   ionic generate page PetMarket
+   ```
+
+6. **实现页面布局和逻辑**
+   在`pet-market.page.html`中,创建搜索条、类型标签和卡片列表:
+   ```html
+   <ion-header>
+     <ion-toolbar>
+       <ion-title>Pet Market</ion-title>
+     </ion-toolbar>
+   </ion-header>
+
+   <ion-content>
+     <ion-searchbar [(ngModel)]="searchQuery" (ionInput)="fetchPets()"></ion-searchbar>
+     <ion-segment [(ngModel)]="selectedType" (ionChange)="fetchPets()">
+       <ion-segment-button *ngFor="let type of petTypes" [value]="type">
+         {{ type }}
+       </ion-segment-button>
+     </ion-segment>
+     <ion-button (click)="toggleSort()">Sort by Price</ion-button>
+     <ion-list>
+       <ion-card *ngFor="let pet of pets">
+         <ion-card-header>
+           <ion-card-title>{{ pet.get('name') }}</ion-card-title>
+         </ion-card-header>
+         <ion-card-content>
+           Type: {{ pet.get('type') }}<br>
+           Price: {{ pet.get('price') | currency }}
+         </ion-card-content>
+       </ion-card>
+     </ion-list>
+   </ion-content>
+   ```
+
+   在`pet-market.page.ts`中,添加逻辑来处理数据请求和状态管理:
+   ```typescript
+   import { Component, OnInit } from '@angular/core';
+   import { PetService } from '../services/pet.service';
+
+   @Component({
+     selector: 'app-pet-market',
+     templateUrl: './pet-market.page.html',
+     styleUrls: ['./pet-market.page.scss'],
+   })
+   export class PetMarketPage implements OnInit {
+     pets: any[] = [];
+     searchQuery: string = '';
+     selectedType: string = '';
+     petTypes: string[] = ['Dog', 'Cat', 'Bird', 'Other'];
+     sortByPrice: boolean = false;
+
+     constructor(private petService: PetService) {}
+
+     ngOnInit() {
+       this.fetchPets();
+     }
+
+     async fetchPets() {
+       const queryParams = {
+         name: this.searchQuery,
+         type: this.selectedType,
+         sortByPrice: this.sortByPrice
+       };
+       this.pets = await this.petService.getPets(queryParams);
+     }
+
+     toggleSort() {
+       this.sortByPrice = !this.sortByPrice;
+       this.fetchPets();
+     }
+   }
+   ```
+
+7. **添加样式**
+   根据需要添加样式以美化页面。在`pet-market.page.scss`中编写CSS样式。
+
+8. **运行项目**
+   最后,运行项目并查看效果:
+   ```bash
+   ionic serve
+   ```
+
+这样,你就可以在Ionic Angular项目中创建一个宠物市场页面,并通过Parse JS SDK来获取和展示宠物数据。页面支持搜索、类型标签切换和价格排序功能。

+ 17 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket-routing.module.ts

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

+ 20 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.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 { CaseParsePetmarketPageRoutingModule } from './case-parse-petmarket-routing.module';
+
+import { CaseParsePetmarketPage } from './case-parse-petmarket.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseParsePetmarketPageRoutingModule
+  ],
+  declarations: [CaseParsePetmarketPage]
+})
+export class CaseParsePetmarketPageModule {}

+ 31 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.html

@@ -0,0 +1,31 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>Pet Market</ion-title>
+    <ion-buttons slot="end">
+      <ion-button routerLink="/study/pet/edit/new" >发布</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-searchbar [(ngModel)]="searchQuery" (ionInput)="fetchPets()"></ion-searchbar>
+  <ion-segment [(ngModel)]="selectedType" (ionChange)="fetchPets()">
+    <ion-segment-button *ngFor="let type of petTypes" [value]="type">
+      {{ type }}
+    </ion-segment-button>
+  </ion-segment>
+  <ion-button (click)="toggleSort()">Sort by Price</ion-button>
+  <ion-list>
+    <ion-card *ngFor="let pet of pets">
+      <ion-card-header>
+        <ion-card-title>{{ pet.get('name') }}</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        Type: {{ pet.get('type') }}<br>
+        Price: {{ pet.get('price') | currency }}
+      </ion-card-content>
+      <ion-button fill="clear" routerLink="/study/pet/{{pet.id}}">详情</ion-button>
+      <ion-button *ngIf="pet?.get('user')?.id == user?.id" fill="clear" routerLink="/study/pet/edit/{{pet.id}}">编辑</ion-button>
+    </ion-card>
+  </ion-list>
+</ion-content>

+ 0 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.scss


+ 17 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.spec.ts

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

+ 36 - 0
src/modules/study/case-js-module/case-parse-petmarket/case-parse-petmarket.page.ts

@@ -0,0 +1,36 @@
+import { Component, OnInit } from '@angular/core';
+import { PetService } from './pet.service';
+import Parse from "parse";
+@Component({
+  selector: 'app-case-parse-petmarket',
+  templateUrl: './case-parse-petmarket.page.html',
+  styleUrls: ['./case-parse-petmarket.page.scss'],
+})
+export class CaseParsePetmarketPage implements OnInit {
+  pets: any[] = [];
+  searchQuery: string = '';
+  selectedType: string = '';
+  petTypes: string[] = ['狗', '猫', '蟑螂', '羊驼'];
+  sortByPrice: boolean = false;
+
+  constructor(private petService: PetService) {}
+  user:Parse.User|undefined
+  ngOnInit() {
+    this.user = Parse.User.current();
+    this.fetchPets();
+  }
+
+  async fetchPets() {
+    const queryParams = {
+      name: this.searchQuery,
+      type: this.selectedType,
+      sortByPrice: this.sortByPrice
+    };
+    this.pets = await this.petService.getPets(queryParams);
+  }
+
+  toggleSort() {
+    this.sortByPrice = !this.sortByPrice;
+    this.fetchPets();
+  }
+}

+ 28 - 0
src/modules/study/case-js-module/case-parse-petmarket/pet.service.ts

@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import Parse from 'parse';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class PetService {
+  private Pet = Parse.Object.extend('Pet');
+  constructor() {
+ 
+  }
+
+  async getPets(queryParams: any): Promise<Parse.Object[]> {
+    const query = new Parse.Query(this.Pet);
+    if (queryParams.name) {
+      query.contains('name', queryParams.name);
+    }
+    if (queryParams.type) {
+      query.equalTo('type', queryParams.type);
+    }
+    if (queryParams.sortByPrice) {
+      query.ascending('price');
+    } else {
+      query.descending('price');
+    }
+    return await query.find();
+  }
+}

+ 49 - 0
src/modules/study/case-js-module/case-parse/README.md

@@ -0,0 +1,49 @@
+# Parse JS SDK 使用示例
+
+- 使用文档:https://docs.parseplatform.org/js/guide/
+- API手册:https://parseplatform.org/Parse-SDK-JS/api/5.2.0/
+
+# 安装依赖
+``` bash
+npm i -S parse
+npm i -D @types/parse
+```
+配置 tsconfig.json
+``` json
+  "compilerOptions": {
+    "allowSyntheticDefaultImports": true,
+  }
+```
+
+# 设置微服务地址+参数
+
+``` ts
+// 引用Parse JS SDK
+import Parse from "parse";
+Parse.initialize("dev"); // 设置applicationId
+Parse.serverURL = "http://web2023.fmode.cn:9999/parse"; // 设置serverURL
+
+```
+
+# 示例:最简单的列表加载
+
+- pet.page.html
+``` html
+<ion-card *ngFor="let pet of petList">
+    {{pet.get("name")}}
+</ion-card>
+```
+
+- pet.page.ts
+``` ts
+ngOnInit() {
+    this.loadPets()
+  }
+  
+  petList:Array<Parse.Object> = []
+  async loadPets(){
+    let query = new Parse.Query("Pet");
+    let list = await query.find();
+    this.petList = list
+  }
+```

+ 17 - 0
src/modules/study/case-js-module/case-parse/case-parse-routing.module.ts

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

+ 20 - 0
src/modules/study/case-js-module/case-parse/case-parse.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 { CaseParsePageRoutingModule } from './case-parse-routing.module';
+
+import { CaseParsePage } from './case-parse.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseParsePageRoutingModule
+  ],
+  declarations: [CaseParsePage]
+})
+export class CaseParsePageModule {}

+ 17 - 0
src/modules/study/case-js-module/case-parse/case-parse.page.html

@@ -0,0 +1,17 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>Parse JS SDK功能演示</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">Parse JS SDK功能演示</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <ion-card *ngFor="let pet of petList" routerLink="/study/pet/{{pet.id}}">
+    {{pet.get("name")}}
+  </ion-card>
+</ion-content>

+ 0 - 0
src/modules/study/case-js-module/case-parse/case-parse.page.scss


+ 17 - 0
src/modules/study/case-js-module/case-parse/case-parse.page.spec.ts

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

+ 28 - 0
src/modules/study/case-js-module/case-parse/case-parse.page.ts

@@ -0,0 +1,28 @@
+import { Component, OnInit } from '@angular/core';
+
+// 引用Parse JS SDK
+import Parse from "parse";
+Parse.initialize("dev"); // 设置applicationId
+Parse.serverURL = "http://web2023.fmode.cn:9999/parse"; // 设置serverURL
+
+@Component({
+  selector: 'app-case-parse',
+  templateUrl: './case-parse.page.html',
+  styleUrls: ['./case-parse.page.scss'],
+})
+export class CaseParsePage implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+    this.loadPets()
+  }
+  
+  petList:Array<Parse.Object> = []
+  async loadPets(){
+    let query = new Parse.Query("Pet");
+    let list = await query.find();
+    this.petList = list
+  }
+
+}

+ 71 - 0
src/modules/study/case-js-module/case-pet-detail/README.md

@@ -0,0 +1,71 @@
+
+# 详情页制作 + 路由跳转 + 路由传参 + 根据ID加载
+
+# 页面创建和路由配置
+
+``` bash
+ionic g page case-pet-detail
+```
+
+- 修改app-routing.module.ts
+``` ts
+  {
+        path: 'pet/:id',
+        loadChildren: () => import('../modules/study/case-js-module/case-pet-detail/case-pet-detail.module').then( m => m.CasePetDetailPageModule)
+      },
+```
+
+# 路由参数id的获取
+
+- case-pet-detail.page.ts
+``` ts
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import Parse from "parse";
+
+@Component({
+  selector: 'app-case-pet-detail',
+  templateUrl: './case-pet-detail.page.html',
+  styleUrls: ['./case-pet-detail.page.scss'],
+})
+export class CasePetDetailPage implements OnInit {
+
+  constructor(private route:ActivatedRoute) { }
+
+  ngOnInit() {
+    this.loadPetById()
+  }
+
+  pet:Parse.Object|undefined
+  async loadPetById(){
+    // let id = location.pathname.split("/").pop();
+    let id = this.route.snapshot.params["id"]
+
+    if(id){
+      let query = new Parse.Query("Pet");
+      this.pet = await query.get(id);
+    }
+  }
+
+}
+```
+
+# 根据ID获取数据的渲染
+> 注意:Parse.Object类型的数据,需要用.get获取内部属性,?.get避免空对象的情况
+- case-pet-detail.page.html
+``` html
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>{{pet?.get("name")}}</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+
+  <ion-card>
+    {{pet?.get("type")}}
+    {{pet?.get("price")}}
+  </ion-card>
+</ion-content>
+
+```

+ 17 - 0
src/modules/study/case-js-module/case-pet-detail/case-pet-detail-routing.module.ts

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

+ 20 - 0
src/modules/study/case-js-module/case-pet-detail/case-pet-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 { CasePetDetailPageRoutingModule } from './case-pet-detail-routing.module';
+
+import { CasePetDetailPage } from './case-pet-detail.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CasePetDetailPageRoutingModule
+  ],
+  declarations: [CasePetDetailPage]
+})
+export class CasePetDetailPageModule {}

+ 13 - 0
src/modules/study/case-js-module/case-pet-detail/case-pet-detail.page.html

@@ -0,0 +1,13 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>{{pet?.get("name")}}</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+
+  <ion-card>
+    {{pet?.get("type")}}
+    {{pet?.get("price")}}
+  </ion-card>
+</ion-content>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.