فهرست منبع

Merge branch 'master' of http://git.fmode.cn:3000/fmode/agent-app

ryanemax 4 روز پیش
والد
کامیت
1e7b4891fb
52فایلهای تغییر یافته به همراه3596 افزوده شده و 349 حذف شده
  1. 2 0
      .gitignore
  2. 58 0
      docs-prod/shop-schema.md
  3. 7 0
      example-book/6.0.cloud.object/README.md
  4. 75 0
      example-book/6.0.cloud.object/cloud-object.js
  5. 105 0
      example-book/6.0.cloud.object/cloud-query.js
  6. 21 0
      example-book/6.0.cloud.object/tutor-cloud.js
  7. 56 0
      example-book/6.0.cloud.object/tutor-fetch.js
  8. 29 0
      example-book/6.0.cloud.object/tutor-query.js
  9. 19 19
      src/agent/tasks/inquiry/2.inquiry-doctor-question.ts
  10. 44 44
      src/app/app.routes.ts
  11. 73 3
      src/app/tab2/tab2.page.html
  12. 105 51
      src/app/tab2/tab2.page.scss
  13. 301 103
      src/app/tab2/tab2.page.ts
  14. 6 3
      src/app/tabs/tabs.page.html
  15. 28 4
      src/app/tabs/tabs.routes.ts
  16. 11 0
      src/app/tabs/test.guard.ts
  17. BIN
      src/assets/avatars/jiaolian1.jpg
  18. BIN
      src/assets/icon/jz.jpg
  19. BIN
      src/assets/icon/yy.jpg
  20. BIN
      src/assets/icon/zj.jpg
  21. 62 51
      src/lib/ncloud.ts
  22. 4 0
      src/lib/user/README.md
  23. 60 23
      src/lib/user/modal-user-edit/modal-user-edit.component.html
  24. 27 0
      src/lib/user/modal-user-edit/modal-user-edit.component.scss
  25. 45 45
      src/lib/user/modal-user-edit/modal-user-edit.component.ts
  26. 62 0
      src/lib/user/page-mine/page-mine.component.html
  27. 36 0
      src/lib/user/page-mine/page-mine.component.scss
  28. 22 0
      src/lib/user/page-mine/page-mine.component.spec.ts
  29. 61 0
      src/lib/user/page-mine/page-mine.component.ts
  30. 3 3
      src/lib/user/token-guard/token.guard.ts
  31. 180 0
      src/modules/demo/bird/README.md
  32. 181 0
      src/modules/demo/bird/import-bird-data.ts
  33. 145 0
      src/modules/demo/bird/page-bird-display/page-bird-display.component.html
  34. 89 0
      src/modules/demo/bird/page-bird-display/page-bird-display.component.scss
  35. 22 0
      src/modules/demo/bird/page-bird-display/page-bird-display.component.spec.ts
  36. 36 0
      src/modules/demo/bird/page-bird-display/page-bird-display.component.ts
  37. 70 0
      src/modules/demo/bird/page-bird-list/page-bird-list.component.html
  38. 42 0
      src/modules/demo/bird/page-bird-list/page-bird-list.component.scss
  39. 22 0
      src/modules/demo/bird/page-bird-list/page-bird-list.component.spec.ts
  40. 68 0
      src/modules/demo/bird/page-bird-list/page-bird-list.component.ts
  41. 61 0
      src/modules/demo/page-training-plan/page-training-plan.component.html
  42. 23 0
      src/modules/demo/page-training-plan/page-training-plan.component.scss
  43. 22 0
      src/modules/demo/page-training-plan/page-training-plan.component.spec.ts
  44. 95 0
      src/modules/demo/page-training-plan/page-training-plan.component.ts
  45. 852 0
      user-server/package-lock.json
  46. 17 0
      user-server/package.json
  47. 227 0
      user-server/server.js
  48. 34 0
      user-server/test/test-auth.js
  49. 31 0
      user-server/test/test-login.js
  50. 20 0
      user-server/test/test-new-login.js
  51. 31 0
      user-server/test/test-signup.js
  52. 6 0
      user-server/user.json

+ 2 - 0
.gitignore

@@ -70,3 +70,5 @@ testem.log
 # System files
 .DS_Store
 Thumbs.db
+
+node_modules/

+ 58 - 0
docs-prod/shop-schema.md

@@ -0,0 +1,58 @@
+
+
+```plantuml
+@startuml
+' 设置类图方向为从左到右
+left to right direction
+
+' 定义用户表实体
+class User {
+  + user_id : INT [PK]
+  username : VARCHAR
+}
+
+' 定义商品表实体
+class Goods {
+  + goods_id : INT [PK]
+  goods_name : VARCHAR
+  unit : VARCHAR
+  unit_price : DECIMAL
+}
+
+' 定义订单主表实体
+class Order {
+  + order_id : VARCHAR [PK]
+  order_date : DATETIME
+  user_id : INT [FK]
+}
+
+' 定义订单明细表实体
+class OrderDetail {
+  + detail_id : INT [PK]
+  order_id : VARCHAR [FK]
+  goods_id : INT [FK]
+  quantity : INT
+  unit_price : DECIMAL
+}
+
+' 定义关联关系
+User "1" --> "0..*" Order : 创建
+Order "1" --> "1..*" OrderDetail : 包含
+Goods "1" --> "0..*" OrderDetail : 被购买
+
+' 添加范式说明注释
+note top of Goods
+  第三范式实现:
+  goods_name/unit 独立存储在商品表
+  通过 goods_id 外键关联消除冗余
+end note
+
+note top of OrderDetail
+  第三范式实现:
+  存储下单时的 unit_price(快照)
+  总价 = quantity * unit_price
+  不存储冗余的 goods_name/unit
+end note
+
+@enduml
+```

+ 7 - 0
example-book/6.0.cloud.object/README.md

@@ -0,0 +1,7 @@
+# 演示微服务案例
+- tutor-fecth.js 网络请求增删查改
+- cloud-object.js 面向对象增删改
+- tutor-cloud.js 面向对象增删改示例
+- cloud-object.js 面向对象查询
+- cloud-query.js 面向对象查询示例
+

+ 75 - 0
example-book/6.0.cloud.object/cloud-object.js

@@ -0,0 +1,75 @@
+
+/**
+ @example
+    let tutor = new CloudObject("Tutor")
+    tutor.set({
+        name:"宋教练",
+        age:22,
+        gender:"女"
+    })
+    tutor.save()
+
+    let food = new CloudObject("FoodMenu")
+    food.set({
+        title:"馅饼",
+        price:10,
+    })
+    food.save()
+
+ */
+class CloudObject {
+    className
+    objectId
+    data = {}
+    constructor(className) {
+        this.className = className
+    }
+    set(data) {
+        Object.keys(data).forEach(key => {
+            this.data[key] = data[key]
+        })
+    }
+    async save() {
+        let method = "POST"
+        let url = "http://dev.fmode.cn:1337/parse/classes/" + this.className
+        if (this.objectId) {
+            method = "PUT"
+            url = url + "/" + this.objectId
+        }
+        delete this.data.createdAt
+        delete this.data.updatedAt
+        delete this.data.objectId
+
+        let response = await fetch(url, {
+            "headers": {
+                "content-type": "text/plain;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            "body": JSON.stringify(this.data),
+            "method": method,
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let data = await response.json()
+        this.objectId = data.objectId
+    }
+
+    async destroy() {
+        let url = "http://dev.fmode.cn:1337/parse/classes/" + this.className + "/" + this.objectId
+
+        let response = await fetch(url, {
+            "headers": {
+                "content-type": "text/plain;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "DELETE",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let data = await response.json()
+        this.objectId = data.objectId
+    }
+}
+
+module.exports.CloudObject = CloudObject

+ 105 - 0
example-book/6.0.cloud.object/cloud-query.js

@@ -0,0 +1,105 @@
+/**
+ * 查询类
+ @example
+// 查询指定教练,修改价格
+    let query = new CloudQuery("Tutor");
+    let tutor = await query.get("imkGFr6Ztj");
+    console.log('tutor', tutor)
+    tutor.set({
+        price: 199
+    })
+    tutor = await tutor.save()
+
+    // 条件查询:价格大于60
+    let query2 = new CloudQuery("FoodMenu");
+    query2.lessThan("price", 60);
+    // query2.greaterThan("price", 20);
+    let foodList = await query2.find();
+    console.log("foodList", foodList)
+
+    // 条件查询:性别等于女
+    let query3 = new CloudQuery("Tutor");
+    query3.equalTo("gender", "女");
+    let tutorList = await query3.find();
+    console.log("tutorList", tutorList);
+
+ */
+const { CloudObject } = require("./cloud-object")
+
+
+class CloudQuery {
+    className
+    queryParams = {
+        where: {}
+    }
+    constructor(className) {
+        this.className = className
+    }
+
+    async get(objectId) {
+        let res = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${objectId}?`, {
+            "headers": {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "x-parse-application-id": "dev"
+            },
+            "referrer": "http://localhost:4040/",
+            "referrerPolicy": "strict-origin-when-cross-origin",
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let data = await res.json()
+        let dataObj = new CloudObject(this.className);
+        dataObj.objectId = data.objectId
+        dataObj.set(data)
+        console.log(data, dataObj)
+        return dataObj
+    }
+    equalTo(key, value) {
+        this.queryParams.where[key] = value
+    }
+    lessThan(key, value) {
+        this.queryParams.where[key] = { "$lt": value }
+    } g
+    greaterThan(key, value) {
+        this.queryParams.where[key] = { "$gt": value }
+    }
+    async find() {
+        // where={%22price%22:{%22$gt%22:60}}
+        let whereStr = ``
+        if (Object.keys(this.queryParams.where)?.length) {
+            whereStr = `where=` + JSON.stringify(this.queryParams.where)
+        }
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?${whereStr}`
+        console.log(url)
+        let res = await fetch(url, {
+            "headers": {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "if-none-match": "W/\"b2-UccTNjvsEYMR2Waro9KVbWJiRJs\"",
+                "x-parse-application-id": "dev"
+            },
+            "referrer": "http://localhost:4040/",
+            "referrerPolicy": "strict-origin-when-cross-origin",
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let data = await res.json()
+        let list = data.results
+        list = list.map(item => {
+            let dataObj = new CloudObject(this.className);
+            dataObj.objectId = item.objectId
+            dataObj.set(item)
+            return dataObj
+        })
+        console.log(list)
+        return list
+    }
+
+}
+
+module.exports.CloudQuery = CloudQuery

+ 21 - 0
example-book/6.0.cloud.object/tutor-cloud.js

@@ -0,0 +1,21 @@
+const { CloudObject } = require("./cloud-object")
+
+async function main() {
+    let tutor = new CloudObject("Tutor")
+    tutor.set({
+        name: "宋教练",
+        age: 22,
+        gender: "女"
+    })
+    tutor.save()
+
+    let foodMenu = new CloudObject("FoodMenu")
+    foodMenu.set({
+        title: "馅饼",
+        price: 10,
+    })
+    foodMenu.save()
+}
+
+
+main();

+ 56 - 0
example-book/6.0.cloud.object/tutor-fetch.js

@@ -0,0 +1,56 @@
+
+
+async function createTutor() {
+    let response = await fetch("http://dev.fmode.cn:1337/parse/classes/Tutor", {
+        "headers": {
+            "content-type": "text/plain;charset=UTF-8",
+            "x-parse-application-id": "dev"
+        },
+        "body": JSON.stringify({
+            name: "王教练",
+            age: 32
+        }),
+        "method": "POST",
+        "mode": "cors",
+        "credentials": "omit"
+    });
+    let data = await response.json()
+    console.log(data)
+}
+
+// createTutor()
+
+
+
+async function updateTutor(objectId, updateData) {
+    let response = await fetch("http://dev.fmode.cn:1337/parse/classes/Tutor/" + objectId, {
+        "headers": {
+            "content-type": "text/plain;charset=UTF-8",
+            "x-parse-application-id": "dev"
+        },
+        "body": JSON.stringify(updateData),
+        "method": "PUT",
+        "mode": "cors",
+        "credentials": "omit"
+    });
+    let data = await response.json()
+    console.log(data)
+}
+
+// updateTutor("DlvWtHrQRt", { price: 200 })
+
+async function deleteTutor(objectId) {
+    let response = await fetch("http://dev.fmode.cn:1337/parse/classes/Tutor/" + objectId, {
+        "headers": {
+            "content-type": "text/plain;charset=UTF-8",
+            "x-parse-application-id": "dev"
+        },
+        "body": null,
+        "method": "DELETE",
+        "mode": "cors",
+        "credentials": "omit"
+    });
+    let data = await response.json()
+    console.log(data)
+}
+deleteTutor("DlvWtHrQRt")

+ 29 - 0
example-book/6.0.cloud.object/tutor-query.js

@@ -0,0 +1,29 @@
+const { CloudQuery } = require("./cloud-query");
+
+async function main() {
+
+    // 查询指定教练,修改价格
+    let query = new CloudQuery("Tutor");
+    let tutor = await query.get("imkGFr6Ztj");
+    console.log('tutor', tutor)
+    tutor.set({
+        price: 199
+    })
+    tutor = await tutor.save()
+
+    // 条件查询:价格大于60
+    let query2 = new CloudQuery("FoodMenu");
+    query2.lessThan("price", 60);
+    // query2.greaterThan("price", 20);
+    let foodList = await query2.find();
+    console.log("foodList", foodList)
+
+    // 条件查询:性别等于女
+    let query3 = new CloudQuery("Tutor");
+    query3.equalTo("gender", "女");
+    let tutorList = await query3.find();
+    console.log("tutorList", tutorList);
+}
+
+
+main();

+ 19 - 19
src/agent/tasks/inquiry/2.inquiry-doctor-question.ts

@@ -52,25 +52,25 @@ export function TaskInqueryDoctorQuestion(options:{
                 let completion = new FmodeChatCompletion([
                     {role:"system",content:""},
                     {role:"user",content:PromptTemplate}
-                    ])
-                    completion.sendCompletion().subscribe((message:any)=>{
-                        if(task1.progress < 0.5){
-                            task1.progress += 0.1
-                        }
-                        if(task1.progress >= 0.5 && task1.progress <= 0.9){
-                            task1.progress += 0.01
-                        }
-                        if(task1.progress >= 0.7){
-                            task1.progress += 0.001
-                        }
-                        // 打印消息体
-                        console.log(message.content)
-                        // 赋值消息内容给组件内属性
-                        if(message.complete){ // 判断message为完成状态,则设置isComplete为完成
-                            options.shareData.userStory.questionList = extactAndParseJsonFromString(message.content).questionList || []
-                            task1.progress = 1
-                            resolve(true)
-                        }
+                ])
+                completion.sendCompletion().subscribe((message:any)=>{
+                    if(task1.progress < 0.5){
+                        task1.progress += 0.1
+                    }
+                    if(task1.progress >= 0.5 && task1.progress <= 0.9){
+                        task1.progress += 0.01
+                    }
+                    if(task1.progress >= 0.7){
+                        task1.progress += 0.001
+                    }
+                    // 打印消息体
+                    console.log(message.content)
+                    // 赋值消息内容给组件内属性
+                    if(message.complete){ // 判断message为完成状态,则设置isComplete为完成
+                        options.shareData.userStory.questionList = extactAndParseJsonFromString(message.content).questionList || []
+                        task1.progress = 1
+                        resolve(true)
+                    }
                 })
             })
 

+ 44 - 44
src/app/app.routes.ts

@@ -5,54 +5,54 @@ export const routes: Routes = [
     path: '',
     loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes),
   },
-   // 向量:文本特征向量
-   {
-      path: "text/embed",
-      loadComponent: () => import('../modules/text/page-text-embed/page-text-embed.component').then(m => m.PageTextEmbedComponent),
-   },
+  // 向量:文本特征向量
+  {
+    path: "text/embed",
+    loadComponent: () => import('../modules/text/page-text-embed/page-text-embed.component').then(m => m.PageTextEmbedComponent),
+  },
   // 知识库:杭州市人才政策知识库
   {
-      path: "story/hangzhou",
-      loadComponent: () => import('../modules/story/page-hangzhou/page-hangzhou.component').then(m => m.PageHangzhouComponent),
-      runGuardsAndResolvers: "always",
+    path: "story/hangzhou",
+    loadComponent: () => import('../modules/story/page-hangzhou/page-hangzhou.component').then(m => m.PageHangzhouComponent),
+    runGuardsAndResolvers: "always",
   },
   // 向量:面部特征向量
+  // {
+  //     path: "face/feat68",
+  //     loadComponent: () => import('../modules/face/page-feat68/page-feat68.component').then(m => m.PageFeat68Component),
+  //     runGuardsAndResolvers: "always",
+  // },
+  // 聊天模块
+  {
+    path: "chat/session/role/:roleId",
+    loadComponent: () => import('./test-chat-panel/test-chat-panel.component').then(m => m.TestChatPanelComponent),
+    runGuardsAndResolvers: "always",
+  },
+  {
+    path: "chat/session/chat/:chatId",
+    loadComponent: () => import('./test-chat-panel/test-chat-panel.component').then(m => m.TestChatPanelComponent),
+    runGuardsAndResolvers: "always",
+  },
+  {
+    path: 'chat/pro/chat/:chatId',
+    redirectTo: '/chat/session/chat/:chatId',
+    pathMatch: 'full'
+  },
+  // 流程任务模块
+  {
+    path: "flow/editor/new",
+    loadComponent: () => import('../modules/flow/comp-flow-editor/comp-flow-editor.component').then(m => m.CompFlowEditorComponent),
+    runGuardsAndResolvers: "always",
+  },
+  {
+    path: "flow/test",
+    loadComponent: () => import('../modules/flow/page-flow-test/page-flow-test.component').then(m => m.PageFlowTestComponent),
+    runGuardsAndResolvers: "always",
+  },
+  // 测试任务模块
   {
-      path: "face/feat68",
-      loadComponent: () => import('../modules/face/page-feat68/page-feat68.component').then(m => m.PageFeat68Component),
-      runGuardsAndResolvers: "always",
-  },
-   // 聊天模块
-   {
-      path: "chat/session/role/:roleId",
-      loadComponent: () => import('./test-chat-panel/test-chat-panel.component').then(m => m.TestChatPanelComponent),
-      runGuardsAndResolvers: "always",
-  },
-  {
-      path: "chat/session/chat/:chatId",
-      loadComponent: () => import('./test-chat-panel/test-chat-panel.component').then(m => m.TestChatPanelComponent),
-      runGuardsAndResolvers: "always",
-  },
-  {
-      path: 'chat/pro/chat/:chatId',
-      redirectTo: '/chat/session/chat/:chatId',
-      pathMatch: 'full'
-    },
-    // 流程任务模块
-    {
-        path: "flow/editor/new",
-        loadComponent: () => import('../modules/flow/comp-flow-editor/comp-flow-editor.component').then(m => m.CompFlowEditorComponent),
-        runGuardsAndResolvers: "always",
-    },
-    {
-      path: "flow/test",
-      loadComponent: () => import('../modules/flow/page-flow-test/page-flow-test.component').then(m => m.PageFlowTestComponent),
-      runGuardsAndResolvers: "always",
-  },
-    // 测试任务模块
-    {
-      path: "task/test",
-      loadComponent: () => import('../modules/task/page-test-completion/page-test-completion.component').then(m => m.PageTestCompletionComponent),
-      runGuardsAndResolvers: "always",
+    path: "task/test",
+    loadComponent: () => import('../modules/task/page-test-completion/page-test-completion.component').then(m => m.PageTestCompletionComponent),
+    runGuardsAndResolvers: "always",
   }
 ];

+ 73 - 3
src/app/tab2/tab2.page.html

@@ -16,6 +16,7 @@
       </ion-card-header>
       <ion-card-content>
         <p>我们的智能助手可以为您提供多种专业领域的咨询服务,点击下方按钮开始体验。</p>
+        <ion-button (click)="goTraining()">文本补全:指定训练计划</ion-button>
       </ion-card-content>
     </ion-card>
   </div>
@@ -39,7 +40,7 @@
       </ion-col>
 
       <ion-col size="12" size-md="6">
-        <ion-card class="feature-card" button (click)="restoreChat('yHEHqMQDNv')">
+        <ion-card class="feature-card" button (click)="restoreChat('0w3es9v8B6')">
           <ion-card-header>
             <ion-icon name="time" color="secondary" class="feature-icon"></ion-icon>
             <ion-card-title>恢复会话</ion-card-title>
@@ -57,7 +58,7 @@
 
     <ion-row>
       <ion-col size="12">
-        <ion-card class="special-feature" button (click)="openInquiry()">
+        <ion-card class="special-feature" button>
           <ion-card-header>
             <ion-icon name="medical" color="danger" class="feature-icon"></ion-icon>
             <ion-card-title>门诊问诊服务</ion-card-title>
@@ -66,12 +67,81 @@
           <ion-card-content>
             我们的医疗智能助手可以为您提供初步的医疗咨询和建议,帮助您了解可能的健康问题。
           </ion-card-content>
-          <ion-button fill="clear" expand="block" color="danger">
+          <ion-button (click)="openInquiry()" fill="clear" expand="block" color="danger">
             进入门诊
             <ion-icon slot="end" name="medkit"></ion-icon>
           </ion-button>
+          <ion-button (click)="openInquiry('0w3es9v8B6')" fill="clear" expand="block" color="primary">
+            恢复对话
+            <ion-icon slot="end" name="medkit"></ion-icon>
+          </ion-button>
         </ion-card>
       </ion-col>
     </ion-row>
+
+    <ion-row>
+      <ion-col size="12" size-md="8" offset-md="2">
+        <ion-card class="special-feature" button>
+          <ion-card-header class="ion-text-center">
+            <ion-icon name="barbell" color="primary" class="feature-icon" style="font-size: 48px;"></ion-icon>
+            <ion-card-title class="ion-padding-top">运动健身服务</ion-card-title>
+            <ion-card-subtitle>专业健身咨询 - 打造完美身材</ion-card-subtitle>
+          </ion-card-header>
+
+          <ion-card-content>
+            <ion-text color="medium" class="ion-text-center ion-margin-bottom">
+              <p>我们的专业教练团队随时为您提供个性化健身指导</p>
+              <ion-button fill="clear" size="small" (click)="loadConsult()">
+                <ion-icon slot="icon-only" name="refresh"></ion-icon>
+              </ion-button>
+            </ion-text>
+
+            @if(consultList?.length){
+            <ion-list lines="none" class="consult-history">
+              <ion-list-header>
+                <ion-label>历史咨询记录</ion-label>
+                <ion-button fill="clear" size="small" (click)="loadConsult()">
+                  <ion-icon slot="icon-only" name="refresh"></ion-icon>
+                </ion-button>
+              </ion-list-header>
+
+              @for(consult of consultList; track consult){
+              <ion-item button detail (click)="openConsult(consult.get('chatId'))" class="consult-item">
+                @if(consult.get("avatar")){
+                <ion-avatar slot="start">
+                  <img [src]="consult.get('avatar')" alt="教练头像">
+                </ion-avatar>
+                } @else {
+                <ion-avatar slot="start">
+                  <ion-icon name="person-circle" style="font-size: 40px;"></ion-icon>
+                </ion-avatar>
+                }
+
+                <ion-label>
+                  <h3>{{consult.get("name") || '未命名咨询'}}</h3>
+                  <p>{{consult.updatedAt | date:'yyyy-MM-dd HH:mm'}}</p>
+                </ion-label>
+
+                <ion-badge slot="end" color="light">
+                  <ion-icon name="chevron-forward" color="medium"></ion-icon>
+                </ion-badge>
+              </ion-item>
+              }
+            </ion-list>
+            }
+          </ion-card-content>
+
+          <ion-footer class="ion-no-border">
+            <ion-toolbar>
+              <ion-button (click)="openConsult()" expand="block" color="primary" shape="round">
+                <ion-icon slot="start" name="chatbubbles"></ion-icon>
+                开始新咨询
+              </ion-button>
+            </ion-toolbar>
+          </ion-footer>
+        </ion-card>
+      </ion-col>
+    </ion-row>
+
   </ion-grid>
 </ion-content>

+ 105 - 51
src/app/tab2/tab2.page.scss

@@ -1,65 +1,119 @@
 /* 页面整体样式 */
 ion-content {
-    --background: #f5f5f5;
+  --background: #f5f5f5;
+}
+
+/* 欢迎卡片样式 */
+.welcome-card {
+  text-align: center;
+  margin: 20px auto;
+  max-width: 800px;
+  border-radius: 15px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+
+  ion-card-header {
+    padding-bottom: 0;
   }
-  
-  /* 欢迎卡片样式 */
-  .welcome-card {
+}
+
+/* 功能卡片样式 */
+.features-grid {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.feature-card {
+  height: 100%;
+  border-radius: 12px;
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+  }
+
+  ion-card-header {
     text-align: center;
-    margin: 20px auto;
-    max-width: 800px;
-    border-radius: 15px;
-    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
-    ion-card-header {
-      padding-bottom: 0;
-    }
+    padding-bottom: 0;
   }
-  
-  /* 功能卡片样式 */
-  .features-grid {
-    max-width: 1200px;
-    margin: 0 auto;
+
+  ion-card-content {
+    min-height: 80px;
   }
-  
-  .feature-card {
-    height: 100%;
-    border-radius: 12px;
-    transition: transform 0.3s ease, box-shadow 0.3s ease;
-    &:hover {
-      transform: translateY(-5px);
-      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
-    }
-    ion-card-header {
-      text-align: center;
-      padding-bottom: 0;
-    }
-    ion-card-content {
-      min-height: 80px;
-    }
+}
+
+.feature-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+/* 特殊功能卡片样式 */
+.special-feature {
+  margin-top: 20px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #fff8f8, #ffffff);
+  border-left: 4px solid var(--ion-color-danger);
+
+  ion-card-header {
+    text-align: center;
   }
-  
-  .feature-icon {
-    font-size: 48px;
-    margin-bottom: 16px;
+
+  ion-card-content {
+    min-height: 80px;
   }
-  
-  /* 特殊功能卡片样式 */
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+
+  .feature-card,
   .special-feature {
-    margin-top: 20px;
+    margin-bottom: 20px;
+  }
+}
+
+.special-feature {
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+
+  .feature-icon {
+    margin-bottom: 8px;
+  }
+
+  .consult-history {
+    background: var(--ion-color-light);
     border-radius: 12px;
-    background: linear-gradient(135deg, #fff8f8, #ffffff);
-    border-left: 4px solid var(--ion-color-danger);
-    ion-card-header {
-      text-align: center;
+    margin: 16px 0;
+
+    ion-list-header {
+      --background: transparent;
+      --color: var(--ion-color-medium);
     }
-    ion-card-content {
-      min-height: 80px;
+
+    .consult-item {
+      --border-radius: 8px;
+      --padding-start: 8px;
+      --padding-end: 8px;
+      margin-bottom: 8px;
+
+      ion-avatar {
+        width: 40px;
+        height: 40px;
+      }
+
+      &:hover {
+        --background: rgba(var(--ion-color-primary-rgb), 0.08);
+      }
     }
   }
-  
-  /* 响应式调整 */
-  @media (max-width: 768px) {
-    .feature-card, .special-feature {
-      margin-bottom: 20px;
+
+  ion-footer {
+    ion-toolbar {
+      --padding-top: 0;
+      --padding-bottom: 0;
+      --padding-start: 16px;
+      --padding-end: 16px;
     }
-  }
+  }
+}

+ 301 - 103
src/app/tab2/tab2.page.ts

@@ -1,38 +1,32 @@
+import { CommonModule } from '@angular/common';
 import { Component } from '@angular/core';
 import { Router } from '@angular/router';
-import { IonHeader, IonToolbar, IonTitle, IonContent, ModalController, IonButton } from '@ionic/angular/standalone';
-import { ExploreContainerComponent } from '../explore-container/explore-container.component';
+import {
+  ModalController,
+  IonHeader, IonTitle, IonContent,
+  IonButton, IonList, IonItem, IonLabel, IonIcon, IonCardContent, IonCardSubtitle, IonCardTitle,
+  IonCardHeader, IonCard, IonRow, IonCol, IonGrid,
+  IonAvatar, IonBadge, IonText, IonListHeader, IonToolbar, IonFooter
+} from '@ionic/angular/standalone';
+
+import { NavController } from '@ionic/angular/standalone';
 import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
 // import { ModalAudioMessageComponent } from 'fmode-ng/lib/aigc/chat/chat-modal-input/modal-audio-message/modal-audio-message.component';
 import Parse from "parse";
-import { 
-  IonCard,
-  IonCardHeader,
-  IonCardTitle,
-  IonCardSubtitle,
-  IonCardContent,
-  IonGrid,
-  IonRow,
-  IonCol,
-  IonIcon
-} from '@ionic/angular/standalone';
+
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   standalone: true,
   imports: [
-    IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent,
-    IonButton,
-    IonCard,
-    IonCardHeader,
-    IonCardTitle,
-    IonCardSubtitle,
-    IonCardContent,
-    IonGrid,
-    IonRow,
-    IonCol,
-    IonIcon,
+    CommonModule,
+    IonHeader, IonTitle, IonContent,
+    IonButton, IonList, IonItem, IonLabel, IonIcon, IonAvatar,
+    IonCardContent, IonCardSubtitle, IonCardTitle,
+    IonCardHeader, IonCard, IonRow, IonCol, IonGrid,
+    IonAvatar, IonBadge, IonText, IonListHeader, IonToolbar, IonFooter,
     // ASR语音输入模块
     FmChatModalInput,
     // ModalAudioMessageComponent
@@ -41,49 +35,76 @@ import {
 export class Tab2Page {
 
   constructor(
-    private modalCtrl:ModalController,
-    private router:Router,
-    ) {
+    private modalCtrl: ModalController,
+    private router: Router,
+    private navCtrl: NavController,
+  ) {
 
   }
-  title:string = "123"
+  goTraining() {
+    this.navCtrl.navigateRoot(['tabs', 'demo', 'training'])
+  }
+
+  title: string = "123"
   /** 示例:问诊ChatPanel面板 */
-  openInquiry(){
-    localStorage.setItem("company","E4KpGvTEto")
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      onChatInit:(chat:FmodeChat)=>{
+  openInquiry(chatId?: string) {
+    localStorage.setItem("company", "E4KpGvTEto")
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK", // 预设,无需更改
+      chatId: chatId, // 若存在,则恢复会话。若不存在,则开启新会话
+      onChatInit: (chat: FmodeChat) => {
         console.log("onChatInit");
-        console.log("预设角色",chat.role);
+        console.log("Chat类", chat);
+        console.log("预设角色", chat.role);
         // 角色名称
-        chat.role.set("name","晓晓");
+        chat.role.set("name", "晓晓");
         // 角色称号
-        chat.role.set("title","全科医生");
+        chat.role.set("title", "全科医生");
         // 角色描述
-        chat.role.set("desc","一名亲切和蔼的门诊全科主任医生,晓晓,年龄36岁");
+        chat.role.set("desc", "一名亲切和蔼的门诊全科主任医生,晓晓,年龄36岁");
         // 角色标签
-        chat.role.set("tags",["全科","门诊"]);
+        chat.role.set("tags", ["全科", "门诊"]);
         // 角色头像
-        chat.role.set("avatar","https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
+        chat.role.set("avatar", "https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
         // 角色提示词
-        chat.role.set("prompt",`
+        chat.role.set("prompt", `
 # 角色设定
 您是一名亲切和蔼的专业的全科医生,晓晓,年龄36岁,需要完成一次完整的门诊服务。
 
 # 对话环节
-0.导诊(根据用户基本情况,引导挂号合适的科室)
-1.预设的问询方式(感冒问呼吸、肚子疼叩诊)
-- 打招呼,以用户自述为主
-- 当信息充足时候,确认用户症状对应的科室,并进入下一个环节
-2.拓展的问询细节
-例如:用户反映呼吸不畅,拓展出:是否咳嗽;是否感觉痛或者痒等其他需要的问题。
-- 当问询细节补充完成后进入下一个环节
-3.初步的诊断结果,并且同时列出检查检验项目
-初步诊断:确定需要有哪些进一步检查
-检查检验:获取医学客观数据
-- 等待用户提交客观数据,进入下一阶段
-4.给出诊断方案并给出处方
-- 完成处方时,请在消息结尾附带: [完成]
+请您严格按照以下环节和用户展开对话,并且注意完成每个环节时,一定要携带[xx完成]的标记。
+## 1. 导诊环节
+- **开始话语**:
+  - “欢迎来到医院,请问您是第一次来吗?我会帮助您找到合适的科室。”
+- **进入下个环节条件**:
+    - “已经大致了解您反映的情况,建议您到XX科室。[导诊完成]
+
+## 2. 问诊环节
+- **对话内容**:
+  - “请您详细描述一下您的情况,我需要了解您的病史和相关症状。”
+  - “您是否有过敏史或其他健康问题?”
+  - “根据您的情况,我认为我们需要进行一些检查,您觉得可以吗?”
+
+- **进入下个环节的条件**:
+  - “谢谢您的配合,我将为您安排相关检查。[问诊完成]”
+
+## 3. 检查环节
+- **对话内容**:
+  - “我们已经完成了问诊,现在我会为您安排必要的检查。”
+  - “请您稍等,检查结果会在不久后出来。”
+  - “检查结果已经出来了,请您填写下报告的具体数据,让我来为您分析一下。”
+
+- **进入下个环节的条件**:
+  - “检查结果已经初步分析,接下来需要请主任医生开始诊断。[检查完成]”
+
+## 4. 诊断与处方环节
+- **对话内容**:
+  - “根据问诊和检查结果,我的诊断是……”
+  - “接下来,我会为您开具相应的处方,请您仔细阅读治疗方案和注意事项。”
+  - “您是否有任何问题或者需要进一步的解释?”
+
+- **进入下个环节的条件**:
+  - “感谢您的配合,您的处方已经开好,请您按照建议进行后续的治疗或复诊安排。[处方完成]”
 
 # 开始话语
 当您准备好了,可以以一个医生的身份,向来访的用户打招呼。`);
@@ -103,26 +124,26 @@ export class Tab2Page {
           }
         ]
         setTimeout(() => {
-          chat.role.set("promptCates",promptCates)
+          chat.role.set("promptCates", promptCates)
         }, 500);
         // 对话灵感列表
         let promptList = [
           {
-            cate:"外科",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/r1ltv1023812146.png",
-            messageList:["局部疼痛或肿胀","伤口出血或感染","关节活动受限","体表肿块或结节","外伤后活动障碍","皮肤溃疡不愈合","异物刺入或嵌顿","术后并发症复查","肢体麻木或无力","运动损伤疼痛"]
+            cate: "外科123", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/r1ltv1023812146.png",
+            messageList: ["局部疼痛或肿胀", "伤口出血或感染", "关节活动受限", "体表肿块或结节", "外伤后活动障碍", "皮肤溃疡不愈合", "异物刺入或嵌顿", "术后并发症复查", "肢体麻木或无力", "运动损伤疼痛"]
           },
           {
-            cate:"内科",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fo81fg034154259.png",
-            messageList:["反复发热或低热","持续咳嗽咳痰","胸闷气短心悸","慢性腹痛腹泻","头晕头痛乏力","体重骤增或骤减","食欲异常或消化不良","尿频尿急尿痛","睡眠障碍易醒","异常出汗或怕冷"]
+            cate: "内科", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fo81fg034154259.png",
+            messageList: ["反复发热或低热", "持续咳嗽咳痰", "胸闷气短心悸", "慢性腹痛腹泻", "头晕头痛乏力", "体重骤增或骤减", "食欲异常或消化不良", "尿频尿急尿痛", "睡眠障碍易醒", "异常出汗或怕冷"]
           },
           {
-            cate:"心理",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fc1nqi034201098.png",
-            messageList:["持续情绪低落","焦虑紧张不安","失眠或睡眠过多","注意力难以集中","社交恐惧回避","强迫思维或行为","记忆减退疑虑","躯体无器质性疼痛","自杀倾向念头","现实感丧失体验"]
+            cate: "心理", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fc1nqi034201098.png",
+            messageList: ["持续情绪低落", "焦虑紧张不安", "失眠或睡眠过多", "注意力难以集中", "社交恐惧回避", "强迫思维或行为", "记忆减退疑虑", "躯体无器质性疼痛", "自杀倾向念头", "现实感丧失体验"]
           },
         ]
         let ChatPrompt = Parse.Object.extend("ChatPrompt");
         setTimeout(() => {
-          chat.promptList = promptList.map(item=>{
+          chat.promptList = promptList.map(item => {
             let prompt = new ChatPrompt();
             prompt.set(item);
             prompt.img = item.img;
@@ -133,74 +154,251 @@ export class Tab2Page {
         // 功能按钮区域预设
         chat.leftButtons = [
           { // 提示 当角色配置预设提示词时 显示
-           title:"话题灵感", // 按钮标题
-           showTitle:true, // 是否显示标题文字
-           icon:"color-wand-outline", // 标题icon图标
-           onClick:()=>{ // 按钮点击事件
-               chat.isPromptModalOpen = true
-           },
-           show:()=>{ // 按钮显示条件
-             return chat?.promptList?.length // 存在话题提示词时显示
-           }
-         },
-         { // 总结 结束并归档本次对话
-            title:"门诊归档",
-            showTitle:true,
-            icon:"archive-outline",
-            onClick:()=>{
-                // 门诊归档,记录用户门诊咨询,并进行过程评价
-                console.log(chat?.chatSession) // 本次会话内容数据
-              },
-            show:()=>{ 
+            title: "话题灵感", // 按钮标题
+            showTitle: true, // 是否显示标题文字
+            icon: "color-wand-outline", // 标题icon图标
+            onClick: () => { // 按钮点击事件
+              chat.isPromptModalOpen = true
+            },
+            show: () => { // 按钮显示条件
+              return chat?.promptList?.length // 存在话题提示词时显示
+            }
+          },
+          { // 总结 结束并归档本次对话
+            title: "门诊归档",
+            showTitle: true,
+            icon: "archive-outline",
+            onClick: () => {
+              // 门诊归档,记录用户门诊咨询,并进行过程评价
+              console.log(chat?.chatSession) // 本次会话内容数据
+            },
+            show: () => {
               return true // 一直显示
             }
-        }
-      ]
+          }
+        ]
 
       },
-      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
-        console.log("onMessage",message)
-        let content:any = message?.content
-        if(typeof content == "string"){
-          if(content?.indexOf("[完成]")>-1){
-            console.log("门诊已完成")
+      onMessage: (chat: FmodeChat, message: FmodeChatMessage) => {
+        console.log("onMessage", message)
+        let content: any = message?.content
+        if (typeof content == "string") {
+          // 根据阶段标记判断下一步处理过程
+          if (content.includes('[导诊完成]')) {
+            // 进入问诊环节
+            console.log('进入问诊环节');
+          } else if (content.includes('[问诊完成]')) {
+            // 进入检查环节
+            console.log('进入检查环节');
+          } else if (content.includes('[检查完成]')) {
+            // 进入诊断与处方环节
+            console.log('进入诊断与处方环节');
+          } else if (content.includes('[处方完成]')) {
+            // 结束会话或其他逻辑
+            console.log('结束会话');
           }
         }
       },
-      onChatSaved:(chat:FmodeChat)=>{
+      onChatSaved: (chat: FmodeChat) => {
         // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
       }
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
+  openConsult(chatId?: string) {
+    localStorage.setItem("company", "E4KpGvTEto")
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK", // 预设,无需更改
+      chatId: chatId, // 若存在,则恢复会话。若不存在,则开启新会话
+      onChatInit: (chat: FmodeChat) => {
+        console.log("onChatInit");
+        console.log("Chat类", chat);
+        console.log("预设角色", chat.role);
+        // 角色名称
+        chat.role.set("name", "宋珀尔");
+        // 角色称号
+        chat.role.set("title", "专业教练");
+        // 角色描述
+        chat.role.set("desc", "一名亲切和蔼的健身教练,宋珀尔,年龄26岁");
+        // 角色标签
+        chat.role.set("tags", ['跑步', '动感单车']);
+        // 角色头像
+        chat.role.set("avatar", "/assets/avatars/jiaolian1.jpg")
+        // 角色提示词
+        chat.role.set("prompt", `
+# 角色设定
+您是一名亲切和蔼的健身教练,宋珀尔,年龄26岁,需要您解答用户健身方面的专业问题。
+请您模仿一个正常教练对话,语言简短不需要长篇大论。
+`);
+        // 对话灵感分类
+        let promptCates = [
+          {
+            "img": "/assets/icon/yy.jpg",
+            "name": "有氧"
+          },
+          {
+            "img": "/assets/icon/jz.jpg",
+            "name": "减脂"
+          },
+          {
+            "img": "/assets/icon/zj.jpg",
+            "name": "增肌"
+          }
+        ]
+        setTimeout(() => {
+          chat.role.set("promptCates", promptCates)
+        }, 500);
+        // 对话灵感列表
+        let promptList = [
+          {
+            cate: "有氧", img: "/assets/icon/yy.jpg",
+            messageList: [
+              "有氧运动多久才能有效减脂?",
+              "跑步和游泳哪个减肥效果更好?",
+              "空腹有氧真的更燃脂吗?",
+              "有氧运动会不会掉肌肉?",
+              "心率控制在多少才能高效燃脂?",
+              "每天做有氧运动会不会过度疲劳?",
+              "有氧运动前要不要吃东西?",
+              "椭圆机和跑步机哪个更适合新手?",
+              "跳绳会不会伤膝盖?",
+              "有氧运动后怎么补充能量?"
+            ]
+          },
+          {
+            cate: "减脂", img: "/assets/icon/jz.jpg",
+            messageList: [
+              "减脂一定要做有氧吗?",
+              "为什么体重没变但看起来瘦了?",
+              "局部减脂(如瘦肚子)真的存在吗?",
+              "减脂期每天应该吃多少热量?",
+              "低碳饮食和低脂饮食哪个更适合减脂?",
+              "为什么运动后体重反而增加了?",
+              "减脂期可以吃零食吗?",
+              "平台期怎么突破?",
+              "晚上吃东西会不会更容易长胖?",
+              "减脂期要不要计算蛋白质摄入?"
+            ]
+          },
+          {
+            cate: "增肌", img: "/assets/icon/zj.jpg",
+            messageList: [
+              "增肌一定要喝蛋白粉吗?",
+              "为什么练了很久肌肉不长?",
+              "增肌期可以同时减脂吗?",
+              "训练后多久补充蛋白质最有效?",
+              "增肌需要每天练同一个部位吗?",
+              "徒手训练(如俯卧撑)能有效增肌吗?",
+              "增肌期体重不增长是怎么回事?",
+              "肌肉酸痛还能继续练吗?",
+              "增肌训练每组做多少次最合适?",
+              "睡眠对增肌的影响有多大?"
+            ]
+          },
+        ]
+        let ChatPrompt = Parse.Object.extend("ChatPrompt");
+        setTimeout(() => {
+          chat.promptList = promptList.map(item => {
+            let prompt = new ChatPrompt();
+            prompt.set(item);
+            prompt.img = item.img;
+            return prompt;
+          })
+        }, 500);
 
+        // 功能按钮区域预设
+        chat.leftButtons = [
+          { // 提示 当角色配置预设提示词时 显示
+            title: "话题灵感", // 按钮标题
+            showTitle: true, // 是否显示标题文字
+            icon: "color-wand-outline", // 标题icon图标
+            onClick: () => { // 按钮点击事件
+              chat.isPromptModalOpen = true
+            },
+            show: () => { // 按钮显示条件
+              return chat?.promptList?.length // 存在话题提示词时显示
+            }
+          },
+        ]
+
+      },
+      onMessage: (chat: FmodeChat, message: FmodeChatMessage) => {
+        console.log("onMessage", message)
+        let content: any = message?.content
+        if (typeof content == "string") {
+          // 根据阶段标记判断下一步处理过程
+          if (content.includes('[导诊完成]')) {
+            // 进入问诊环节
+            console.log('进入问诊环节');
+          } else if (content.includes('[问诊完成]')) {
+            // 进入检查环节
+            console.log('进入检查环节');
+          } else if (content.includes('[检查完成]')) {
+            // 进入诊断与处方环节
+            console.log('进入诊断与处方环节');
+          } else if (content.includes('[处方完成]')) {
+            // 结束会话或其他逻辑
+            console.log('结束会话');
+          }
+        }
+      },
+      onChatSaved: async (chat: FmodeChat) => {
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
+        let chatId = chat?.chatSession?.id;
+        console.log("chatId", chatId)
+        let query = new CloudQuery("TrainingConsult");
+        let trainingConsult = await query.get(chatId);
+        console.log("trainingConsult", trainingConsult);
+        if (!trainingConsult?.id) { // 若无重复记录,则实例化一个新的咨询记录
+          trainingConsult = new CloudObject("TrainingConsult")
+        }
+        trainingConsult.set({
+          "chatId": chatId,
+          "messageList": chat.messageList,
+          "name": chat.role.get("name"),
+          "avatar": chat.role.get("avatar")
+        })
+        console.log("trainingConsult", trainingConsult)
+        trainingConsult.save();
+      }
+    }
+    openChatPanelModal(this.modalCtrl, options)
+  }
+  // 加载健康咨询记录
+  consultList: Array<CloudObject> = []
+  async loadConsult() {
+    let query = new CloudQuery("TrainingConsult");
+    this.consultList = await query.find()
+    console.log(this.consultList)
+  }
   /**
    * 开始聊天
    */
-  openChat(){
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      onChatSaved:(chat:FmodeChat)=>{
+  openChat() {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      onChatSaved: (chat: FmodeChat) => {
         // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
       },
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
   /**
    * 恢复聊天
    * @chatId 从onChatSaved生命周期中,获取chat?.chatSession?.id
    */
-  restoreChat(chatId:string){
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      chatId:chatId
+  restoreChat(chatId: string) {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      chatId: chatId
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
 
-  goChat(){
+  goChat() {
     this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
   }
 

+ 6 - 3
src/app/tabs/tabs.page.html

@@ -16,8 +16,11 @@
       <ion-label>知识库</ion-label>
     </ion-tab-button>
 
-   
+    <ion-tab-button tab="mine" href="/tabs/mine">
+      <ion-icon aria-hidden="true" name="person-outline"></ion-icon>
+      <ion-label>我的</ion-label>
+    </ion-tab-button>
+
 
-   
   </ion-tab-bar>
-</ion-tabs>
+</ion-tabs>

+ 28 - 4
src/app/tabs/tabs.routes.ts

@@ -1,6 +1,7 @@
 import { Routes } from '@angular/router';
 import { TabsPage } from './tabs.page';
 import { TokenGuard } from 'src/lib/user/token-guard/token.guard';
+import { TestGuard } from './test.guard';
 
 export const routes: Routes = [
   {
@@ -9,28 +10,51 @@ export const routes: Routes = [
     children: [
       {
         path: 'tab1',
-        canActivate:[TokenGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../../modules/flow/page-flow-test/page-flow-test.component').then((m) => m.PageFlowTestComponent),
-          // import('../tab1/tab1.page').then((m) => m.Tab1Page),
+        // import('../tab1/tab1.page').then((m) => m.Tab1Page),
       },
       {
         path: 'tab2',
-        canActivate:[TokenGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../tab2/tab2.page').then((m) => m.Tab2Page),
       },
       {
         path: 'tab3',
-        canActivate:[TokenGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../tab3/tab3.page').then((m) => m.Tab3Page),
       },
+      {
+        path: 'mine',
+        loadComponent: () =>
+          import('../../lib/user/page-mine/page-mine.component').then((m) => m.PageMineComponent),
+      },
       {
         path: '',
         redirectTo: '/tabs/tab3',
         pathMatch: 'full',
       },
+      {
+        path: 'demo/training',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/page-training-plan/page-training-plan.component').then((m) => m.PageTrainingPlanComponent),
+      },
+      {
+        path: 'demo/bird/list',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/bird/page-bird-list/page-bird-list.component').then((m) => m.PageBirdListComponent),
+      },
+      {
+        path: 'demo/bird/display/:birdId',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/bird/page-bird-display/page-bird-display.component').then((m) => m.PageBirdDisplayComponent),
+      },
     ],
   },
   {

+ 11 - 0
src/app/tabs/test.guard.ts

@@ -0,0 +1,11 @@
+import { CanActivateFn } from '@angular/router';
+
+export const TestGuard: CanActivateFn = (route, state) => {
+  // 守卫:用户在 localStorage 开启 chatEnabled
+  let chatEnabled = localStorage.getItem("chatEnabled");
+  if(chatEnabled){
+    return true
+  }else{
+    return false;
+  }
+};

BIN
src/assets/avatars/jiaolian1.jpg


BIN
src/assets/icon/jz.jpg


BIN
src/assets/icon/yy.jpg


BIN
src/assets/icon/zj.jpg


+ 62 - 51
src/lib/ncloud.ts

@@ -1,9 +1,15 @@
 // CloudObject.ts
+
+let serverURL = `https://dev.fmode.cn/parse`;
+if (location.protocol == "http:") {
+    serverURL = `http://dev.fmode.cn:1337/parse`;
+}
+
 export class CloudObject {
     className: string;
-    id: string | null = null;
-    createdAt:any;
-    updatedAt:any;
+    id: string | undefined = undefined;
+    createdAt: any;
+    updatedAt: any;
     data: Record<string, any> = {};
 
     constructor(className: string) {
@@ -29,7 +35,7 @@ export class CloudObject {
 
     async save() {
         let method = "POST";
-        let url = `https://dev.fmode.cn/parse/classes/${this.className}`;
+        let url = serverURL + `/classes/${this.className}`;
 
         // 更新
         if (this.id) {
@@ -61,7 +67,7 @@ export class CloudObject {
 
     async destroy() {
         if (!this.id) return;
-        const response = await fetch(`https://dev.fmode.cn/parse/classes/${this.className}/${this.id}`, {
+        const response = await fetch(serverURL + `/classes/${this.className}/${this.id}`, {
             headers: {
                 "x-parse-application-id": "dev"
             },
@@ -73,7 +79,7 @@ export class CloudObject {
 
         const result = await response?.json();
         if (result) {
-            this.id = null;
+            this.id = undefined;
         }
         return true;
     }
@@ -82,13 +88,13 @@ export class CloudObject {
 // CloudQuery.ts
 export class CloudQuery {
     className: string;
-    queryParams: Record<string, any> = {};
+    queryParams: Record<string, any> = { where: {} };
 
     constructor(className: string) {
         this.className = className;
     }
 
-    include(...fileds:string[]) {
+    include(...fileds: string[]) {
         this.queryParams["include"] = fileds;
     }
     greaterThan(key: string, value: any) {
@@ -117,7 +123,7 @@ export class CloudQuery {
     }
 
     async get(id: string) {
-        const url = `https://dev.fmode.cn/parse/classes/${this.className}/${id}?`;
+        const url = serverURL + `/classes/${this.className}/${id}?`;
 
         const response = await fetch(url, {
             headers: {
@@ -138,23 +144,23 @@ export class CloudQuery {
         return null
     }
 
-    async find():Promise<Array<CloudObject>> {
-        let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
+    async find(): Promise<Array<CloudObject>> {
+        let url = serverURL + `/classes/${this.className}?`;
 
         let queryStr = ``
-        Object.keys(this.queryParams).forEach(key=>{
+        Object.keys(this.queryParams).forEach(key => {
             let paramStr = JSON.stringify(this.queryParams[key]);
-            if(key=="include"){
+            if (key == "include") {
                 paramStr = this.queryParams[key]?.join(",")
             }
-            if(queryStr) {
+            if (queryStr) {
                 url += `${key}=${paramStr}`;
-            }else{
+            } else {
                 url += `&${key}=${paramStr}`;
             }
         })
         // if (Object.keys(this.queryParams["where"]).length) {
-            
+
         // }
 
         const response = await fetch(url, {
@@ -170,17 +176,17 @@ export class CloudQuery {
 
         const json = await response?.json();
         let list = json?.results || []
-        let objList = list.map((item:any)=>this.dataToObj(item))
+        let objList = list.map((item: any) => this.dataToObj(item))
         return objList || [];
     }
 
 
     async first() {
-        let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
+        let url = serverURL + `/classes/${this.className}?`;
 
         if (Object.keys(this.queryParams["where"]).length) {
             const whereStr = JSON.stringify(this.queryParams["where"]);
-            url += `where=${whereStr}`;
+            url += `where=${whereStr}&limit=1`;
         }
 
         const response = await fetch(url, {
@@ -203,8 +209,13 @@ export class CloudQuery {
         return null
     }
 
-    dataToObj(exists:any):CloudObject{
+    dataToObj(exists: any): CloudObject {
         let existsObject = new CloudObject(this.className);
+        Object.keys(exists).forEach(key => {
+            if (exists[key]?.__type == "Object") {
+                exists[key] = this.dataToObj(exists[key])
+            }
+        })
         existsObject.set(exists);
         existsObject.id = exists.objectId;
         existsObject.createdAt = exists.createdAt;
@@ -219,7 +230,7 @@ export class CloudUser extends CloudObject {
         super("_User"); // 假设用户类在Parse中是"_User"
         // 读取用户缓存信息
         let userCacheStr = localStorage.getItem("NCloud/dev/User")
-        if(userCacheStr){
+        if (userCacheStr) {
             let userData = JSON.parse(userCacheStr)
             // 设置用户信息
             this.id = userData?.objectId;
@@ -228,7 +239,7 @@ export class CloudUser extends CloudObject {
         }
     }
 
-    sessionToken:string|null = ""
+    sessionToken: string | null = ""
     /** 获取当前用户信息 */
     async current() {
         if (!this.sessionToken) {
@@ -236,7 +247,7 @@ export class CloudUser extends CloudObject {
             return null;
         }
         return this;
-        // const response = await fetch(`https://dev.fmode.cn/parse/users/me`, {
+        // const response = await fetch(serverURL + `/users/me`, {
         //     headers: {
         //         "x-parse-application-id": "dev",
         //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
@@ -253,8 +264,8 @@ export class CloudUser extends CloudObject {
     }
 
     /** 登录 */
-    async login(username: string, password: string):Promise<CloudUser|null> {
-        const response = await fetch(`https://dev.fmode.cn/parse/login`, {
+    async login(username: string, password: string): Promise<CloudUser | null> {
+        const response = await fetch(serverURL + `/login`, {
             headers: {
                 "x-parse-application-id": "dev",
                 "Content-Type": "application/json"
@@ -268,14 +279,14 @@ export class CloudUser extends CloudObject {
             console.error(result?.error);
             return null;
         }
-        
+
         // 设置用户信息
         this.id = result?.objectId;
         this.sessionToken = result?.sessionToken;
         this.data = result; // 保存用户数据
         // 缓存用户信息
         console.log(result)
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
         return this;
     }
 
@@ -286,7 +297,7 @@ export class CloudUser extends CloudObject {
             return;
         }
 
-        const response = await fetch(`https://dev.fmode.cn/parse/logout`, {
+        const response = await fetch(serverURL + `/logout`, {
             headers: {
                 "x-parse-application-id": "dev",
                 "x-parse-session-token": this.sessionToken
@@ -298,7 +309,7 @@ export class CloudUser extends CloudObject {
 
         if (result?.error) {
             console.error(result?.error);
-            if(result?.error=="Invalid session token"){
+            if (result?.error == "Invalid session token") {
                 this.clearUserCache()
                 return true;
             }
@@ -308,10 +319,10 @@ export class CloudUser extends CloudObject {
         this.clearUserCache()
         return true;
     }
-    clearUserCache(){
+    clearUserCache() {
         // 清除用户信息
         localStorage.removeItem("NCloud/dev/User")
-        this.id = null;
+        this.id = undefined;
         this.sessionToken = null;
         this.data = {};
     }
@@ -324,7 +335,7 @@ export class CloudUser extends CloudObject {
             ...additionalData // 合并额外的用户数据
         };
 
-        const response = await fetch(`https://dev.fmode.cn/parse/users`, {
+        const response = await fetch(serverURL + `/users`, {
             headers: {
                 "x-parse-application-id": "dev",
                 "Content-Type": "application/json"
@@ -342,7 +353,7 @@ export class CloudUser extends CloudObject {
         // 设置用户信息
         // 缓存用户信息
         console.log(result)
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
         this.id = result?.objectId;
         this.sessionToken = result?.sessionToken;
         this.data = result; // 保存用户数据
@@ -351,21 +362,21 @@ export class CloudUser extends CloudObject {
 
     override async save() {
         let method = "POST";
-        let url = `https://dev.fmode.cn/parse/users`;
-    
+        let url = serverURL + `/users`;
+
         // 更新用户信息
         if (this.id) {
             url += `/${this.id}`;
             method = "PUT";
         }
-    
-        let data:any = JSON.parse(JSON.stringify(this.data))
+
+        let data: any = JSON.parse(JSON.stringify(this.data))
         delete data.createdAt
         delete data.updatedAt
         delete data.ACL
         delete data.objectId
         const body = JSON.stringify(data);
-        let headersOptions:any = {
+        let headersOptions: any = {
             "content-type": "application/json;charset=UTF-8",
             "x-parse-application-id": "dev",
             "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
@@ -377,7 +388,7 @@ export class CloudUser extends CloudObject {
             mode: "cors",
             credentials: "omit"
         });
-    
+
         const result = await response?.json();
         if (result?.error) {
             console.error(result?.error);
@@ -385,18 +396,18 @@ export class CloudUser extends CloudObject {
         if (result?.objectId) {
             this.id = result?.objectId;
         }
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(this.data))
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(this.data))
         return this;
     }
 }
 
-export class CloudApi{
-    async fetch(path:string,body:any,options?:{
-        method:string
-        body:any
-    }){
+export class CloudApi {
+    async fetch(path: string, body: any, options?: {
+        method: string
+        body: any
+    }) {
 
-        let reqOpts:any =  {
+        let reqOpts: any = {
             headers: {
                 "x-parse-application-id": "dev",
                 "Content-Type": "application/json"
@@ -405,16 +416,16 @@ export class CloudApi{
             mode: "cors",
             credentials: "omit"
         }
-        if(body||options?.body){
+        if (body || options?.body) {
             reqOpts.body = JSON.stringify(body || options?.body);
             reqOpts.json = true;
         }
         let host = `https://dev.fmode.cn`
         // host = `http://127.0.0.1:1337`
-        let url = `${host}/api/`+path
-        console.log(url,reqOpts)
-        const response = await fetch(url,reqOpts);
+        let url = `${host}/api/` + path
+        console.log(url, reqOpts)
+        const response = await fetch(url, reqOpts);
         let json = await response.json();
         return json
     }
-}
+}

+ 4 - 0
src/lib/user/README.md

@@ -0,0 +1,4 @@
+# 用户逻辑讲解
+
+# 用户登录逻辑
+- ncloud.ts > CloudUser

+ 60 - 23
src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -1,29 +1,66 @@
-<!-- 用户登录状态 -->
-<ion-card>
-  <ion-card-header>
-    <ion-card-title>
-      用户名:{{currentUser?.get("username")}}
-    </ion-card-title>
-    <ion-card-subtitle>请输入您的详细资料</ion-card-subtitle>
-   </ion-card-header>
- <ion-card-content>
+<ion-header>
+  <ion-toolbar color="primary">
+    <ion-title>编辑资料</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="cancel()">
+        <ion-icon name="close-outline" slot="icon-only"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
 
-   <ion-item>
-     <ion-input [value]="userData['realname']" (ionChange)="userDataChange('realname',$event)" label="姓名" placeholder="请您输入真实姓名"></ion-input>
-   </ion-item>
-   <ion-item>
-     <ion-input type="number" [value]="userData['age']" (ionChange)="userDataChange('age',$event)" label="年龄" placeholder="请您输入年龄"></ion-input>
+<ion-content class="ion-padding">
+  <ion-list>
+    <ion-item>
+      <ion-icon name="person-outline" slot="start"></ion-icon>
+      <ion-input [value]="userData['username']" (ionChange)="userDataChange('username',$event)" label="用户名"
+        label-placement="floating" placeholder="请输入用户名">
+      </ion-input>
     </ion-item>
-  <ion-item>
-     <ion-input [value]="userData['gender']" (ionChange)="userDataChange('gender',$event)" label="性别" placeholder="请您输入男/女"></ion-input>
+
+    <ion-item>
+      <ion-icon name="id-card-outline" slot="start"></ion-icon>
+      <ion-input [value]="userData['realname']" (ionChange)="userDataChange('realname',$event)" label="真实姓名"
+        label-placement="floating" placeholder="请输入真实姓名">
+      </ion-input>
+    </ion-item>
+
+    <ion-item>
+      <ion-icon name="calendar-outline" slot="start"></ion-icon>
+      <ion-input type="number" [value]="userData['age']" (ionChange)="userDataChange('age',$event)" label="年龄"
+        label-placement="floating" placeholder="请输入年龄">
+      </ion-input>
     </ion-item>
+
     <ion-item>
-      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$event)" label="头像" placeholder="请您输入头像地址"></ion-input>
-     </ion-item>
+      <ion-icon name="transgender-outline" slot="start"></ion-icon>
+      <ion-label>性别</ion-label>
+      <ion-segment [value]="userData['gender']" (ionChange)="userDataChange('gender', $event)">
+        <ion-segment-button value="男">
+          <ion-label>男</ion-label>
+        </ion-segment-button>
+        <ion-segment-button value="女">
+          <ion-label>女</ion-label>
+        </ion-segment-button>
+      </ion-segment>
+    </ion-item>
 
-   <ion-button expand="block" (click)="save()">保存</ion-button>
-   <ion-button expand="block" (click)="cancel()">取消</ion-button>
- 
+    <ion-item>
+      <ion-icon name="image-outline" slot="start"></ion-icon>
+      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$event)" label="头像URL"
+        label-placement="floating" placeholder="请输入头像URL地址">
+      </ion-input>
+    </ion-item>
+  </ion-list>
 
-</ion-card-content>
-</ion-card>
+  <div class="action-buttons">
+    <ion-button expand="block" (click)="save()" color="primary" shape="round">
+      <ion-icon name="save-outline" slot="start"></ion-icon>
+      保存
+    </ion-button>
+    <ion-button expand="block" (click)="cancel()" color="medium" fill="outline" shape="round">
+      <ion-icon name="close-outline" slot="start"></ion-icon>
+      取消
+    </ion-button>
+  </div>
+</ion-content>

+ 27 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.scss

@@ -0,0 +1,27 @@
+/* modal-user-edit.component.scss */
+ion-content {
+    --padding-bottom: 80px;
+}
+
+.action-buttons {
+    margin-top: 32px;
+
+    ion-button {
+        margin-bottom: 16px;
+    }
+}
+
+ion-segment {
+    width: 100%;
+    max-width: 200px;
+    margin-left: auto;
+}
+
+ion-item {
+    --padding-start: 0;
+
+    ion-icon {
+        margin-right: 16px;
+        color: var(--ion-color-medium);
+    }
+}

+ 45 - 45
src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -1,65 +1,65 @@
-import { Input, OnInit } from '@angular/core';
-import { Component } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { Component, OnInit } from '@angular/core';
+import {
+  IonHeader, IonToolbar, IonTitle, IonContent,
+  IonButton, IonInput, IonItem, IonList, IonIcon,
+  IonSegment, IonSegmentButton, IonLabel, IonButtons
+} from '@ionic/angular/standalone';
+import { ModalController } from '@ionic/angular/standalone';
 import { CloudUser } from 'src/lib/ncloud';
+import { addIcons } from 'ionicons';
+import {
+  closeOutline, saveOutline, personOutline,
+  idCardOutline, calendarOutline, transgenderOutline,
+  imageOutline
+} from 'ionicons/icons';
 
 @Component({
   selector: 'app-modal-user-edit',
   templateUrl: './modal-user-edit.component.html',
   styleUrls: ['./modal-user-edit.component.scss'],
   standalone: true,
-  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
-    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
-    IonInput,IonItem,
-    IonSegment,IonSegmentButton,IonLabel
+  imports: [
+    IonHeader, IonToolbar, IonTitle, IonContent,
+    IonButton, IonInput, IonItem, IonList, IonIcon,
+    IonSegment, IonSegmentButton, IonLabel, IonButtons
   ],
 })
-export class ModalUserEditComponent  implements OnInit {
+export class ModalUserEditComponent implements OnInit {
+  currentUser: CloudUser | undefined;
+  userData: any = {};
 
-  currentUser:CloudUser|undefined
-  userData:any = {}
-  userDataChange(key:string,ev:any){
-    let value = ev?.detail?.value
-    if(value){
-      this.userData[key] = value
-    }
-  }
-  constructor(private modalCtrl:ModalController) { 
+  constructor(private modalCtrl: ModalController) {
     this.currentUser = new CloudUser();
-    this.userData = this.currentUser.data;
+    this.userData = { ...this.currentUser.data };
+    addIcons({
+      closeOutline, saveOutline, personOutline,
+      idCardOutline, calendarOutline, transgenderOutline,
+      imageOutline
+    });
   }
 
-  ngOnInit() {}
-
-  async save(){
-    Object.keys(this.userData).forEach(key=>{
-      if(key=="age"){
-        this.userData[key] = Number(this.userData[key])
-      }
-    })
-
-    this.currentUser?.set(this.userData)
-    await this.currentUser?.save()
-    this.modalCtrl.dismiss(this.currentUser,"confirm")
+  userDataChange(key: string, ev: any) {
+    let value = ev?.detail?.value;
+    if (value) {
+      this.userData[key] = value;
+    }
   }
-  cancel(){
-    this.modalCtrl.dismiss(null,"cancel")
 
-  }
-}
+  ngOnInit() { }
 
-export async function openUserEditModal(modalCtrl:ModalController):Promise<CloudUser|null>{
-  const modal = await modalCtrl.create({
-    component: ModalUserEditComponent,
-    breakpoints:[0.7,1.0],
-    initialBreakpoint:0.7
-  });
-  modal.present();
+  async save() {
+    Object.keys(this.userData).forEach(key => {
+      if (key == "age") {
+        this.userData[key] = Number(this.userData[key]);
+      }
+    });
 
-  const { data, role } = await modal.onWillDismiss();
+    this.currentUser?.set(this.userData);
+    await this.currentUser?.save();
+    this.modalCtrl.dismiss(this.currentUser, "confirm");
+  }
 
-  if (role === 'confirm') {
-    return data;
+  cancel() {
+    this.modalCtrl.dismiss(null, "cancel");
   }
-  return null
 }

+ 62 - 0
src/lib/user/page-mine/page-mine.component.html

@@ -0,0 +1,62 @@
+<ion-content [fullscreen]="true" class="ion-padding">
+  <ion-header [translucent]="true">
+    <ion-toolbar color="primary">
+      <ion-title>我的资料</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <div class="profile-section">
+    @if(currentUser?.id){
+    <!-- 已登录状态 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-avatar class="profile-avatar">
+          <img [src]="currentUser?.get('avatar') || 'assets/icon/avatar-default.png'" alt="用户头像" />
+        </ion-avatar>
+        <ion-card-title class="ion-text-center">
+          {{currentUser?.get("username")}}
+        </ion-card-title>
+        <ion-card-subtitle class="ion-text-center">
+          {{currentUser?.get('realname') || '未设置姓名'}}
+        </ion-card-subtitle>
+      </ion-card-header>
+
+      <ion-card-content>
+        <ion-list lines="none">
+          <ion-item>
+            <ion-icon name="person-outline" slot="start"></ion-icon>
+            <ion-label>性别</ion-label>
+            <ion-note slot="end">{{currentUser?.get('gender') || '未设置'}}</ion-note>
+          </ion-item>
+          <ion-item>
+            <ion-icon name="calendar-outline" slot="start"></ion-icon>
+            <ion-label>年龄</ion-label>
+            <ion-note slot="end">{{currentUser?.get('age') || '未设置'}}</ion-note>
+          </ion-item>
+        </ion-list>
+
+        <ion-button expand="block" (click)="edit()" color="primary">
+          <ion-icon name="create-outline" slot="start"></ion-icon>
+          编辑资料
+        </ion-button>
+        <ion-button expand="block" (click)="logout()" color="danger" fill="outline">
+          <ion-icon name="log-out-outline" slot="start"></ion-icon>
+          登出
+        </ion-button>
+      </ion-card-content>
+    </ion-card>
+    }
+    @if(!currentUser?.id){
+    <!-- 未登录状态 -->
+    <div class="login-prompt">
+      <ion-icon name="person-circle-outline" class="login-icon"></ion-icon>
+      <h2>您还未登录</h2>
+      <p>登录后可以保存您的个人资料</p>
+      <ion-button expand="block" (click)="login()" color="primary">
+        <ion-icon name="log-in-outline" slot="start"></ion-icon>
+        登录用户 abc 1234
+      </ion-button>
+    </div>
+    }
+  </div>
+</ion-content>

+ 36 - 0
src/lib/user/page-mine/page-mine.component.scss

@@ -0,0 +1,36 @@
+/* page-mine.component.scss */
+.profile-section {
+    max-width: 600px;
+    margin: 0 auto;
+}
+
+.profile-avatar {
+    width: 100px;
+    height: 100px;
+    margin: 0 auto 16px;
+}
+
+.login-prompt {
+    text-align: center;
+    padding: 40px 20px;
+
+    .login-icon {
+        font-size: 80px;
+        color: var(--ion-color-medium);
+        margin-bottom: 20px;
+    }
+
+    h2 {
+        font-size: 1.5rem;
+        margin-bottom: 8px;
+    }
+
+    p {
+        color: var(--ion-color-medium);
+        margin-bottom: 30px;
+    }
+}
+
+ion-button {
+    margin-top: 16px;
+}

+ 22 - 0
src/lib/user/page-mine/page-mine.component.spec.ts

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

+ 61 - 0
src/lib/user/page-mine/page-mine.component.ts

@@ -0,0 +1,61 @@
+import { Component, OnInit } from '@angular/core';
+import { CloudUser } from 'src/lib/ncloud';
+import { ModalUserEditComponent } from '../modal-user-edit/modal-user-edit.component';
+import { ModalController } from "@ionic/angular/standalone";
+import {
+  IonContent, IonHeader, IonTitle, IonToolbar, IonCard,
+  IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent,
+  IonButton, IonAvatar, IonList, IonItem, IonLabel, IonNote, IonIcon
+} from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+import {
+  personOutline, calendarOutline, createOutline,
+  logOutOutline, logInOutline, personCircleOutline
+} from 'ionicons/icons';
+
+@Component({
+  selector: 'app-page-mine',
+  templateUrl: './page-mine.component.html',
+  styleUrls: ['./page-mine.component.scss'],
+  standalone: true,
+  imports: [
+    IonContent, IonHeader, IonTitle, IonToolbar, IonCard,
+    IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent,
+    IonButton, IonAvatar, IonList, IonItem, IonLabel, IonNote, IonIcon
+  ]
+})
+export class PageMineComponent implements OnInit {
+  currentUser: CloudUser | undefined;
+
+  constructor(private modalCtrl: ModalController) {
+    this.currentUser = new CloudUser();
+    addIcons({
+      personOutline, calendarOutline, createOutline,
+      logOutOutline, logInOutline, personCircleOutline
+    });
+  }
+
+  async edit() {
+    const modal = await this.modalCtrl.create({
+      component: ModalUserEditComponent,
+    });
+    modal.present();
+
+    const { data, role } = await modal.onWillDismiss();
+  }
+
+  async login() {
+    let user: any = new CloudUser();
+    user = await user?.login("abctest", "1234");
+    if (user?.id) {
+      this.currentUser = user;
+    }
+  }
+
+  logout() {
+    this.currentUser?.logout();
+    this.currentUser = undefined;
+  }
+
+  ngOnInit() { }
+}

+ 3 - 3
src/lib/user/token-guard/token.guard.ts

@@ -50,7 +50,7 @@ export class TokenGuard implements CanActivate {
   ) {}
 
   async canActivate(): Promise<boolean> {
-    let token = localStorage.getItem('parseSessionToken');
+    let token = localStorage.getItem('token');
 
     if (!token) {
       token = await this.showTokenPrompt();
@@ -62,11 +62,11 @@ export class TokenGuard implements CanActivate {
 
     const isValid = await this.validateToken(token);
     if (!isValid) {
-      localStorage.removeItem('parseSessionToken');
+      localStorage.removeItem('token');
       return this.canActivate();
     }
 
-    localStorage.setItem('parseSessionToken', token);
+    localStorage.setItem('token', token);
     return true;
   }
 

+ 180 - 0
src/modules/demo/bird/README.md

@@ -0,0 +1,180 @@
+
+
+# 鸟类知识科普AI应用
+- 示例代码:http://git.fmode.cn:3000/fmode/agent-app/commit/be05c8a641cea514ac0af4d179320234c820d161
+- 操作视频:
+
+会议录制:脑控未来的快速会议
+日期:2025-05-23 15:56:02
+录制文件:https://meeting.tencent.com/wework/cloud-record/share?id=e166c306-530e-44ae-b6c3-88431350fa81&hide_more_btn=true&from=qywx
+
+``` bash
+mkdir -p src/modules/demo/bird
+ng g component --standalone page-bird-list
+ng g component --standalone page-bird-display
+```
+
+- 地址
+    - tabs/demo/bird/list
+    - tabs/demo/bird/display
+``` ts
+{
+        path: 'demo/bird/list',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/bird/page-bird-list/page-bird-list.component').then((m) => m.PageBirdListComponent),
+      },
+      {
+        path: 'demo/bird/display/:birdId',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/bird/page-bird-display/page-bird-display.component').then((m) => m.PageBirdListComponent),
+      },
+```
+
+- 思考鸟类科普的列表页面,以及鸟类的范式数据
+    - 参考:http://git.fmode.cn:3000/ljf123/ai-sport/src/master/docs/schema.md
+        - 设计数据范式
+
+设定要求 您是一名专业的数据库工程师,熟悉PostgreSQL和ParseServer。 请注意表名用大驼峰,字段小驼峰。 有预留字段:objectId、updatedAt、createdAt。 关于ParseServer中数据类的描述,字段的主要类型有: String => String Number => Number Bool => bool Array => JSON Array Object => JSON Object Date => Date File => Parse.File Pointer => other Parse.Object // 指针类型的字段不需要用xxxId形式命名,直接写xxx即可。 Relation => Parse.Relation Null => null GeoPoint => {latitude: 40.0, longitude: -30.0}
+
+项目需求 鸟类专业知识科普的AI应用,鸟类表,鸟类的分类表,以及鸟类专业知识的应用经验,设计以上两张表。
+期中鸟类表尽可能有丰富的参数字段,还需要有整体的描述字段
+
+输出结果(UML类图) 请您帮我用plantuml的类图描述设计好的几张表及其关系
+
+输出结果(信息结构图) 请您帮我用markmap格式表示上面的信息结构图
+
+输出结果(SQL语句) 请您帮我用sql格式给我建表语句和测试数据插入语句,注意字段请使用小驼峰用""引起来。
+
+``` markdown
+以下是根据您的需求设计的PostgreSQL/ParseServer数据库表结构,使用PlantUML类图表示:
+
+```plantuml
+@startuml BirdDatabaseSchema
+
+class Bird {
+  + objectId: String [PK]
+  + createdAt: Date
+  + updatedAt: Date
+  
+  + commonName: String
+  + scientificName: String
+  + description: String
+  + conservationStatus: String
+  + avgWeight: Number
+  + avgLength: Number
+  + wingspan: Number
+  + lifespan: Number
+  + diet: String
+  + habitat: String
+  + migrationPattern: String
+  + breedingSeason: String
+  + clutchSize: Number
+  + incubationPeriod: Number
+  + vocalization: String
+  + identificationTips: String
+  + behaviorNotes: String
+  + distributionRange: String
+  + similarSpecies: JSON Array
+  + images: JSON Array<Parse.File>
+  + thumbnail: Parse.File
+  + location: GeoPoint
+  + taxonomy: Pointer<Taxonomy>
+  + isEndangered: bool
+  + isExtinct: bool
+  + featured: bool
+}
+
+class Taxonomy {
+  + objectId: String [PK]
+  + createdAt: Date
+  + updatedAt: Date
+  
+  + kingdom: String
+  + phylum: String
+  + class: String
+  + order: String
+  + family: String
+  + genus: String
+  + species: String
+  + subspecies: String
+  + scientificName: String
+  + description: String
+  + characteristics: JSON Array
+  + evolutionaryHistory: String
+  + relatedTaxa: JSON Array
+}
+
+class BirdExpertise {
+  + objectId: String [PK]
+  + createdAt: Date
+  + updatedAt: Date
+  
+  + title: String
+  + content: String
+  + author: String
+  + experienceLevel: String
+  + applicationScenarios: JSON Array
+  + bestPractices: String
+  + equipmentRecommendations: JSON Array
+  + seasonalityTips: String
+  + locationTips: String
+  + relatedBirds: JSON Array<Pointer<Bird>>
+  + featuredImage: Parse.File
+  + isVerified: bool
+  + references: JSON Array
+}
+
+Bird "1" -- "1" Taxonomy : belongs to >
+BirdExpertise "n" -- "n" Bird : references >
+
+note top of Bird: 鸟类主表\n包含全面的鸟类信息\n包括生态特征、形态特征\n和行为特征等丰富字段
+
+note bottom of Taxonomy: 鸟类分类表\n按照生物分类学标准\n记录鸟类的分类信息
+
+note right of BirdExpertise: 鸟类专业知识表\n记录观鸟技巧、研究经验\n和保护实践等专业知识
+
+@enduml
+```
+
+设计说明:
+1. **Bird表**:包含全面的鸟类信息,包括:
+   - 基础信息(名称、描述)
+   - 物理特征(体重、长度、翼展)
+   - 生态信息(食性、栖息地)
+   - 行为特征(迁徙模式、繁殖季节)
+   - 多媒体资料(图片、缩略图)
+   - 地理位置信息
+   - 分类关联(指向Taxonomy表)
+
+2. **Taxonomy表**:按照生物分类学标准设计,包含:
+   - 完整的分类层级(界门纲目科属种)
+   - 分类特征描述
+   - 进化历史信息
+
+3. **BirdExpertise表**:存储专业知识应用经验,包括:
+   - 专业知识内容
+   - 应用场景
+   - 最佳实践
+   - 相关鸟类关联
+   - 经验等级和验证状态
+
+关系说明:
+- 一只鸟类对应一个分类条目(1:1)
+- 一个专业知识可以关联多个鸟类,反之亦然(n:m)
+```
+
+- 根据表结构,创建测试数据,并进行导入
+
+> {{ncloud.ts复制}}{{UML描述复制}}请您根据以上三张表的内容,帮我生成大量真实鸟类的数据作为测试。帮我用CloudObject实现导入函数将三类数据的数组保存。
+
+
+- 美化每个页面
+    - 数据结构,还有CloudObject .get取属性的用法描述清晰
+    - list页面内容复制
+    - 指导AI用ionic angular 完成页面美化工作
+
+- 美化鸟类详情页
+> 以上是鸟信息结构,请帮我用ionic angular美化鸟类展示的详情页。其中CloudObject需要用.get获取属性。
+> 以下是我已经完成的基本结构

+ 181 - 0
src/modules/demo/bird/import-bird-data.ts

@@ -0,0 +1,181 @@
+import { CloudObject } from "src/lib/ncloud";
+
+// 鸟类测试数据
+const testBirds = [
+    {
+        commonName: "红腹锦鸡",
+        scientificName: "Chrysolophus pictus",
+        description: "又名金鸡,中型鸡类,体长59-110厘米。尾特长,约38-42厘米。雄鸟羽色华丽,头具金黄色丝状羽冠,上体除上背浓绿色外,其余为金黄色,后颈被有橙棕色而缀有黑边的扇状羽,形成披肩状。下体深红色,尾羽黑褐色,满缀以桂黄色斑点。",
+        conservationStatus: "无危",
+        avgWeight: 0.8,
+        avgLength: 90,
+        wingspan: 65,
+        lifespan: 10,
+        diet: "杂食性,主要以植物的叶、芽、花、果实和种子为食,也吃小麦、大豆、玉米、四季豆等农作物。此外也吃甲虫、蠕虫、双翅目和鳞翅目昆虫等动物性食物。",
+        habitat: "常栖息于海拔500-2500米的阔叶林、针阔叶混交林和林缘疏林灌丛地带",
+        migrationPattern: "留鸟",
+        breedingSeason: "4-6月",
+        clutchSize: 8,
+        incubationPeriod: 22,
+        vocalization: "繁殖期间雄鸟常发出'cha、cha'的叫声和'cha-cha-cha'的连续叫声",
+        identificationTips: "雄鸟羽色艳丽,具有金色羽冠和红色腹部;雌鸟通体棕黄,密布黑褐色虫蠹状斑",
+        behaviorNotes: "白天多活动,尤以早晨和下午活动较多,中午多在隐蔽处休息,晚上栖息于树冠隐蔽处",
+        distributionRange: "中国中部和西部山区",
+        similarSpecies: ["白腹锦鸡"],
+        isEndangered: false,
+        isExtinct: false,
+        featured: true
+    },
+    {
+        commonName: "丹顶鹤",
+        scientificName: "Grus japonensis",
+        description: "大型涉禽,体长120-160厘米。颈、脚较长,通体大多白色,头顶鲜红色,喉和颈黑色,耳至头枕白色,脚黑色,站立时颈、尾部飞羽和脚黑色,头顶红色,其余全为白色。",
+        conservationStatus: "濒危",
+        avgWeight: 7.5,
+        avgLength: 150,
+        wingspan: 220,
+        lifespan: 30,
+        diet: "杂食性,主要以鱼、虾、水生昆虫、软体动物、蝌蚪、沙蚕、蛤蜊、钉螺以及水生植物的茎、叶、块根、球茎和果实为食。",
+        habitat: "栖息于开阔平原、沼泽、湖泊、草地、海边滩涂、芦苇、沼泽以及河岸沼泽地带",
+        migrationPattern: "部分迁徙,部分留鸟",
+        breedingSeason: "4-6月",
+        clutchSize: 2,
+        incubationPeriod: 31,
+        vocalization: "高亢宏亮的鸣叫声",
+        identificationTips: "头顶红色,身体白色,颈部黑色",
+        behaviorNotes: "常成对或成家族群和小群活动。迁徙季节和冬季,常由数个或数十个家族群结成较大的群体。",
+        distributionRange: "东亚地区",
+        similarSpecies: ["白鹤", "灰鹤"],
+        isEndangered: true,
+        isExtinct: false,
+        featured: true
+    },
+    // 更多鸟类数据...
+];
+
+// 分类学测试数据
+const testTaxonomies = [
+    {
+        kingdom: "动物界",
+        phylum: "脊索动物门",
+        class: "鸟纲",
+        order: "鸡形目",
+        family: "雉科",
+        genus: "锦鸡属",
+        species: "红腹锦鸡",
+        scientificName: "Chrysolophus pictus",
+        description: "锦鸡属鸟类特征描述...",
+        characteristics: ["鲜艳羽毛", "性二态性明显", "陆禽"],
+        evolutionaryHistory: "起源于亚洲东部山区"
+    },
+    {
+        kingdom: "动物界",
+        phylum: "脊索动物门",
+        class: "鸟纲",
+        order: "鹤形目",
+        family: "鹤科",
+        genus: "鹤属",
+        species: "丹顶鹤",
+        scientificName: "Grus japonensis",
+        description: "鹤属鸟类特征描述...",
+        characteristics: ["长腿", "长颈", "大型涉禽"],
+        evolutionaryHistory: "古老物种,化石记录可追溯到中新世"
+    },
+    // 更多分类数据...
+];
+
+// 专业知识测试数据
+const testExpertise = [
+    {
+        title: "如何观察红腹锦鸡",
+        content: "观察红腹锦鸡的最佳时间是清晨和傍晚...详细观察技巧...",
+        author: "张观鸟",
+        experienceLevel: "中级",
+        applicationScenarios: ["野外考察", "生态摄影", "生物多样性调查"],
+        bestPractices: "穿着迷彩服,保持安静,使用望远镜远距离观察",
+        equipmentRecommendations: ["8-10倍双筒望远镜", "400mm以上长焦镜头", "迷彩伪装"],
+        seasonalityTips: "春季繁殖期雄鸟羽毛最艳丽,行为活跃",
+        locationTips: "四川、甘肃、陕西等地的山区阔叶林",
+        isVerified: true,
+        references: ["《中国鸟类野外手册》", "《中国雉类》"]
+    },
+    // 更多专业知识数据...
+];
+
+
+// 导入鸟类数据
+async function importBirds(birds: any[]) {
+    const results = [];
+    for (const birdData of birds) {
+        const bird = new CloudObject('Bird');
+        bird.set(birdData);
+        await bird.save();
+        results.push(bird);
+        console.log(`导入鸟类: ${bird.get('commonName')} (ID: ${bird.id})`);
+    }
+    return results;
+}
+
+// 导入分类数据
+async function importTaxonomies(taxonomies: any[]) {
+    const results = [];
+    for (const taxData of taxonomies) {
+        const tax = new CloudObject('Taxonomy');
+        tax.set(taxData);
+        await tax.save();
+        results.push(tax);
+        console.log(`导入分类: ${tax.get('species')} (ID: ${tax.id})`);
+    }
+    return results;
+}
+
+// 导入专业知识数据
+async function importExpertise(expertiseList: any[], relatedBirds: any[]) {
+    const results = [];
+    for (const expData of expertiseList) {
+        const exp = new CloudObject('BirdExpertise');
+
+        // 关联鸟类
+        if (expData.relatedBirds) {
+            expData.relatedBirds = expData.relatedBirds.map((birdName: string) => {
+                const bird = relatedBirds.find(b => b.get('commonName') === birdName);
+                return bird ? bird.toPointer() : null;
+            }).filter((b: any) => b !== null);
+        }
+
+        exp.set(expData);
+        await exp.save();
+        results.push(exp);
+        console.log(`导入专业知识: ${exp.get('title')} (ID: ${exp.id})`);
+    }
+    return results;
+}
+
+// 完整导入流程
+export async function importAllData() {
+    try {
+        console.log('开始导入分类数据...');
+        const taxonomies = await importTaxonomies(testTaxonomies);
+
+        console.log('\n开始导入鸟类数据...');
+        const birds = await importBirds(testBirds);
+
+        // 建立鸟类和分类的关联
+        for (const bird of birds) {
+            const scientificName = bird.get('scientificName');
+            const taxonomy = taxonomies.find(t => t.get('scientificName') === scientificName);
+            if (taxonomy) {
+                bird.set({ taxonomy: taxonomy.toPointer() });
+                await bird.save();
+                console.log(`关联 ${bird.get('commonName')} 与分类 ${taxonomy.get('species')}`);
+            }
+        }
+
+        console.log('\n开始导入专业知识数据...');
+        await importExpertise(testExpertise, birds);
+
+        console.log('\n所有数据导入完成!');
+    } catch (error) {
+        console.error('导入过程中出错:', error);
+    }
+}

+ 145 - 0
src/modules/demo/bird/page-bird-display/page-bird-display.component.html

@@ -0,0 +1,145 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-button text="返回" (click)="back()">返回</ion-button>
+    </ion-buttons>
+    <ion-title>{{bird?.get("commonName") || '鸟类详情'}}</ion-title>
+  </ion-toolbar>
+</ion-header>
+<ion-content class="ion-padding">
+  <div *ngIf="bird" class="bird-detail-container">
+    <!-- 头部卡片 -->
+    <ion-card class="header-card">
+      <ion-card-header>
+        <ion-card-title class="ion-text-center">
+          {{bird.get("commonName")}}
+          <small class="scientific-name">{{bird.get("scientificName")}}</small>
+        </ion-card-title>
+      </ion-card-header>
+
+      <ion-card-content>
+        <div class="conservation-status" [class.endangered]="bird.get('isEndangered')">
+          {{bird.get("conservationStatus")}}
+        </div>
+        <p class="description">{{bird.get("description")}}</p>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 基本信息网格 -->
+    <ion-grid class="info-grid">
+      <ion-row>
+        <ion-col size="6">
+          <ion-item>
+            <ion-label>平均体重</ion-label>
+            <ion-note slot="end">{{bird.get("avgWeight")}} kg</ion-note>
+          </ion-item>
+        </ion-col>
+        <ion-col size="6">
+          <ion-item>
+            <ion-label>平均体长</ion-label>
+            <ion-note slot="end">{{bird.get("avgLength")}} cm</ion-note>
+          </ion-item>
+        </ion-col>
+      </ion-row>
+      <ion-row>
+        <ion-col size="6">
+          <ion-item>
+            <ion-label>翼展</ion-label>
+            <ion-note slot="end">{{bird.get("wingspan")}} cm</ion-note>
+          </ion-item>
+        </ion-col>
+        <ion-col size="6">
+          <ion-item>
+            <ion-label>寿命</ion-label>
+            <ion-note slot="end">{{bird.get("lifespan")}} 年</ion-note>
+          </ion-item>
+        </ion-col>
+      </ion-row>
+    </ion-grid>
+
+    <!-- 栖息地与分布 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>栖息地与分布</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p><strong>栖息地:</strong> {{bird.get("habitat")}}</p>
+        <p><strong>分布范围:</strong> {{bird.get("distributionRange")}}</p>
+        <p><strong>迁徙模式:</strong> {{bird.get("migrationPattern")}}</p>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 饮食习性 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>饮食习性</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p>{{bird.get("diet")}}</p>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 繁殖信息 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>繁殖信息</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <ion-grid>
+          <ion-row>
+            <ion-col size="6">
+              <ion-item>
+                <ion-label>繁殖季节</ion-label>
+                <ion-note slot="end">{{bird.get("breedingSeason")}}</ion-note>
+              </ion-item>
+            </ion-col>
+            <ion-col size="6">
+              <ion-item>
+                <ion-label>每窝卵数</ion-label>
+                <ion-note slot="end">{{bird.get("clutchSize")}}</ion-note>
+              </ion-item>
+            </ion-col>
+          </ion-row>
+          <ion-row>
+            <ion-col>
+              <ion-item>
+                <ion-label>孵化期</ion-label>
+                <ion-note slot="end">{{bird.get("incubationPeriod")}} 天</ion-note>
+              </ion-item>
+            </ion-col>
+          </ion-row>
+        </ion-grid>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 行为特征 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>行为特征</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p><strong>叫声:</strong> {{bird.get("vocalization")}}</p>
+        <p><strong>行为:</strong> {{bird.get("behaviorNotes")}}</p>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 识别特征 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>识别特征</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p>{{bird.get("identificationTips")}}</p>
+        <p *ngIf="bird.get('similarSpecies')?.length > 0">
+          <strong>相似物种:</strong> {{bird.get("similarSpecies").join('、')}}
+        </p>
+      </ion-card-content>
+    </ion-card>
+  </div>
+
+  <!-- 加载状态 -->
+  <div *ngIf="!bird" class="loading-container">
+    <ion-spinner name="crescent"></ion-spinner>
+    <p>加载中...</p>
+  </div>
+</ion-content>

+ 89 - 0
src/modules/demo/bird/page-bird-display/page-bird-display.component.scss

@@ -0,0 +1,89 @@
+.bird-detail-container {
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.header-card {
+    margin-bottom: 20px;
+    text-align: center;
+
+    .scientific-name {
+        display: block;
+        font-style: italic;
+        font-size: 0.8em;
+        color: var(--ion-color-medium);
+    }
+}
+
+.conservation-status {
+    display: inline-block;
+    padding: 4px 8px;
+    border-radius: 12px;
+    background-color: var(--ion-color-success);
+    color: white;
+    font-size: 0.8em;
+    margin-bottom: 12px;
+
+    &.endangered {
+        background-color: var(--ion-color-danger);
+    }
+}
+
+.description {
+    text-align: justify;
+    line-height: 1.6;
+}
+
+.info-grid {
+    background: var(--ion-color-light);
+    border-radius: 12px;
+    margin-bottom: 20px;
+
+    ion-item {
+        --inner-padding-end: 0;
+        --padding-start: 0;
+    }
+}
+
+.loading-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 50vh;
+
+    ion-spinner {
+        width: 48px;
+        height: 48px;
+    }
+
+    p {
+        margin-top: 16px;
+        color: var(--ion-color-medium);
+    }
+}
+
+ion-card {
+    margin-bottom: 20px;
+    border-radius: 12px;
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+
+    ion-card-header {
+        padding-bottom: 0;
+
+        ion-card-title {
+            font-size: 1.2em;
+        }
+    }
+
+    ion-card-content {
+        p {
+            margin-bottom: 12px;
+            line-height: 1.6;
+
+            &:last-child {
+                margin-bottom: 0;
+            }
+        }
+    }
+}

+ 22 - 0
src/modules/demo/bird/page-bird-display/page-bird-display.component.spec.ts

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

+ 36 - 0
src/modules/demo/bird/page-bird-display/page-bird-display.component.ts

@@ -0,0 +1,36 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
+import { IonicModule, NavController } from '@ionic/angular';
+
+@Component({
+  selector: 'app-page-bird-display',
+  templateUrl: './page-bird-display.component.html',
+  styleUrls: ['./page-bird-display.component.scss'],
+  imports: [CommonModule, IonicModule],
+  standalone: true,
+})
+export class PageBirdDisplayComponent implements OnInit {
+  bird: CloudObject | undefined | null;
+
+  back() {
+    this.navCtrl.back()
+  }
+  constructor(
+    private route: ActivatedRoute,
+    private navCtrl: NavController
+  ) {
+    this.route.params.subscribe(params => {
+      console.log(params);
+      this.loadBird(params['birdId']);
+    });
+  }
+
+  async loadBird(birdId: string) {
+    let query = new CloudQuery("Bird");
+    this.bird = await query.get(birdId);
+  }
+
+  ngOnInit() { }
+}

+ 70 - 0
src/modules/demo/bird/page-bird-list/page-bird-list.component.html

@@ -0,0 +1,70 @@
+<ion-header>
+  <ion-toolbar color="primary">
+    <ion-title>鸟类数据库</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="importData()">
+        <ion-icon slot="icon-only" name="cloud-download"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <!-- 鸟类列表 -->
+  <ion-list>
+    <ion-list-header>
+      <ion-label>鸟类名录</ion-label>
+    </ion-list-header>
+
+    <ion-item-sliding *ngFor="let bird of birdList">
+      <ion-item (click)="goBird(bird)" detail>
+        <ion-label>
+          <h2>{{bird?.get('commonName')}}</h2>
+          <p>{{bird?.get('scientificName')}}</p>
+        </ion-label>
+      </ion-item>
+
+      <ion-item-options side="end">
+        <ion-item-option color="primary" (click)="showTaxonomy(bird)">
+          <ion-icon slot="icon-only" name="podium"></ion-icon>
+        </ion-item-option>
+      </ion-item-options>
+    </ion-item-sliding>
+  </ion-list>
+
+  <!-- 分类学信息 -->
+  <ion-card *ngIf="selectedTaxonomy">
+    <ion-card-header>
+      <ion-card-title>分类信息</ion-card-title>
+      <ion-card-subtitle>{{selectedTaxonomy?.get('species')}}</ion-card-subtitle>
+    </ion-card-header>
+
+    <ion-card-content>
+      <ion-grid>
+        <ion-row>
+          <ion-col size="6"><strong>界:</strong> {{selectedTaxonomy?.get('kingdom')}}</ion-col>
+          <ion-col size="6"><strong>门:</strong> {{selectedTaxonomy?.get('phylum')}}</ion-col>
+          <ion-col size="6"><strong>纲:</strong> {{selectedTaxonomy?.get('class')}}</ion-col>
+          <ion-col size="6"><strong>目:</strong> {{selectedTaxonomy?.get('order')}}</ion-col>
+          <ion-col size="6"><strong>科:</strong> {{selectedTaxonomy?.get('family')}}</ion-col>
+          <ion-col size="6"><strong>属:</strong> {{selectedTaxonomy?.get('genus')}}</ion-col>
+        </ion-row>
+      </ion-grid>
+    </ion-card-content>
+  </ion-card>
+
+  <!-- 专业知识 -->
+  <ion-list>
+    <ion-list-header>
+      <ion-label>观鸟专业知识</ion-label>
+    </ion-list-header>
+
+    <ion-item *ngFor="let exp of expList" detail (click)="showExpertise(exp)">
+      <ion-label>
+        <h2>{{exp?.get('title')}}</h2>
+        <p>{{exp?.get('author')}} • {{exp?.get('experienceLevel')}}</p>
+      </ion-label>
+      <ion-badge slot="end" *ngIf="exp?.get('isVerified')" color="success">已验证</ion-badge>
+    </ion-item>
+  </ion-list>
+</ion-content>

+ 42 - 0
src/modules/demo/bird/page-bird-list/page-bird-list.component.scss

@@ -0,0 +1,42 @@
+ion-content {
+    --background: #f5f5f5;
+}
+
+ion-card {
+    margin: 16px;
+    border-radius: 12px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+ion-item {
+    --border-radius: 8px;
+    margin-bottom: 8px;
+    --padding-start: 12px;
+    --padding-end: 12px;
+}
+
+ion-list {
+    background: transparent;
+}
+
+ion-list-header {
+    font-weight: bold;
+    font-size: 1.1rem;
+}
+
+.scientific-name {
+    color: var(--ion-color-medium);
+    font-style: italic;
+}
+
+ion-badge {
+    margin-left: 8px;
+}
+
+ion-grid {
+    padding: 0;
+}
+
+ion-col {
+    padding: 4px 0;
+}

+ 22 - 0
src/modules/demo/bird/page-bird-list/page-bird-list.component.spec.ts

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

+ 68 - 0
src/modules/demo/bird/page-bird-list/page-bird-list.component.ts

@@ -0,0 +1,68 @@
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
+import { NavController } from "@ionic/angular/standalone";
+import { IonHeader, IonButtons, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonButton, IonIcon, IonBadge, IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent, IonGrid, IonRow, IonCol, IonListHeader, IonItemSliding, IonItemOptions, IonItemOption } from "@ionic/angular/standalone";
+import { addIcons } from "ionicons";
+import { cloudDownload, podium } from "ionicons/icons";
+import { Component, OnInit } from '@angular/core';
+import { importAllData } from '../import-bird-data';
+import { CommonModule } from '@angular/common';
+
+
+@Component({
+  selector: 'app-page-bird-list',
+  templateUrl: './page-bird-list.component.html',
+  styleUrls: ['./page-bird-list.component.scss'],
+  standalone: true,
+  imports: [
+    CommonModule,
+    IonHeader, IonButtons, IonToolbar, IonTitle,
+    IonContent, IonList, IonItem, IonLabel, IonButton, IonIcon,
+    IonBadge, IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent, IonGrid, IonRow, IonCol, IonListHeader, IonItemSliding, IonItemOptions, IonItemOption]
+})
+export class PageBirdListComponent implements OnInit {
+  birdList: Array<CloudObject> = [];
+  expList: Array<CloudObject> = [];
+  taxList: Array<CloudObject> = [];
+  selectedTaxonomy: CloudObject | null = null;
+  selectedExpertise: CloudObject | null = null;
+
+  constructor(
+    private navCtrl: NavController
+  ) {
+    addIcons({ cloudDownload, podium });
+  }
+
+  ngOnInit() {
+    this.loadBirdData();
+  }
+
+  async loadBirdData() {
+    let query1 = new CloudQuery("Bird");
+    this.birdList = await query1.find();
+
+    let query2 = new CloudQuery("BirdExpertise");
+    this.expList = await query2.find();
+
+    let query3 = new CloudQuery("Taxonomy");
+    this.taxList = await query3.find();
+  }
+
+  goBird(bird: CloudObject) {
+    this.navCtrl.navigateForward(['tabs', 'demo', 'bird', 'display', bird?.id]);
+  }
+
+  showTaxonomy(bird: CloudObject) {
+    const species = bird.get('species');
+    this.selectedTaxonomy = this.taxList.find(tax => tax.get('species') === species) || null;
+  }
+
+  showExpertise(exp: CloudObject) {
+    this.selectedExpertise = exp;
+    // 这里可以添加显示专业知识的逻辑,比如打开模态框
+  }
+
+  importData() {
+    importAllData();
+  }
+
+}

+ 61 - 0
src/modules/demo/page-training-plan/page-training-plan.component.html

@@ -0,0 +1,61 @@
+<ion-header>
+  <ion-toolbar color="primary">
+    <ion-title>个性化训练计划生成器</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="resetForm()">
+        <ion-icon slot="icon-only" name="refresh"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <div class="form-container">
+    <ion-item>
+      <ion-textarea label="您的训练目标" labelPlacement="floating" placeholder="例如: 增肌、减脂、提高耐力等" [(ngModel)]="userGoal"
+        [disabled]="isLoading" rows="3" autoGrow="true"></ion-textarea>
+    </ion-item>
+
+    <ion-item>
+      <ion-select label="您的健身水平" [(ngModel)]="fitnessLevel" [disabled]="isLoading">
+        <ion-select-option *ngFor="let level of fitnessLevels" [value]="level.value">
+          {{level.label}}
+        </ion-select-option>
+      </ion-select>
+    </ion-item>
+
+    <ion-item>
+      <ion-range label="每周可训练天数: {{availableDays}}天" [(ngModel)]="availableDays" [disabled]="isLoading" min="1" max="7"
+        snaps="true"></ion-range>
+    </ion-item>
+
+    <ion-item>
+      <ion-range label="每次训练时间: {{availableTime}}分钟" [(ngModel)]="availableTime" [disabled]="isLoading" min="15"
+        max="120" step="5"></ion-range>
+    </ion-item>
+
+    <ion-button expand="block" color="primary" (click)="generatePlan()" [disabled]="isLoading || !userGoal.trim()">
+      <ion-spinner *ngIf="isLoading" name="lines"></ion-spinner>
+      <span *ngIf="!isLoading">生成训练计划</span>
+    </ion-button>
+  </div>
+
+  <ion-card class="ai-response-card" *ngIf="aiContent">
+    <ion-card-header>
+      <ion-card-title>
+        <ion-icon name="barbell-outline"></ion-icon>
+        您的个性化训练计划
+      </ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <div class="ai-content">
+        @if(!isPlanGenerated){
+        <div [innerHTML]="aiContent"></div>
+        }
+        @if(isPlanGenerated){
+        <fm-markdown-preview class="content-style" [content]="aiContent"></fm-markdown-preview>
+        }
+      </div>
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 23 - 0
src/modules/demo/page-training-plan/page-training-plan.component.scss

@@ -0,0 +1,23 @@
+.form-container {
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.ai-response-card {
+    margin-top: 20px;
+    background: var(--ion-color-light);
+
+    ion-card-header {
+        background: var(--ion-color-primary);
+        color: white;
+    }
+
+    .ai-content {
+        white-space: pre-line;
+        line-height: 1.6;
+    }
+}
+
+ion-spinner {
+    margin-right: 8px;
+}

+ 22 - 0
src/modules/demo/page-training-plan/page-training-plan.component.spec.ts

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

+ 95 - 0
src/modules/demo/page-training-plan/page-training-plan.component.ts

@@ -0,0 +1,95 @@
+import { Component, OnInit } from '@angular/core';
+import { IonicModule } from '@ionic/angular';
+import { FormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-page-training-plan',
+  templateUrl: './page-training-plan.component.html',
+  styleUrls: ['./page-training-plan.component.scss'],
+  standalone: true,
+  imports: [IonicModule, FormsModule, CommonModule, MarkdownPreviewModule]
+})
+export class PageTrainingPlanComponent implements OnInit {
+  // 用户输入
+  userGoal: string = '';
+  fitnessLevel: string = 'beginner';
+  availableDays: number = 3;
+  availableTime: number = 30;
+
+  // AI响应
+  aiContent: string = '请填写您的训练目标,然后点击"生成计划"按钮';
+  isLoading: boolean = false;
+  isPlanGenerated: boolean = false;
+
+  // 训练水平选项
+  fitnessLevels = [
+    { value: 'beginner', label: '初级' },
+    { value: 'intermediate', label: '中级' },
+    { value: 'advanced', label: '高级' }
+  ];
+
+  constructor() { }
+
+  ngOnInit() { }
+
+  generatePlan() {
+    if (!this.userGoal.trim()) {
+      this.aiContent = '请输入您的训练目标';
+      return;
+    }
+
+    this.isLoading = true;
+    this.aiContent = '正在为您生成个性化训练计划...';
+    this.isPlanGenerated = false;
+
+    const now = new Date();
+    const timeParams = `当前时间:${now.toLocaleString()}`;
+
+    const prompt = `你是一名专业的健身教练,请根据以下信息为用户制定个性化的训练计划:
+    
+    用户目标: ${this.userGoal}
+    健身水平: ${this.fitnessLevel}
+    每周可训练天数: ${this.availableDays}天
+    每次训练时间: ${this.availableTime}分钟
+    
+    请生成一个详细、科学的训练计划,包括:
+    1. 训练目标概述
+    2. 每周训练安排
+    3. 每次训练的具体内容(动作名称、组数、次数、休息时间)
+    4. 注意事项和建议
+    
+    使用清晰易读的格式,适当使用标题和分段。`;
+
+    const completion = new FmodeChatCompletion([
+      { role: "system", content: timeParams },
+      { role: "user", content: prompt }
+    ]);
+
+    completion.sendCompletion().subscribe({
+      next: (message: any) => {
+        this.aiContent = message.content;
+        if (message.complete) {
+          this.isLoading = false;
+          this.isPlanGenerated = true;
+        }
+      },
+      error: (err) => {
+        this.aiContent = '生成计划时出错,请重试';
+        this.isLoading = false;
+        console.error(err);
+      }
+    });
+  }
+
+  resetForm() {
+    this.userGoal = '';
+    this.fitnessLevel = 'beginner';
+    this.availableDays = 3;
+    this.availableTime = 30;
+    this.aiContent = '请填写您的训练目标,然后点击"生成计划"按钮';
+    this.isPlanGenerated = false;
+  }
+
+}

+ 852 - 0
user-server/package-lock.json

@@ -0,0 +1,852 @@
+{
+  "name": "user-server",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "user-server",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "bcrypt": "^6.0.0",
+        "body-parser": "^2.2.0",
+        "express": "^5.1.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/bcrypt": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-6.0.0.tgz",
+      "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "node-addon-api": "^8.3.0",
+        "node-gyp-build": "^4.8.4"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz",
+      "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "8.3.1",
+      "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.3.1.tgz",
+      "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.8.4",
+      "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+      "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+      "license": "MIT",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+      "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.0.tgz",
+      "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.6.3",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    }
+  }
+}

+ 17 - 0
user-server/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "user-server",
+  "version": "1.0.0",
+  "main": "server.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "node server.js"
+  },
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "bcrypt": "^6.0.0",
+    "body-parser": "^2.2.0",
+    "express": "^5.1.0"
+  }
+}

+ 227 - 0
user-server/server.js

@@ -0,0 +1,227 @@
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+const bcrypt = require('bcrypt');
+const bodyParser = require('body-parser');
+
+const app = express();
+const PORT = 3000;
+const USER_FILE = path.join(__dirname, 'user.json');
+
+// 中间件
+app.use(bodyParser.json());
+
+// 初始化用户文件(如果不存在)
+if (!fs.existsSync(USER_FILE)) {
+    fs.writeFileSync(USER_FILE, JSON.stringify([], null, 2));
+}
+
+// 读取用户数据
+function readUsers() {
+    try {
+        const data = fs.readFileSync(USER_FILE, 'utf8');
+        return JSON.parse(data);
+    } catch (err) {
+        console.error('Error reading user file:', err);
+        return [];
+    }
+}
+
+// 写入用户数据
+function writeUsers(users) {
+    try {
+        fs.writeFileSync(USER_FILE, JSON.stringify(users, null, 2));
+    } catch (err) {
+        console.error('Error writing user file:', err);
+    }
+}
+
+// 文档路由
+app.get("/device", (req, res) => {
+    /**
+     这个JSON结构包含了以下主要信息:
+
+    设备基本信息(ID、类型、制造商等)
+
+    时间戳(ISO 8601格式)
+
+    定位信息(经纬度、高度、精度、速度和方向)
+
+    电池状态(电量、电压、充电状态和剩余续航)
+
+    车辆状态(点火状态、锁定状态等)
+
+    环境数据(温度和湿度)
+
+    遥测数据(里程、总运行时间、信号强度)
+
+    附加数据(GPS定位类型、可见卫星数等)
+
+    您可以根据实际需求调整字段或添加更多信息。
+     */
+    res.json({
+        "device": {
+            "id": "EBike-123456789",
+            "type": "electric_bike",
+            "manufacturer": "ExampleTech",
+            "model": "ET-3000",
+            "firmware_version": "1.2.3"
+        },
+        "timestamp": "2023-05-16T14:30:45Z",
+        "location": {
+            "latitude": 39.9042,
+            "longitude": 116.4074,
+            "altitude": 45.2,
+            "accuracy": 5.0,
+            "speed": 25.5,
+            "heading": 180.0
+        },
+        "battery": {
+            "level": 78.5,
+            "voltage": 48.2,
+            "charging": false,
+            "remaining_range": 65.2
+        },
+        "status": {
+            "ignition": true,
+            "locked": false,
+            "alarm_triggered": false,
+            "maintenance_required": false
+        },
+        "environment": {
+            "temperature": 28.5,
+            "humidity": 45.0
+        },
+        "telemetry": {
+            "odometer": 1250.3,
+            "total_runtime": 3568,
+            "signal_strength": 85
+        },
+        "additional_data": {
+            "gps_fix_type": "3D",
+            "satellites_in_view": 12,
+            "cellular_network": "5G",
+            "timestamp_device": "2023-05-16T14:30:45Z"
+        }
+    })
+})
+// API文档路由
+app.get('/', (req, res) => {
+    const apiDocs = {
+        description: '用户认证服务API文档',
+        endpoints: [
+            {
+                method: 'POST',
+                path: '/signup',
+                description: '用户注册',
+                requestBody: {
+                    username: 'string (必需)',
+                    password: 'string (必需)'
+                },
+                responses: {
+                    '201': '用户创建成功',
+                    '400': '用户名已存在或缺少用户名/密码'
+                }
+            },
+            {
+                method: 'POST',
+                path: '/login',
+                description: '用户登录',
+                requestBody: {
+                    username: 'string (必需)',
+                    password: 'string (必需)'
+                },
+                responses: {
+                    '200': '登录成功',
+                    '400': '缺少用户名/密码',
+                    '401': '无效的用户名或密码'
+                }
+            }
+        ],
+        note: '所有密码都会经过哈希处理后再存储'
+    };
+
+    res.json(apiDocs);
+});
+
+app.get("/test", (req, res) => {
+
+    let style = `
+        h1{
+            color:red;
+        }
+    `
+    let template = `<h1>这里是测试页面</h1>`
+
+
+    res.write(`<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>TEST UTF-8编码示例</title>
+    <style>${style}</style>
+</head>
+<body>
+    ${template}
+</body>
+</html>`)
+})
+
+// 注册路由
+app.post('/signup', async (req, res) => {
+    const { username, password } = req.body;
+
+    if (!username || !password) {
+        return res.status(400).json({ error: 'Username and password are required' });
+    }
+
+    const users = readUsers();
+
+    // 检查用户是否已存在
+    if (users.some(user => user.username === username)) {
+        return res.status(400).json({ error: 'Username already exists' });
+    }
+
+    // 哈希密码
+    const hashedPassword = await bcrypt.hash(password, 10);
+
+    // 添加新用户
+    users.push({
+        username,
+        password: hashedPassword
+    });
+
+    writeUsers(users);
+
+    res.status(201).json({ message: 'User created successfully' });
+});
+
+// 登录路由
+app.post('/login', async (req, res) => {
+    const { username, password } = req.body;
+
+    if (!username || !password) {
+        return res.status(400).json({ error: 'Username and password are required' });
+    }
+
+    const users = readUsers();
+    const user = users.find(user => user.username === username);
+
+    if (!user) {
+        return res.status(401).json({ error: 'Invalid username or password' });
+    }
+
+    // 验证密码
+    const passwordMatch = await bcrypt.compare(password, user.password);
+
+    if (!passwordMatch) {
+        return res.status(401).json({ error: 'Invalid username or password' });
+    }
+
+    res.json({ message: 'Login successful' });
+});
+
+// 启动服务器
+app.listen(PORT, () => {
+    console.log(`Server is running on http://localhost:${PORT}`);
+});

+ 34 - 0
user-server/test/test-auth.js

@@ -0,0 +1,34 @@
+// 测试用户认证流程
+async function testAuthFlow() {
+    const testUsername = `testuser_${Math.floor(Math.random() * 10000)}`;
+    const testPassword = 'testpassword123';
+
+    console.log(`测试用户: ${testUsername}`);
+
+    try {
+        // 尝试登录(预期失败)
+        console.log('尝试登录未注册用户...');
+        await login(testUsername, testPassword);
+    } catch (error) {
+        console.log('预期中的登录失败:', error.message);
+    }
+
+    try {
+        // 注册新用户
+        console.log('注册新用户...');
+        await signUp(testUsername, testPassword);
+
+        // 再次尝试登录(预期成功)
+        console.log('尝试登录已注册用户...');
+        await login(testUsername, testPassword);
+
+        // 尝试重复注册(预期失败)
+        console.log('尝试重复注册...');
+        await signUp(testUsername, testPassword);
+    } catch (error) {
+        console.log('捕获到预期错误:', error.message);
+    }
+}
+
+// 运行测试
+testAuthFlow();

+ 31 - 0
user-server/test/test-login.js

@@ -0,0 +1,31 @@
+async function login(username, password) {
+    try {
+        const response = await fetch('http://localhost:3000/login', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                username,
+                password
+            })
+        });
+
+        const data = await response.json();
+
+        if (!response.ok) {
+            throw new Error(data.error || '登录失败');
+        }
+
+        console.log('登录成功:', data.message);
+        return data;
+    } catch (error) {
+        console.error('登录错误:', error.message);
+        throw error;
+    }
+}
+
+// 使用示例 - 已注册用户
+login('newuser', 'password123')
+    .then(() => console.log('登录流程完成'))
+    .catch(() => console.log('登录流程失败'));

+ 20 - 0
user-server/test/test-new-login.js

@@ -0,0 +1,20 @@
+async function signUpAndLogin(username, password) {
+    try {
+        // 1. 先注册
+        await signUp(username, password);
+
+        // 2. 注册成功后立即登录
+        const loginResult = await login(username, password);
+
+        console.log('注册并登录成功');
+        return loginResult;
+    } catch (error) {
+        console.error('注册并登录流程错误:', error.message);
+        throw error;
+    }
+}
+
+// 使用示例
+signUpAndLogin('brandnewuser', 'securepassword')
+    .then(() => console.log('注册并登录流程完成'))
+    .catch(() => console.log('注册并登录流程失败'));

+ 31 - 0
user-server/test/test-signup.js

@@ -0,0 +1,31 @@
+async function signUp(username, password) {
+    try {
+        const response = await fetch('http://localhost:3000/signup', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                username,
+                password
+            })
+        });
+
+        const data = await response.json();
+        
+        if (!response.ok) {
+            throw new Error(data.error || '注册失败');
+        }
+
+        console.log('注册成功:', data.message);
+        return data;
+    } catch (error) {
+        console.error('注册错误:', error.message);
+        throw error;
+    }
+}
+
+// 使用示例
+signUp('newuser', 'password123')
+    .then(() => console.log('注册流程完成'))
+    .catch(() => console.log('注册流程失败'));

+ 6 - 0
user-server/user.json

@@ -0,0 +1,6 @@
+[
+  {
+    "username": "newuser",
+    "password": "$2b$10$U/A47G5bMENYyZwAFja8QOirrU04jRAxbgNMEs7Gm.ZfCkp7AXKpS"
+  }
+]