Browse Source

feat:cloud all

0225304 2 days ago
parent
commit
59435f3e50
29 changed files with 1750 additions and 580 deletions
  1. 46 90
      docs/info-map.md
  2. 117 166
      docs/schema.md
  3. 0 75
      myapp/src/app/login/login.page.ts
  4. 17 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail-routing.module.ts
  5. 20 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail.module.ts
  6. 66 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.html
  7. 109 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.scss
  8. 17 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.spec.ts
  9. 199 0
      myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.ts
  10. 4 0
      myapp/src/app/tab2/tab2-routing.module.ts
  11. 55 122
      myapp/src/app/tab2/tab2.page.html
  12. 1 1
      myapp/src/app/tab2/tab2.page.scss
  13. 218 5
      myapp/src/app/tab2/tab2.page.ts
  14. 6 9
      myapp/src/app/tab2/thanks-cloud/thanks-cloud.page.html
  15. 120 112
      myapp/src/lib/import-data.ts
  16. 4 0
      myapp/src/lib/user/README.md
  17. 66 0
      myapp/src/lib/user/modal-user-edit/modal-user-edit.component.html
  18. 27 0
      myapp/src/lib/user/modal-user-edit/modal-user-edit.component.scss
  19. 22 0
      myapp/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts
  20. 65 0
      myapp/src/lib/user/modal-user-edit/modal-user-edit.component.ts
  21. 36 0
      myapp/src/lib/user/modal-user-login/modal-user-login.component.html
  22. 0 0
      myapp/src/lib/user/modal-user-login/modal-user-login.component.scss
  23. 22 0
      myapp/src/lib/user/modal-user-login/modal-user-login.component.spec.ts
  24. 92 0
      myapp/src/lib/user/modal-user-login/modal-user-login.component.ts
  25. 62 0
      myapp/src/lib/user/page-mine/page-mine.component.html
  26. 36 0
      myapp/src/lib/user/page-mine/page-mine.component.scss
  27. 22 0
      myapp/src/lib/user/page-mine/page-mine.component.spec.ts
  28. 61 0
      myapp/src/lib/user/page-mine/page-mine.component.ts
  29. 240 0
      myapp/src/lib/user/token-guard/token.guard.ts

+ 46 - 90
docs/info-map.md

@@ -1,94 +1,50 @@
-# AI日记系统数据库结构
+# AI日记应用数据库结构
 
-## _User
-- **系统字段**
-  - objectId
-  - username
-  - email
-  - emailVerified
-  - authData
-  - password
-  - createdAt
-  - updatedAt
-- **扩展字段**
-  - nickname
-  - avatar
-  - bio
-  - lastActiveAt
-  - privacySettings
+## _User (系统预留表)
+- **字段**
+  - objectId (String) [PK]
+  - username (String)
+  - email (String)
+  - authData (Object)
+  - emailVerified (bool)
+  - profilePicture (File)
+  - lastActiveAt (Date)
+  - createdAt/updatedAt (Date)
 
-## Diary
-- **基础字段**
-  - objectId
-  - createdAt
-  - updatedAt
-  - title
-  - content
-- **关联字段**
-  - author → _User
-- **功能字段**
-  - mood
-  - tags
-  - isPublic
-  - location(GeoPoint)
-  - weather
-  - aiAnalysis
+## Dynamic (动态表)
+- **字段**
+  - objectId (String) [PK]
+  - content (String)
+  - images (Array\<File\>)
+  - location (GeoPoint)
+  - mood (String)
+  - tags (Array\<String\>)
+  - isPublic (bool)
+  - likeCount/commentCount/shareCount (Number)
+  - createdAt/updatedAt (Date)
+  - author (Pointer<_User>)
+- **关系**
+  - 1:n → _User (发布关系)
+  - 1:n → Interaction (拥有关系)
 
-## ChatMessage
-- **基础字段**
-  - objectId
-  - createdAt
-  - updatedAt
-  - content
-- **关联字段**
-  - sender → _User
-  - receiver → _User
-  - diaryRef → Diary
-- **状态字段**
-  - isRead
-  - messageType
-- **媒体字段**
-  - attachments[File]
+## Interaction (互动表)
+- **字段**
+  - objectId (String) [PK]
+  - type (String) `like|comment|share`
+  - content (String)
+  - createdAt/updatedAt (Date)
+  - user (Pointer<_User>)
+  - diary (Pointer<Dynamic>)
+  - targetUser (Pointer<_User>)
+- **关系**
+  - n:1 → _User (发起用户)
+  - n:1 → Moment (所属动态)
+  - n:1 → _User (目标用户)
 
-## Moment
-- **基础字段**
-  - objectId
-  - createdAt
-  - updatedAt
-  - content
-- **关联字段**
-  - creator → _User
-  - refDiary → Diary
-- **媒体字段**
-  - images[File]
-- **地理字段**
-  - location(GeoPoint)
-- **统计字段**
-  - interactionCount
-
-## Interaction
-- **基础字段**
-  - objectId
-  - createdAt
-  - updatedAt
-  - type
-- **关联字段**
-  - fromUser → _User
-  - toUser → _User
-  - targetId(Pointer)
-- **分类字段**
-  - targetType
-- **内容字段**
-  - content
-- **状态字段**
-  - status
-
-## 关系网络
-- _User 1→n Diary
-- _User 1→n ChatMessage
-- _User 1→n Moment
-- _User 1→n Interaction
-- Diary 1→n ChatMessage
-- Diary 1→n Moment
-- Diary 1→n Interaction
-- Moment 1→n Interaction
+## 核心关系网
+```mermaid
+graph TD
+    _User -- 发布 --> Dynamic
+    _User -- 发起 --> Interaction
+    Dynamic -- 拥有 --> Interaction
+    Interaction -- 指向 --> _User(targetUser)

+ 117 - 166
docs/schema.md

@@ -29,194 +29,145 @@ AI日记的辅助AI应用,用户(User)、日记、聊天消息、动态、互
 
 # UML类图
 ```plantuml
-@startuml AI日记系统数据库设计
-
-class _User {
-  .. 系统预留字段 ..
-  + objectId: String
-  + username: String
-  + email: String
-  + emailVerified: bool
-  + authData: Object
-  + password: String
-  + createdAt: Date
-  + updatedAt: Date
-  .. 自定义字段 ..
-  + nickname: String
-  + avatar: File
-  + bio: String
-  + lastActiveAt: Date
-  + privacySettings: Object
-}
-
-class Diary {
-  + objectId: String
-  + createdAt: Date
-  + updatedAt: Date
-  + title: String
-  + content: String
-  + author: Pointer<_User>
-  + mood: String
-  + tags: Array
-  + isPublic: bool
-  + location: GeoPoint
-  + weather: Object
-  + aiAnalysis: Object
+@startuml
+' 设置主题
+skinparam class {
+    BackgroundColor White
+    ArrowColor #0078D7
+    BorderColor #0078D7
 }
 
-class ChatMessage {
-  + objectId: String
-  + createdAt: Date
-  + updatedAt: Date
-  + sender: Pointer<_User>
-  + receiver: Pointer<_User>
-  + content: String
-  + diaryRef: Pointer<Diary>
-  + isRead: bool
-  + messageType: String
-  + attachments: Array<File>
+' 用户表(使用Parse预留表)
+class _User {
+    + objectId: String [PK]
+    + username: String
+    + email: String
+    + authData: Object
+    + emailVerified: bool
+    + createdAt: Date
+    + updatedAt: Date
+    + profilePicture: File
+    + lastActiveAt: Date
 }
 
-class Moment {
-  + objectId: String
-  + createdAt: Date
-  + updatedAt: Date
-  + creator: Pointer<_User>
-  + content: String
-  + images: Array<File>
-  + refDiary: Pointer<Diary>
-  + location: GeoPoint
-  + tags: Array
-  + interactionCount: Number
+' 动态表
+class Dynamic {
+    + objectId: String [PK]
+    + content: String
+    + images: Array<File>
+    + location: GeoPoint
+    + mood: String
+    + tags: Array<String>
+    + isPublic: bool
+    + likeCount: Number
+    + commentCount: Number
+    + shareCount: Number
+    + createdAt: Date
+    + updatedAt: Date
+    + author: Pointer<_User>
 }
 
+' 互动表(包含点赞、评论、分享)
 class Interaction {
-  + objectId: String
-  + createdAt: Date
-  + updatedAt: Date
-  + type: String
-  + fromUser: Pointer<_User>
-  + toUser: Pointer<_User>
-  + targetType: String
-  + targetId: Pointer
-  + content: String
-  + status: String
+    + objectId: String [PK]
+    + type: String // 'like'|'comment'|'share'
+    + content: String // 评论内容
+    + createdAt: Date
+    + updatedAt: Date
+    + user: Pointer<_User>
+    + diary: Pointer<Dynamic>
+    + targetUser: Pointer<_User> // 互动目标用户(分享给谁/回复谁)
 }
 
 ' 关系定义
-_User "1" *-- "many" Diary : 创作
-_User "1" *-- "many" ChatMessage : 发送
-_User "1" *-- "many" Moment : 发布
-_User "1" *-- "many" Interaction : 发起
-
-Diary "1" *-- "many" ChatMessage : 关联
-Diary "1" *-- "many" Moment : 引用
-Diary "1" *-- "many" Interaction : 接收
-
-Moment "1" *-- "many" Interaction : 接收
-
+_User "1" --> "*" Dynamic : 发布
+_User "1" --> "*" Interaction : 发起
+Dynamic "1" --> "*" Interaction : 拥有
+_User "1" --> "*" Interaction : 接收互动(targetUser)
 @enduml
 ```
 # SQL语句
 
 ### 建表语句
 ```sql
-<!-- -- 注意:实际使用ParseServer时_User表已存在,此处仅为演示字段结构 -->
+-- 用户表(基于ParseServer的_User表结构扩展)
 CREATE TABLE "_User" (
-  "objectId" VARCHAR(36) PRIMARY KEY,
-  "username" VARCHAR(255) UNIQUE NOT NULL,
-  "email" VARCHAR(255) UNIQUE,
-  "emailVerified" BOOLEAN DEFAULT false,
-  "authData" JSONB,
-  "password" VARCHAR(255),
-  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "nickname" VARCHAR(100),
-  "avatar" VARCHAR(255), -- Parse.File存储路径
-  "bio" TEXT,
-  "lastActiveAt" TIMESTAMP WITH TIME ZONE,
-  "privacySettings" JSONB
-);
-
-CREATE TABLE "Diary" (
-  "objectId" VARCHAR(36) PRIMARY KEY,
-  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "title" VARCHAR(200) NOT NULL,
-  "content" TEXT NOT NULL,
-  "author" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "mood" VARCHAR(50),
-  "tags" JSONB DEFAULT '[]'::JSONB,
-  "isPublic" BOOLEAN DEFAULT false,
-  "location" JSONB, -- {latitude: xx, longitude: xx}
-  "weather" JSONB,
-  "aiAnalysis" JSONB
+    "objectId" VARCHAR(36) PRIMARY KEY,
+    "username" VARCHAR(50) UNIQUE NOT NULL,
+    "email" VARCHAR(255) UNIQUE,
+    "authData" JSONB,
+    "emailVerified" BOOLEAN DEFAULT false,
+    "profilePicture" TEXT, -- Parse.File存储的URL
+    "lastActiveAt" TIMESTAMP WITH TIME ZONE,
+    "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
 
-CREATE TABLE "ChatMessage" (
-  "objectId" VARCHAR(36) PRIMARY KEY,
-  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "sender" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "receiver" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "content" TEXT,
-  "diaryRef" VARCHAR(36) REFERENCES "Diary"("objectId"),
-  "isRead" BOOLEAN DEFAULT false,
-  "messageType" VARCHAR(20) DEFAULT 'text',
-  "attachments" JSONB DEFAULT '[]'::JSONB
-);
-
-CREATE TABLE "Moment" (
-  "objectId" VARCHAR(36) PRIMARY KEY,
-  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "creator" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "content" TEXT,
-  "images" JSONB DEFAULT '[]'::JSONB, -- Parse.File路径数组
-  "refDiary" VARCHAR(36) REFERENCES "Diary"("objectId"),
-  "location" JSONB,
-  "tags" JSONB DEFAULT '[]'::JSONB,
-  "interactionCount" INTEGER DEFAULT 0
+-- 动态表
+CREATE TABLE "Dynamic" (
+    "objectId" VARCHAR(36) PRIMARY KEY,
+    "content" TEXT NOT NULL,
+    "images" JSONB, -- 存储File URL数组
+    "location" JSONB, -- {latitude: xx, longitude: yy}
+    "mood" VARCHAR(20),
+    "tags" JSONB, -- 字符串数组
+    "isPublic" BOOLEAN DEFAULT true,
+    "likeCount" INTEGER DEFAULT 0,
+    "commentCount" INTEGER DEFAULT 0,
+    "shareCount" INTEGER DEFAULT 0,
+    "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "author" VARCHAR(36) NOT NULL REFERENCES "_User"("objectId")
 );
 
+-- 互动表
 CREATE TABLE "Interaction" (
-  "objectId" VARCHAR(36) PRIMARY KEY,
-  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-  "type" VARCHAR(20) NOT NULL, -- like/comment/share
-  "fromUser" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "toUser" VARCHAR(36) REFERENCES "_User"("objectId"),
-  "targetType" VARCHAR(20) NOT NULL, -- Diary/Moment
-  "targetId" VARCHAR(36), -- 多态关联
-  "content" TEXT,
-  "status" VARCHAR(20) DEFAULT 'unread'
+    "objectId" VARCHAR(36) PRIMARY KEY,
+    "type" VARCHAR(10) CHECK ("type" IN ('like', 'comment', 'share')),
+    "content" TEXT,
+    "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "user" VARCHAR(36) NOT NULL REFERENCES "_User"("objectId"),
+    "diary" VARCHAR(36) NOT NULL REFERENCES "Dynamic"("objectId"),
+    "targetUser" VARCHAR(36) REFERENCES "_User"("objectId")
 );
-```
 
-###  测试数据插入语句
-```sql
--- 插入测试用户
-INSERT INTO "_User" ("objectId", "username", "email", "nickname", "createdAt") VALUES
-('usr001', 'alice123', 'alice@example.com', 'Alice', NOW()),
-('usr002', 'bob456', 'bob@example.com', 'Bob', NOW());
-
--- 插入日记
-INSERT INTO "Diary" ("objectId", "title", "content", "author", "createdAt") VALUES
-('d001', '美好的一天', '今天天气晴朗...', 'usr001', NOW()),
-('d002', '项目总结', '完成了数据库设计...', 'usr002', NOW());
-
--- 插入动态
-INSERT INTO "Moment" ("objectId", "creator", "content", "refDiary", "createdAt") VALUES
-('m001', 'usr001', '分享我的日记', 'd001', NOW()),
-('m002', 'usr002', '技术分享', 'd002', NOW());
-
--- 插入互动
-INSERT INTO "Interaction" ("objectId", "type", "fromUser", "toUser", "targetType", "targetId", "content") VALUES
-('i001', 'like', 'usr002', 'usr001', 'Diary', 'd001', NULL),
-('i002', 'comment', 'usr001', 'usr002', 'Moment', 'm002', '写得真好!');
-
--- 插入聊天消息
-INSERT INTO "ChatMessage" ("objectId", "sender", "receiver", "content", "diaryRef") VALUES
-('msg001', 'usr001', 'usr002', '你好,看了你的日记', 'd002'),
-('msg002', 'usr002', 'usr001', '谢谢关注!', 'd002');
+-- 创建更新时间触发器函数
+CREATE OR REPLACE FUNCTION update_timestamp()
+RETURNS TRIGGER AS $$
+BEGIN
+    NEW."updatedAt" = CURRENT_TIMESTAMP;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- 为每张表添加更新时间触发器
+CREATE TRIGGER update_user_timestamp
+BEFORE UPDATE ON "_User"
+FOR EACH ROW EXECUTE FUNCTION update_timestamp();
+
+CREATE TRIGGER update_diary_timestamp
+BEFORE UPDATE ON "Diary"
+FOR EACH ROW EXECUTE FUNCTION update_timestamp();
+
+CREATE TRIGGER update_interaction_timestamp
+BEFORE UPDATE ON "Interaction"
+FOR EACH ROW EXECUTE FUNCTION update_timestamp();
+
+-- 测试数据插入
+INSERT INTO "_User" ("objectId", "username", "email", "emailVerified", "profilePicture", "lastActiveAt")
+VALUES 
+    ('usr001', 'testUser1', 'user1@example.com', true, 'https://example.com/pic1.jpg', NOW()),
+    ('usr002', 'testUser2', 'user2@example.com', true, 'https://example.com/pic2.jpg', NOW());
+
+INSERT INTO "Dynamic" ("objectId", "content", "images", "location", "mood", "tags", "isPublic", "author")
+VALUES
+    ('d001', '今天天气真好!', '["https://example.com/img1.jpg"]', '{"latitude": 39.9042, "longitude": 116.4074}', 'happy', '["天气","户外"]', true, 'usr001'),
+    ('d002', '学习PostgreSQL很有趣', '[]', NULL, 'excited', '["学习","数据库"]', true, 'usr002');
+
+INSERT INTO "Interaction" ("objectId", "type", "content", "user", "diary", "targetUser")
+VALUES
+    ('i001', 'like', NULL, 'usr002', 'd001', NULL),
+    ('i002', 'comment', '我也觉得天气不错!', 'usr002', 'd001', NULL),
+    ('i003', 'share', NULL, 'usr001', 'd002', 'usr002');
 ```

+ 0 - 75
myapp/src/app/login/login.page.ts

@@ -1,75 +0,0 @@
-// login.page.ts
-import { Component } from '@angular/core';
-import { AuthService } from '../services/auth.service';
-import { Router } from '@angular/router';
-import { ToastController } from '@ionic/angular';
-
-@Component({
-  selector: 'app-login',
-  templateUrl: './login.page.html',
-  styleUrls: ['./login.page.scss'],
-  standalone:false,
-})
-export class LoginPage {
-  username = '';
-  password = '';
-  isLogin = true; // 切换登录/注册模式
-  email = ''; // 注册时使用
-
-  constructor(
-    private authService: AuthService,
-    private router: Router,
-    private toastCtrl: ToastController
-  ) {}
-
-  async submit() {
-    if (this.isLogin) {
-      await this.handleLogin();
-    } else {
-      await this.handleRegister();
-    }
-  }
-
-  private async handleLogin() {
-    if (!this.username || !this.password) {
-      this.showToast('请输入用户名和密码');
-      return;
-    }
-
-    const success = await this.authService.login(this.username, this.password);
-    if (success) {
-      this.showToast('登录成功');
-      this.router.navigate(['/tabs/tab4']);
-    } else {
-      this.showToast('登录失败,请检查用户名和密码');
-    }
-  }
-
-  private async handleRegister() {
-    if (!this.username || !this.password) {
-      this.showToast('请输入用户名和密码');
-      return;
-    }
-
-    const success = await this.authService.register(this.username, this.password, this.email);
-    if (success) {
-      this.showToast('注册成功,已自动登录');
-      this.router.navigate(['/tabs/tab1']);
-    } else {
-      this.showToast('注册失败,用户名可能已被使用');
-    }
-  }
-
-  toggleMode() {
-    this.isLogin = !this.isLogin;
-  }
-
-  private async showToast(message: string) {
-    const toast = await this.toastCtrl.create({
-      message,
-      duration: 2000,
-      position: 'top'
-    });
-    await toast.present();
-  }
-}

+ 17 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail-routing.module.ts

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

+ 20 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail.module.ts

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

+ 66 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.html

@@ -0,0 +1,66 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab2"></ion-back-button>
+    </ion-buttons>
+    <ion-title>动态详情</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <div class="dynamic-container" *ngIf="dynamic">
+    <!-- 用户信息 -->
+    <div class="user-info">
+      <div class="user-avatar">{{ dynamic.get('author')?.username?.charAt(0) || 'U' }}</div>
+      <div class="user-details">
+        <div class="user-name">{{ dynamic.get('author')?.username || '匿名用户' }}</div>
+        <div class="post-time">{{ dynamic.createdAt | date:'yyyy-MM-dd HH:mm' }}</div>
+      </div>
+    </div>
+
+    <!-- 动态内容 -->
+    <div class="dynamic-content">
+      {{ dynamic.get('content') }}
+    </div>
+
+    <!-- 动态图片 -->
+    <div class="dynamic-images" *ngIf="dynamic.get('images')?.length > 0">
+      <img *ngFor="let image of dynamic.get('images')" [src]="image" alt="动态图片">
+    </div>
+
+    <!-- 互动栏 -->
+    <div class="action-bar">
+      <ion-button fill="clear" (click)="toggleLike()">
+        <ion-icon 
+          [name]="isLiked ? 'heart' : 'heart-outline'" 
+          [color]="isLiked ? 'danger' : ''">
+        </ion-icon>
+        {{ dynamic.get('likeCount') || 0 }}
+      </ion-button>
+      
+      <ion-button fill="clear" (click)="addComment()">
+        <ion-icon name="chatbubble-ellipses-outline"></ion-icon>
+        {{ dynamic.get('commentCount') || 0 }}
+      </ion-button>
+      
+      <ion-button fill="clear" (click)="shareDynamic()">
+        <ion-icon name="share-social-outline"></ion-icon>
+        {{ dynamic.get('shareCount') || 0 }}
+      </ion-button>
+    </div>
+
+    <!-- 评论列表 -->
+    <div class="comments-section">
+      <h3>评论 ({{ comments.length }})</h3>
+      
+      <div class="comment-item" *ngFor="let comment of comments">
+        <div class="comment-user">
+          <div class="user-avatar">{{ comment.get('user')?.username?.charAt(0) || 'U' }}</div>
+          <div class="user-name">{{ comment.get('user')?.username || '匿名用户' }}</div>
+          <div class="comment-time">{{ comment.createdAt | date:'yyyy-MM-dd HH:mm' }}</div>
+        </div>
+        <div class="comment-content">{{ comment.get('content') }}</div>
+      </div>
+    </div>
+  </div>
+</ion-content>

+ 109 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.scss

@@ -0,0 +1,109 @@
+.dynamic-container {
+  padding: 16px;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+  
+  .user-avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background-color: #3880ff;
+    color: white;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 12px;
+    font-weight: bold;
+  }
+  
+  .user-details {
+    flex: 1;
+    
+    .user-name {
+      font-weight: bold;
+    }
+    
+    .post-time {
+      font-size: 12px;
+      color: #666;
+    }
+  }
+}
+
+.dynamic-content {
+  margin-bottom: 16px;
+  font-size: 16px;
+  line-height: 1.5;
+}
+
+.dynamic-images {
+  margin-bottom: 16px;
+  
+  img {
+    max-width: 100%;
+    border-radius: 8px;
+    margin-bottom: 8px;
+  }
+}
+
+.action-bar {
+  display: flex;
+  justify-content: space-around;
+  padding: 8px 0;
+  border-top: 1px solid #eee;
+  border-bottom: 1px solid #eee;
+  margin-bottom: 16px;
+}
+
+.comments-section {
+  h3 {
+    font-size: 18px;
+    margin-bottom: 16px;
+  }
+}
+
+.comment-item {
+  margin-bottom: 16px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #f0f0f0;
+  
+  .comment-user {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    
+    .user-avatar {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      background-color: #3880ff;
+      color: white;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-right: 8px;
+      font-size: 14px;
+    }
+    
+    .user-name {
+      font-weight: bold;
+      font-size: 14px;
+      margin-right: 8px;
+    }
+    
+    .comment-time {
+      font-size: 12px;
+      color: #666;
+    }
+  }
+  
+  .comment-content {
+    font-size: 14px;
+    line-height: 1.4;
+    padding-left: 40px;
+  }
+}

+ 17 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.spec.ts

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

+ 199 - 0
myapp/src/app/tab2/dynamic-detail/dynamic-detail.page.ts

@@ -0,0 +1,199 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { AlertController } from '@ionic/angular';
+import { CloudUser, CloudObject, CloudQuery } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-dynamic-detail',
+  templateUrl: './dynamic-detail.page.html',
+  styleUrls: ['./dynamic-detail.page.scss'],
+  standalone:false,
+})
+export class DynamicDetailPage implements OnInit {
+  dynamic: any = null;
+  currentUser: any = null;
+  comments: any[] = [];
+  isLiked = false;
+
+  constructor(
+    private route: ActivatedRoute,
+    private alertController: AlertController
+  ) {}
+
+  async ngOnInit() {
+    const dynamicId = this.route.snapshot.paramMap.get('id');
+    //const dynamicId = this.route.snapshot.paramMap.get('id');
+    if (!dynamicId) {
+      // 处理id不存在的情况,比如重定向或显示错误
+      console.error('Dynamic ID is missing');
+      return;
+    }
+    this.currentUser = new CloudUser();
+    await this.currentUser.current();
+    
+    await this.loadDynamic(dynamicId);
+    await this.loadComments(dynamicId);
+    await this.checkUserInteraction(dynamicId);
+  }
+
+  async loadDynamic(dynamicId: string) {
+    const query = new CloudQuery("Dynamic");
+    query.include("author");
+    this.dynamic = await query.get(dynamicId);
+  }
+
+  async loadComments(dynamicId: string) {
+    const query = new CloudQuery("Interaction");
+    query.equalTo("dynamic", dynamicId);
+    query.equalTo("type", "comment");
+    query.include("user");
+    query.include("targetUser");
+    this.comments = await query.find();
+  }
+
+  async checkUserInteraction(dynamicId: string) {
+    if (!this.currentUser.id) return;
+    
+    const query = new CloudQuery("Interaction");
+    query.equalTo("user", this.currentUser.id);
+    query.equalTo("dynamic", dynamicId);
+    query.equalTo("type", "like");
+    const likeInteraction = await query.first();
+    this.isLiked = !!likeInteraction;
+  }
+
+  async toggleLike() {
+    const dynamicId = this.dynamic.id;
+    
+    if (this.isLiked) {
+      // 取消点赞
+      const query = new CloudQuery("Interaction");
+      query.equalTo("user", this.currentUser.id);
+      query.equalTo("dynamic", dynamicId);
+      query.equalTo("type", "like");
+      const likeInteraction = await query.first();
+      
+      if (likeInteraction) {
+        await likeInteraction.destroy();
+      }
+      
+      this.dynamic.set({
+        likeCount: this.dynamic.get('likeCount') - 1
+      });
+    } else {
+      // 点赞
+      const interaction = new CloudObject("Interaction");
+      interaction.set({
+        type: "like",
+        user: this.currentUser.toPointer(),
+        dynamic: this.dynamic.toPointer()
+      });
+      await interaction.save();
+      
+      this.dynamic.set({
+        likeCount: this.dynamic.get('likeCount') + 1
+      });
+    }
+    
+    await this.dynamic.save();
+    this.isLiked = !this.isLiked;
+  }
+
+  async addComment() {
+    const alert = await this.alertController.create({
+      header: '添加评论',
+      inputs: [
+        {
+          name: 'comment',
+          type: 'text',
+          placeholder: '输入评论内容'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '提交',
+          handler: async (data) => {
+            if (data.comment) {
+              const interaction = new CloudObject("Interaction");
+              interaction.set({
+                type: "comment",
+                content: data.comment,
+                user: this.currentUser.toPointer(),
+                dynamic: this.dynamic.toPointer()
+              });
+              await interaction.save();
+              
+              // 更新计数器
+              this.dynamic.set({
+                commentCount: this.dynamic.get('commentCount') + 1
+              });
+              await this.dynamic.save();
+              
+              // 刷新评论列表
+              await this.loadComments(this.dynamic.id);
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+  async shareDynamic() {
+    const alert = await this.alertController.create({
+      header: '分享动态',
+      message: '选择要分享给的用户',
+      inputs: [
+        {
+          name: 'user1',
+          type: 'radio',
+          label: '用户1',
+          value: 'user1',
+          checked: true
+        },
+        {
+          name: 'user2',
+          type: 'radio',
+          label: '用户2',
+          value: 'user2'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '分享',
+          handler: async (data) => {
+            if (data) {
+              const targetUser = new CloudObject("_User");
+              targetUser.id = data;
+              
+              const interaction = new CloudObject("Interaction");
+              interaction.set({
+                type: "share",
+                user: this.currentUser.toPointer(),
+                dynamic: this.dynamic.toPointer(),
+                targetUser: targetUser.toPointer()
+              });
+              await interaction.save();
+              
+              this.dynamic.set({
+                shareCount: this.dynamic.get('shareCount') + 1
+              });
+              await this.dynamic.save();
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+}

+ 4 - 0
myapp/src/app/tab2/tab2-routing.module.ts

@@ -10,6 +10,10 @@ const routes: Routes = [
   {
     path: 'thanks',
     loadChildren: () => import('./thanks-cloud/thanks-cloud.module').then( m => m.ThanksCloudPageModule)
+  },
+  {
+    path: 'dynamic-detail/:id',
+    loadChildren: () => import('./dynamic-detail/dynamic-detail.module').then( m => m.DynamicDetailPageModule)
   }
 ];
 

+ 55 - 122
myapp/src/app/tab2/tab2.page.html

@@ -1,130 +1,63 @@
 <ion-header [translucent]="true">
-  
-  <!-- 顶部搜索栏 -->
-    <div class="header">
-         <!-- <i class="envelope-icon ion-ios-mail"></i> -->
-         <ion-item class="search-bar">
-            <ion-icon name="search-outline"></ion-icon>
-            <ion-input class="Input with placeholder" placeholder="负面情绪禁止过夜——现在立刻销毁"></ion-input>
-        </ion-item>
-        
-        <!-- <div class="search-bar">
-            <i class="search-icon ion-ios-search"></i>
-            <div class="search-placeholder">负面情绪禁止过夜——现在立刻销毁</div>
-        </div> -->
-    </div>
-
+  <div class="header">
+    <ion-item class="search-bar">
+      <ion-icon name="search-outline"></ion-icon>
+      <ion-input class="Input with placeholder" placeholder="负面情绪禁止过夜——现在立刻销毁"></ion-input>
+    </ion-item>
+  </div>
 </ion-header>
 
 <ion-content [fullscreen]="true">
-    
-    <!-- 功能板块 -->
-    <div class="function-grid">
-        <div class="function-card card-1">树洞</div>
-        <div (click)="goThankslist('清单')" class="function-card card-2">感恩清单</div>
-        <div class="function-card card-3">漂流瓶</div>
-        <div class="function-card card-4">情绪发泄室</div>
-    </div>
-    
-    <!-- 动态日记标题栏 -->
-    <div class="tab-bar">
-        <div class="tab-item active">推荐</div>
-        <div class="tab-item">关注</div>
-        <div class="tab-item">话题</div>
-    </div>
-    
-    <!-- 动态日记列表 -->
-    <div class="diary-list">
-        <!-- 动态1 -->
-        <div class="diary-item">
-            <div class="user-info">
-                <div class="user-left">
-                    <div class="user-avatar">A</div>
-                    <div class="user-name">情绪管理师</div>
-                </div>
-                <ion-icon name="ellipsis-horizontal" class="more-icon"></ion-icon>
-                <!-- <i class="more-icon ion-ios-more"></i> -->
-            </div>
-            <div class="diary-content">
-                今天学会了用正念冥想缓解焦虑,分享给大家:找一个安静的地方,专注于呼吸,感受空气进入和离开身体的感觉...
-            </div>
-            <div class="diary-image">
-                <img src= "../../assets/images/4.jpg" alt="冥想">
-            </div>
-            <div class="action-bar">
-                <div class="action-item">
-                    <ion-icon name="heart" class="action-icon heart-icon"></ion-icon>
-                    <span class="action-count">256</span>
-                </div> 
-                <div class="action-item">
-                    <ion-icon name="chatbubble-ellipses" class="action-icon"></ion-icon>
-                    <span class="action-count">43</span>
-                </div>
-                <div class="action-item">
-                    <ion-icon name="share-social" class="action-icon"></ion-icon>
-                    <span class="action-text">5</span>
-                </div>
-            </div>
+  <!-- 功能板块 -->
+  <div class="function-grid">
+    <div (click)="importData()" class="function-card card-1">树洞</div>
+    <div (click)="goThankslist('清单')" class="function-card card-2">感恩清单</div>
+    <div class="function-card card-3">漂流瓶</div>
+    <div class="function-card card-4">情绪发泄室</div>
+  </div>
+  
+  <!-- 动态日记标题栏 -->
+  <div class="tab-bar">
+    <div class="tab-item active">推荐</div>
+    <div class="tab-item">关注</div>
+    <div class="tab-item">话题</div>
+  </div>
+  
+  <!-- 动态日记列表 -->
+  <div class="diary-list">
+    <!-- 动态项 -->
+    <div (click)="goToDynamic(dynamic.id)" class="diary-item" *ngFor="let dynamic of dynamics">
+      <div class="user-info">
+        <div class="user-left">
+          <div class="user-avatar">{{ dynamic.get('author')?.username?.charAt(0) || 'U' }}</div>
+          <div class="user-name">{{ dynamic.get('author')?.username || '匿名用户' }}</div>
         </div>
-        
-        <!-- 动态2 -->
-        <div class="diary-item">
-            <div class="user-info">
-                <div class="user-left">
-                    <div class="user-avatar">B</div>
-                    <div class="user-name">阳光小筑</div>
-                </div>
-                <ion-icon name="ellipsis-horizontal" class="more-icon"></ion-icon>
-            </div>
-            <div class="diary-content">
-                记录三件今天感恩的事:1. 早晨听到鸟叫声 2. 同事分享了好吃的饼干 3. 下班路上看到了美丽的晚霞
-            </div>
-            <div class="action-bar">
-                <div class="action-item">
-                    <ion-icon name="heart" class="action-icon heart-icon"></ion-icon>
-                    <span class="action-count">189</span>
-                </div>
-                <div class="action-item">
-                    <ion-icon name="chatbubble-ellipses" class="action-icon"></ion-icon>
-                    <span class="action-count">32</span>
-                </div>
-                <div class="action-item">
-                    <ion-icon name="share-social" class="action-icon"></ion-icon>
-                    <span class="action-text">3</span>
-                </div>
-            </div>
+        <ion-icon name="ellipsis-horizontal" class="more-icon"></ion-icon>
+      </div>
+      <div class="diary-content">
+        {{ dynamic.get('content') }}
+      </div>
+      <div class="diary-image" *ngIf="dynamic.get('images')?.length > 0">
+        <img [src]="dynamic.get('images')[0]" alt="动态图片">
+      </div>
+      <div class="action-bar">
+        <div class="action-item" (click)="toggleLike(dynamic)">
+          <ion-icon 
+            [name]="userInteractions[dynamic.id]?.liked ? 'heart' : 'heart-outline'" 
+            [color]="userInteractions[dynamic.id]?.liked ? 'danger' : ''" 
+            class="action-icon">
+          </ion-icon>
+          <span class="action-count">{{ dynamic.get('likeCount') || 0 }}</span>
+        </div> 
+        <div class="action-item" (click)="addComment(dynamic)">
+          <ion-icon name="chatbubble-ellipses-outline" class="action-icon"></ion-icon>
+          <span class="action-count">{{ dynamic.get('commentCount') || 0 }}</span>
         </div>
-        
-        <!-- 动态3 -->
-        <div class="diary-item">
-            <div class="user-info">
-                <div class="user-left">
-                    <div class="user-avatar">C</div>
-                    <div class="user-name">树洞倾听者</div>
-                </div>
-                <ion-icon name="ellipsis-horizontal" class="more-icon"></ion-icon>
-            </div>
-            <div class="diary-content">
-                今天在公园长椅上发现了一个漂流瓶,里面写着"希望找到能懂我的人"。如果你看到这条消息,请记住世界上总有人愿意倾听你的故事。
-            </div>
-            <div class="diary-image">
-                <img src="../../assets/images/5.jpg" alt="漂流瓶">
-            </div>
-            <div class="action-bar">
-                <div class="action-item">
-                    <ion-icon name="heart" class="action-icon heart-icon"></ion-icon>
-                    <span class="action-count">312</span>
-                </div>
-                <div class="action-item">
-                    <ion-icon name="chatbubble-ellipses" class="action-icon"></ion-icon>
-                    <span class="action-count">76</span>
-                </div>
-                <div class="action-item">
-                    <ion-icon name="share-social" class="action-icon"></ion-icon>
-                    <span class="action-text">2</span>
-                </div>
-            </div>
+        <div class="action-item" (click)="shareDynamic(dynamic)">
+          <ion-icon name="share-social-outline" class="action-icon"></ion-icon>
+          <span class="action-text">{{ dynamic.get('shareCount') || 0 }}</span>
         </div>
+      </div>
     </div>
-
-</ion-content>
+  </div>
+</ion-content>

+ 1 - 1
myapp/src/app/tab2/tab2.page.scss

@@ -216,4 +216,4 @@ body {
 .action-icon {
     font-size: 1.1rem;
     margin-right: 4px;
-}
+}

+ 218 - 5
myapp/src/app/tab2/tab2.page.ts

@@ -1,19 +1,232 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { NavController } from '@ionic/angular';
 import { Router } from '@angular/router'; 
-
+import { CloudUser, CloudObject, CloudQuery } from 'src/lib/ncloud';
+import { importTestData } from 'src/lib/import-data';
+import { AlertController } from '@ionic/angular';
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   standalone: false,
 })
-export class Tab2Page {
+export class Tab2Page implements OnInit {
 
+  currentUser: any = null;
+  dynamics: any[] = [];
+  userInteractions: Record<string, any> = {}; // 记录用户对每个动态的互动状态
+  
   constructor(
     private navCtrl:NavController,
-    private router: Router
-  ) {}
+    private router: Router,
+    private alertController: AlertController
+  ) {
+    //this.chaXun()
+  }
+
+  // async chaXun(){
+  //   //获取当前用户
+  //   let user:any =new CloudUser();
+  //   console.log(user)
+  //   //user = user.current();
+  //   let query =new CloudQuery("Dynamic")
+  //   query.equalTo("user",user.id)
+  //   //query.include("dynamic");
+  //   console.log(await query.find())
+  // }
+
+  async ngOnInit() {
+     await this.loadData();
+  }
+
+  goToDynamic(dynamicId:string){
+    this.navCtrl.navigateForward(["tabs","tab2","dynamic-detail",dynamicId])
+    //this.router.navigate(['/dynamic-detail', dynamicId]);
+  }
+
+  async loadData() {
+    // 获取当前用户
+    this.currentUser = new CloudUser();
+    const user = await this.currentUser.current();
+    if (!user) {
+      console.log('用户未登录');
+      return;
+    }
+
+    // 查询所有公开动态
+    const dynamicQuery = new CloudQuery("Dynamic");
+    dynamicQuery.equalTo("isPublic", true);
+    dynamicQuery.include("author");
+    this.dynamics = await dynamicQuery.find();
+
+    // 查询当前用户的互动
+    const interactionQuery = new CloudQuery("Interaction");
+    interactionQuery.equalTo("user", this.currentUser.id);
+    const interactions = await interactionQuery.find();
+
+    // 初始化互动状态
+    this.userInteractions = {};
+    interactions.forEach((interaction: any) => {
+      const dynamicId = interaction.get('dynamic')?.objectId;
+      if (dynamicId) {
+        this.userInteractions[dynamicId] = {
+          liked: interaction.get('type') === 'like',
+          commented: interaction.get('type') === 'comment',
+          shared: interaction.get('type') === 'share'
+        };
+      }
+    });
+  }
+
+  // 点赞/取消点赞
+  async toggleLike(dynamic: any) {
+    const dynamicId = dynamic.id;
+    const isLiked = this.userInteractions[dynamicId]?.liked;
+
+    const interaction = new CloudObject("Interaction");
+    
+    if (isLiked) {
+      // 取消点赞 - 删除互动记录
+      const query = new CloudQuery("Interaction");
+      query.equalTo("user", this.currentUser.id);
+      query.equalTo("dynamic", dynamicId);
+      query.equalTo("type", "like");
+      const likeInteraction = await query.first();
+      
+      if (likeInteraction) {
+        await likeInteraction.destroy();
+      }
+      
+      // 更新计数器
+      dynamic.set({
+        likeCount: dynamic.get('likeCount') - 1
+      });
+    } else {
+      // 点赞 - 创建互动记录
+      interaction.set({
+        type: "like",
+        user: this.currentUser.toPointer(),
+        dynamic: dynamic.toPointer()
+      });
+      await interaction.save();
+      
+      // 更新计数器
+      dynamic.set({
+        likeCount: dynamic.get('likeCount') + 1
+      });
+    }
+    
+    await dynamic.save();
+    await this.loadData(); // 刷新数据
+  }
+
+  // 评论
+  async addComment(dynamic: any) {
+    const alert = await this.alertController.create({
+      header: '添加评论',
+      inputs: [
+        {
+          name: 'comment',
+          type: 'text',
+          placeholder: '输入评论内容'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '提交',
+          handler: async (data) => {
+            if (data.comment) {
+              const interaction = new CloudObject("Interaction");
+              interaction.set({
+                type: "comment",
+                content: data.comment,
+                user: this.currentUser.toPointer(),
+                dynamic: dynamic.toPointer()
+              });
+              await interaction.save();
+              
+              // 更新计数器
+              dynamic.set({
+                commentCount: (dynamic.get('commentCount') + 1)
+              });
+              await dynamic.save();
+              
+              await this.loadData(); // 刷新数据
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+  // 分享
+  async shareDynamic(dynamic: any) {
+    const alert = await this.alertController.create({
+      header: '分享动态',
+      message: '选择要分享给的用户',
+      inputs: [
+        {
+          name: 'user1',
+          type: 'radio',
+          label: '用户1',
+          value: 'user1',
+          checked: true
+        },
+        {
+          name: 'user2',
+          type: 'radio',
+          label: '用户2',
+          value: 'user2'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '分享',
+          handler: async (data) => {
+            if (data) {
+              // 这里简化处理,实际应该查询目标用户
+              const targetUser = new CloudObject("_User");
+              targetUser.id = data; // 实际应该使用用户ID
+              
+              const interaction = new CloudObject("Interaction");
+              interaction.set({
+                type: "share",
+                user: this.currentUser.toPointer(),
+                dynamic: dynamic.toPointer(),
+                targetUser: targetUser.toPointer()
+              });
+              await interaction.save();
+              
+              // 更新计数器
+              dynamic.set({
+                shareCount: (dynamic.get('shareCount') + 1)
+              });
+              await dynamic.save();
+              
+              await this.loadData(); // 刷新数据
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+
+  importData(){
+    importTestData();
+  }
 
   goThankslist(thanks?:string){
     this.navCtrl.navigateForward(["tabs","tab2","thanks"],{

+ 6 - 9
myapp/src/app/tab2/thanks-cloud/thanks-cloud.page.html

@@ -4,10 +4,11 @@
       <ion-back-button defaultHref="/tabs/tab2"></ion-back-button>
     </ion-buttons>
     <ion-title>感恩清单</ion-title>
-    <div class="empty-state">
-      <!-- <ion-icon name="cloud-outline"></ion-icon> -->
-      <ion-button (click)="addNewThanks()">添加记录</ion-button>
-    </div>
+    <ion-buttons>
+    <ion-button (click)="importThanks()">
+        <ion-icon name="cloud-outline"></ion-icon>
+    </ion-button>
+    </ion-buttons>
   </ion-toolbar>
   
 </ion-header>
@@ -73,9 +74,5 @@
         </ion-accordion-group>
       </ion-card-content>
     </ion-card>
-  </div>
-  
-  <button class="import-btn" (click)="importThanks()">
-    导入数据
-  </button>
+  </div> 
 </ion-content>

+ 120 - 112
myapp/src/lib/import-data.ts

@@ -1,127 +1,135 @@
-import { CloudUser, CloudObject } from './ncloud';
+// testDataImporter.ts
+import { CloudObject, CloudUser } from './ncloud';
 
-// 测试数据导入函数
-export async function importTestDiaries() {
-    try {
-        // 1. 首先登录一个测试用户(或创建)
+// 创建测试用户
+async function createTestUsers(): Promise<CloudUser[]> {
+    const users = [
+        { username: 'user1', password: '123456', email: 'user1@example.com' },
+        { username: 'user2', password: '123456', email: 'user2@example.com' },
+        { username: 'user3', password: '123456', email: 'user3@example.com' }
+    ];
+
+    const createdUsers: CloudUser[] = [];
+    for (const userData of users) {
         const user = new CloudUser();
-        let currentUser = await user.login('testuser', 'test123');
-        
-        if (!currentUser) {
-            // 如果用户不存在,先注册
-            console.log('测试用户不存在,正在创建...');
-            currentUser = await user.signUp('testuser', 'test123', {
-                nickname: '测试用户',
-                email: 'test@example.com',
-                bio: '这是一个用于测试的AI日记系统用户'
+        const existing = await user.login(userData.username, userData.password);
+        if (!existing) {
+            await user.signUp(userData.username, userData.password, {
+                email: userData.email,
+                profilePicture: `https://randomuser.me/api/portraits/${Math.random() > 0.5 ? 'men' : 'women'}/${Math.floor(Math.random() * 50)}.jpg`
             });
         }
+        createdUsers.push(user);
+        console.log(`User created: ${user.id}`);
+    }
+    return createdUsers;
+}
 
-        if (!currentUser) {
-            console.error('无法登录或创建测试用户');
-            return;
+// 创建测试动态(表名改为Dynamic)
+async function createTestDynamics(users: CloudUser[]): Promise<CloudObject[]> {
+    const dynamics = [
+        {
+            content: "今天天气真好,去公园散步了!🌞",
+            images: ["https://example.com/park1.jpg", "https://example.com/park2.jpg"],
+            location: { latitude: 39.9042, longitude: 116.4074 },
+            mood: "happy",
+            tags: ["户外", "休闲"]
+        },
+        {
+            content: "刚学会用PostgreSQL,太强大了!💪",
+            images: [],
+            mood: "excited",
+            tags: ["学习", "编程"]
+        },
+        {
+            content: "分享一首好听的歌:https://music.163.com/song?id=123456",
+            images: ["https://example.com/music_cover.jpg"],
+            mood: "relaxed",
+            tags: ["音乐", "分享"]
         }
+    ];
 
-        console.log('已登录用户:', currentUser.data['username']);
+    const createdDynamics: CloudObject[] = [];
+    for (const dynamicData of dynamics) {
+        const dynamic = new CloudObject("Dynamic");  // 修改为Dynamic
+        dynamic.set({
+            ...dynamicData,
+            isPublic: true,
+            author: users[Math.floor(Math.random() * users.length)].toPointer()
+        });
+        await dynamic.save();
+        createdDynamics.push(dynamic);
+        console.log(`Dynamic created: ${dynamic.id}`);
+    }
+    return createdDynamics;
+}
 
-        // 2. 创建测试日记数据
-        const testDiaries = [
-            {
-                title: '美好的一天',
-                content: '今天天气晴朗,我去了公园散步,看到了许多美丽的花朵。心情非常愉快!',
-                mood: 'happy',
-                tags: ['公园', '散步', '花朵'],
-                isPublic: true,
-                location: {
-                    latitude: 39.9042,
-                    longitude: 116.4074
-                },
-                weather: {
-                    condition: '晴朗',
-                    temperature: 22
-                },
-                aiAnalysis: {
-                    sentiment: 'positive',
-                    keywords: ['公园', '散步', '花朵', '愉快']
-                }
-            },
-            {
-                title: '工作压力',
-                content: '今天项目截止日期临近,感觉压力很大。需要更好的时间管理方法。',
-                mood: 'stressed',
-                tags: ['工作', '压力', '时间管理'],
-                isPublic: false,
-                aiAnalysis: {
-                    sentiment: 'negative',
-                    keywords: ['项目', '截止日期', '压力', '时间管理']
-                }
-            },
-            {
-                title: '读书笔记',
-                content: '今天读了《人类简史》,对人类文明的发展有了新的认识。特别有趣的是认知革命的部分。',
-                mood: 'thoughtful',
-                tags: ['读书', '人类简史', '学习'],
-                isPublic: true,
-                aiAnalysis: {
-                    sentiment: 'neutral',
-                    keywords: ['读书', '人类简史', '认知革命', '学习']
-                }
-            },
-            {
-                title: '家庭聚餐',
-                content: '周末和家人一起聚餐,妈妈做了我最爱吃的红烧肉。大家聊得很开心,感受到了家庭的温暖。',
-                mood: 'joyful',
-                tags: ['家庭', '聚餐', '美食'],
-                isPublic: true,
-                location: {
-                    latitude: 39.9138,
-                    longitude: 116.3637
-                },
-                weather: {
-                    condition: '多云',
-                    temperature: 18
-                },
-                aiAnalysis: {
-                    sentiment: 'positive',
-                    keywords: ['家庭', '聚餐', '红烧肉', '温暖']
-                }
-            },
-            {
-                title: '健身计划',
-                content: '开始了新的健身计划,今天完成了30分钟的有氧运动。虽然很累,但感觉很充实。',
-                mood: 'energetic',
-                tags: ['健身', '运动', '健康'],
-                isPublic: false,
-                aiAnalysis: {
-                    sentiment: 'positive',
-                    keywords: ['健身计划', '有氧运动', '充实']
-                }
-            }
-        ];
+// 创建测试互动(关联Dynamic表)
+async function createTestInteractions(users: CloudUser[], dynamics: CloudObject[]) {
+    const interactionTypes = ['like', 'comment', 'share'];
+    const comments = [
+        "太棒了!",
+        "我也想去!",
+        "感谢分享!",
+        "有意思 😄",
+        "学到了新知识"
+    ];
 
-        // 3. 批量创建日记
-        console.log('开始导入测试日记...');
-        for (const diaryData of testDiaries) {
-            const diary = new CloudObject('Diary');
+    // 每个动态生成3-5个互动
+    for (const dynamic of dynamics) {
+        const interactionCount = 3 + Math.floor(Math.random() * 3);
+        
+        for (let i = 0; i < interactionCount; i++) {
+            const interaction = new CloudObject("Interaction");
+            const user = users[Math.floor(Math.random() * users.length)];
+            const type = interactionTypes[Math.floor(Math.random() * interactionTypes.length)];
             
-            // 设置日记数据
-            diary.set({
-                ...diaryData,
-                author: currentUser.toPointer(),
-                createdAt: new Date(),
-                updatedAt: new Date()
-            });
+            const interactionData: any = {
+                type,
+                user: user.toPointer(),
+                dynamic: dynamic.toPointer()  // 字段名改为dynamic
+            };
 
-            // 保存日记
-            await diary.save();
-            console.log(`已创建日记: ${diaryData.title} (ID: ${diary.id})`);
-        }
+            if (type === 'comment') {
+                interactionData.content = comments[Math.floor(Math.random() * comments.length)];
+            } else if (type === 'share') {
+                interactionData.targetUser = users.find(u => u.id !== user.id)?.toPointer();
+            }
 
-        console.log('测试数据导入完成!');
-    } catch (error) {
-        console.error('导入测试数据时出错:', error);
+            interaction.set(interactionData);
+            await interaction.save();
+            console.log(`Interaction created: ${interaction.id} (${type})`);
+
+            // 更新动态的计数器
+            const dynamicUpdate = new CloudObject("Dynamic");  // 修改为Dynamic
+            dynamicUpdate.id = dynamic.id;
+            dynamicUpdate.set({
+                [`${type}Count`]: { __op: "Increment", amount: 1 }
+            });
+            await dynamicUpdate.save();
+        }
     }
 }
 
-// 执行导入
-//importTestDiaries();
+// 主执行函数
+export async function importTestData() {
+    try {
+        console.log("Starting test data import...");
+        
+        // 1. 创建测试用户
+        console.log("Creating test users...");
+        const users = await createTestUsers();
+        
+        // 2. 创建测试动态
+        console.log("Creating test dynamics...");
+        const dynamics = await createTestDynamics(users);
+        
+        // 3. 创建互动数据
+        console.log("Creating test interactions...");
+        await createTestInteractions(users, dynamics);
+        
+        console.log("Test data import completed successfully!");
+    } catch (error) {
+        console.error("Error during test data import:", error);
+    }
+} 

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

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

+ 66 - 0
myapp/src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,66 @@
+<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-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-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-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-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>
+
+  <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
myapp/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);
+    }
+}

+ 22 - 0
myapp/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts

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

+ 65 - 0
myapp/src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -0,0 +1,65 @@
+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,
+    IonButton, IonInput, IonItem, IonList, IonIcon,
+    IonSegment, IonSegmentButton, IonLabel, IonButtons
+  ],
+})
+export class ModalUserEditComponent implements OnInit {
+  currentUser: CloudUser | undefined;
+  userData: any = {};
+
+  constructor(private modalCtrl: ModalController) {
+    this.currentUser = new CloudUser();
+    this.userData = { ...this.currentUser.data };
+    addIcons({
+      closeOutline, saveOutline, personOutline,
+      idCardOutline, calendarOutline, transgenderOutline,
+      imageOutline
+    });
+  }
+
+  userDataChange(key: string, ev: any) {
+    let value = ev?.detail?.value;
+    if (value) {
+      this.userData[key] = value;
+    }
+  }
+
+  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");
+  }
+
+  cancel() {
+    this.modalCtrl.dismiss(null, "cancel");
+  }
+}

+ 36 - 0
myapp/src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,36 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      <ion-segment [value]="type" (ionChange)="typeChange($event)">
+        <ion-segment-button value="login">
+          <ion-label>登录</ion-label>
+        </ion-segment-button>
+        <ion-segment-button value="signup">
+          <ion-label>注册</ion-label>
+        </ion-segment-button>
+      </ion-segment>
+    </ion-card-title>
+    <ion-card-subtitle>请输入账号密码</ion-card-subtitle>
+  </ion-card-header>
+  <ion-card-content>
+    <ion-item>
+      <ion-input [value]="username" (ionChange)="usernameChange($event)" label="账号" placeholder="请您输入账号/手机号"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="password" (ionChange)="passwordChange($event)" label="密码" type="password" value="password"></ion-input>
+    </ion-item>
+
+    @if(type=="signup"){
+      <ion-item>
+        <ion-input [value]="password2" (ionChange)="password2Change($event)" label="密码二次" type="password" value="password"></ion-input>
+      </ion-item>
+    }
+    @if(type=="login"){
+      <ion-button expand="block" (click)="login()">登录</ion-button>
+    }
+    @if(type=="signup"){
+      <ion-button expand="block" (click)="signup()">注册</ion-button>
+    }
+  </ion-card-content>
+</ion-card>

+ 0 - 0
myapp/src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
myapp/src/lib/user/modal-user-login/modal-user-login.component.spec.ts

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

+ 92 - 0
myapp/src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -0,0 +1,92 @@
+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 { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-login',
+  templateUrl: './modal-user-login.component.html',
+  styleUrls: ['./modal-user-login.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserLoginComponent  implements OnInit {
+  @Input()
+  type:"login"|"signup" = "login"
+  typeChange(ev:any){
+    this.type = ev?.detail?.value || ev?.value || 'login'
+  }
+  username:string = ""
+  usernameChange(ev:any){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) { }
+
+  ngOnInit() {}
+
+  async login(){
+    if(!this.username || !this.password){
+      console.log("请输入完整")
+      return
+    }
+    let user:any = new CloudUser();
+    user = await user.login(this.username,this.password);
+    if(user?.id){
+       this.modalCtrl.dismiss(user,"confirm")
+    }else{
+      console.log("登录失败")
+    }
+  }
+
+  async signup(){
+    if(!this.username || !this.password || !this.password2){
+      console.log("请输入完整")
+      return
+    }
+    if(this.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 62 - 0
myapp/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
myapp/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
myapp/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
myapp/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() { }
+}

+ 240 - 0
myapp/src/lib/user/token-guard/token.guard.ts

@@ -0,0 +1,240 @@
+/**
+@desc
+请您帮我设计一个ionic/angular项目的TokenGuard路由守卫,检查localStorage是否有token值。若不存在,通过dom构建ui交互提示用户填写token(不使用modal和angular逻辑)。若存在token或填写后,则调用接口:
+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
+  https://YOUR.PARSE-SERVER.HERE/parse/users/me 
+可用fetch请求验证token是否正常。
+若不正常,提示错误请重新填写直到填写了有效token。
+若正常则返回true。
+接口地址为https://server.fmode.cn/parse 应用id为 ncloudmaster。
+
+@example 路由守卫使用
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { TokenGuard } from './guards/token.guard';
+
+const routes: Routes = [
+  {
+    path: 'protected',
+    loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedPageModule),
+    canActivate: [TokenGuard]
+  },
+  // 其他路由...
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule]
+})
+export class AppRoutingModule {}
+
+ */
+
+// src/app/guards/token.guard.ts
+import { Injectable } from '@angular/core';
+import { CanActivate, Router } from '@angular/router';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TokenGuard implements CanActivate {
+  private readonly PARSE_SERVER_URL = 'https://server.fmode.cn/parse';
+  private readonly APPLICATION_ID = 'ncloudmaster';
+
+  constructor(
+    private http: HttpClient,
+    private router: Router
+  ) {}
+
+  async canActivate(): Promise<boolean> {
+    let token = localStorage.getItem('token');
+
+    if (!token) {
+      token = await this.showTokenPrompt();
+      if (!token) {
+        this.router.navigate(['/login']);
+        return false;
+      }
+    }
+
+    const isValid = await this.validateToken(token);
+    if (!isValid) {
+      localStorage.removeItem('token');
+      return this.canActivate();
+    }
+
+    localStorage.setItem('token', token);
+    return true;
+  }
+
+  private async validateToken(token: string): Promise<boolean> {
+    const headers = new HttpHeaders({
+      'X-Parse-Application-Id': this.APPLICATION_ID,
+      'X-Parse-Session-Token': token
+    });
+
+    try {
+      const response: any = await this.http.get(
+        `${this.PARSE_SERVER_URL}/users/me`,
+        { headers }
+      ).toPromise();
+      return !!response?.objectId;
+    } catch (error) {
+      return false;
+    }
+  }
+
+  private showTokenPrompt(): Promise<string | null> {
+    return new Promise((resolve) => {
+      // 创建遮罩层
+      const overlay = document.createElement('div');
+      overlay.style.position = 'fixed';
+      overlay.style.top = '0';
+      overlay.style.left = '0';
+      overlay.style.width = '100%';
+      overlay.style.height = '100%';
+      overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
+      overlay.style.display = 'flex';
+      overlay.style.justifyContent = 'center';
+      overlay.style.alignItems = 'center';
+      overlay.style.zIndex = '1000';
+
+      // 创建对话框
+      const dialog = document.createElement('div');
+      dialog.style.backgroundColor = 'white';
+      dialog.style.padding = '20px';
+      dialog.style.borderRadius = '8px';
+      dialog.style.width = '80%';
+      dialog.style.maxWidth = '400px';
+
+      // 创建标题
+      const title = document.createElement('h3');
+      title.textContent = '请输入 Token';
+      title.style.marginTop = '0';
+      title.style.color = "black";
+      dialog.appendChild(title);
+
+      // 创建描述
+      // 使用以下指令获取token
+      
+      const descEl:HTMLElement = document.createElement("div");
+      descEl.innerHTML = `获取token方法:<br>
+      1.登录<a href="https://ai.fmode.cn" target="_blank">https://ai.fmode.cn</a><br>
+      2.按F12进入调试——打开控制台Console<br>
+      3.输入指令:<br>
+      <span style="color:blue;">JSON.parse(localStorage.getItem("Parse/ncloudmaster/currentUser"))?.sessionToken</span><br>
+      4.复制字符串内容,形如:<br>
+      <span style="color:red">r:xxxxxxxxxxxxx</span>
+      `
+      descEl.style.color = "black"
+      dialog.appendChild(descEl);
+
+      // 创建错误消息容器
+      const errorMsg = document.createElement('div');
+      errorMsg.style.color = 'red';
+      errorMsg.style.minHeight = '20px';
+      errorMsg.style.margin = '10px 0';
+      dialog.appendChild(errorMsg);
+
+      // 创建输入框
+      const input = document.createElement('input');
+      input.type = 'text';
+      input.placeholder = '请输入您的 Token';
+      input.style.width = '100%';
+      input.style.padding = '10px';
+      input.style.marginBottom = '15px';
+      input.style.boxSizing = 'border-box';
+      dialog.appendChild(input);
+
+      // 创建按钮容器
+      const buttonContainer = document.createElement('div');
+      buttonContainer.style.display = 'flex';
+      buttonContainer.style.justifyContent = 'flex-end';
+      buttonContainer.style.gap = '10px';
+
+      // 创建提交按钮
+      const submitBtn = document.createElement('button');
+      submitBtn.textContent = '提交';
+      submitBtn.style.padding = '8px 16px';
+      submitBtn.style.backgroundColor = '#3880ff';
+      submitBtn.style.color = 'white';
+      submitBtn.style.border = 'none';
+      submitBtn.style.borderRadius = '4px';
+      submitBtn.disabled = true;
+
+      // 创建取消按钮
+      const cancelBtn = document.createElement('button');
+      cancelBtn.textContent = '取消';
+      cancelBtn.style.padding = '8px 16px';
+      cancelBtn.style.backgroundColor = '#eb445a';
+      cancelBtn.style.color = 'white';
+      cancelBtn.style.border = 'none';
+      cancelBtn.style.borderRadius = '4px';
+
+      // 添加按钮到容器
+      buttonContainer.appendChild(cancelBtn);
+      buttonContainer.appendChild(submitBtn);
+      dialog.appendChild(buttonContainer);
+
+      // 添加到遮罩层
+      overlay.appendChild(dialog);
+      document.body.appendChild(overlay);
+
+      // 自动聚焦输入框
+      input.focus();
+
+      // 输入验证
+      input.addEventListener('input', () => {
+        submitBtn.disabled = !input.value.trim();
+      });
+
+      // 取消按钮事件
+      const cleanup = () => {
+        document.body.removeChild(overlay);
+      };
+
+      cancelBtn.addEventListener('click', () => {
+        cleanup();
+        resolve(null);
+      });
+
+      // 提交按钮事件
+      submitBtn.addEventListener('click', async () => {
+        const token = input.value.trim();
+        if (!token) return;
+
+        const isValid = await this.validateToken(token);
+        if (isValid) {
+          cleanup();
+          resolve(token);
+        } else {
+          errorMsg.textContent = 'Token 无效,请重新输入';
+          input.value = '';
+          submitBtn.disabled = true;
+          input.focus();
+        }
+      });
+
+      // 回车键提交
+      input.addEventListener('keypress', async (e) => {
+        if (e.key === 'Enter' && input.value.trim()) {
+          const token = input.value.trim();
+          const isValid = await this.validateToken(token);
+          
+          if (isValid) {
+            cleanup();
+            resolve(token);
+          } else {
+            errorMsg.textContent = 'Token 无效,请重新输入';
+            input.value = '';
+            submitBtn.disabled = true;
+            input.focus();
+          }
+        }
+      });
+    });
+  }
+}