Browse Source

修改信息

15270821319 6 months ago
parent
commit
73bdf8a4a7

+ 98 - 0
AiStudy-app/src/app/pages/profile/profile.page.html

@@ -0,0 +1,98 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab3"></ion-back-button>
+    </ion-buttons>
+    <ion-title>个人资料</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="saveProfile()">
+        保存
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <div class="avatar-section">
+    <ion-avatar class="large-avatar" (click)="changeAvatar()">
+      <img [src]="userProfile.avatar" alt="头像">
+      <div class="avatar-overlay">
+        <ion-icon name="camera-outline"></ion-icon>
+      </div>
+    </ion-avatar>
+    <p class="change-avatar-text">点击更换头像</p>
+  </div>
+
+  <ion-list>
+    <ion-item>
+      <ion-label position="stacked">用户名</ion-label>
+      <ion-input 
+        [(ngModel)]="userProfile.username"
+        placeholder="请输入用户名"
+        minlength="3">
+      </ion-input>
+    </ion-item>
+
+    <ion-item>
+      <ion-label position="stacked">邮箱</ion-label>
+      <ion-input 
+        [(ngModel)]="userProfile.email"
+        type="email"
+        placeholder="请输入邮箱">
+      </ion-input>
+    </ion-item>
+
+    <ion-item>
+      <ion-label position="stacked">个人简介</ion-label>
+      <ion-input 
+        [(ngModel)]="userProfile.bio"
+        placeholder="介绍一下自己吧"
+        maxlength="100">
+      </ion-input>
+    </ion-item>
+
+    <ion-item>
+      <ion-label position="stacked">等级</ion-label>
+      <ion-input 
+        [value]="userProfile.level"
+        disabled>
+      </ion-input>
+    </ion-item>
+
+    <ion-item>
+      <ion-label position="stacked">性别</ion-label>
+      <ion-select 
+        [(ngModel)]="userProfile.gender"
+        placeholder="请选择性别"
+        interface="action-sheet"
+        [interfaceOptions]="{ header: '选择性别' }">
+        <ion-select-option 
+          *ngFor="let option of genderOptions" 
+          [value]="option.value">
+          {{ option.label }}
+        </ion-select-option>
+      </ion-select>
+    </ion-item>
+
+    <ion-item>
+      <ion-label position="stacked">年龄</ion-label>
+      <ion-select 
+        [(ngModel)]="userProfile.age"
+        placeholder="请选择年龄"
+        interface="action-sheet"
+        [interfaceOptions]="{ header: '选择年龄' }">
+        <ion-select-option 
+          *ngFor="let option of ageOptions" 
+          [value]="option.value">
+          {{ option.label }}
+        </ion-select-option>
+      </ion-select>
+    </ion-item>
+
+    <ion-item lines="none" class="password-change-item">
+      <ion-button expand="block" (click)="changePassword()" color="primary" fill="outline">
+        修改密码
+      </ion-button>
+    </ion-item>
+  </ion-list>
+</ion-content> 

+ 106 - 0
AiStudy-app/src/app/pages/profile/profile.page.scss

@@ -0,0 +1,106 @@
+.avatar-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24px 0;
+
+  .large-avatar {
+    width: 120px;
+    height: 120px;
+    position: relative;
+    margin-bottom: 12px;
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    .avatar-overlay {
+      position: absolute;
+      bottom: 0;
+      right: 0;
+      background: var(--ion-color-primary);
+      border-radius: 50%;
+      width: 32px;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      ion-icon {
+        color: white;
+        font-size: 18px;
+      }
+    }
+  }
+
+  .change-avatar-text {
+    color: var(--ion-color-medium);
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+ion-list {
+  background: transparent;
+
+  ion-item {
+    --padding-start: 0;
+    --background: transparent;
+    margin-bottom: 16px;
+
+    ion-label {
+      color: var(--ion-color-medium);
+      font-size: 14px;
+      margin-bottom: 8px;
+    }
+
+    ion-input {
+      --background: #f8f9fa;
+      --padding-start: 16px;
+      --padding-end: 16px;
+      --padding-top: 12px;
+      --padding-bottom: 12px;
+      --border-radius: 8px;
+      --placeholder-color: #a0aec0;
+      margin-top: 4px;
+      
+      &.native-input[disabled] {
+        opacity: 0.7;
+      }
+    }
+  }
+}
+
+.password-change-item {
+  --padding-start: 0;
+  --background: transparent;
+  margin-top: 24px;
+
+  ion-button {
+    width: 100%;
+    height: 44px;
+    --border-radius: 8px;
+    font-weight: 500;
+  }
+}
+
+ion-select {
+  --padding-start: 16px;
+  --padding-end: 16px;
+  --padding-top: 12px;
+  --padding-bottom: 12px;
+  --border-radius: 8px;
+  --background: #f8f9fa;
+  width: 100%;
+  margin-top: 4px;
+}
+
+ion-select::part(placeholder) {
+  color: #a0aec0;
+}
+
+ion-select::part(text) {
+  color: var(--ion-text-color);
+} 

+ 309 - 0
AiStudy-app/src/app/pages/profile/profile.page.ts

@@ -0,0 +1,309 @@
+import { Component, OnInit } from '@angular/core';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonBackButton,
+  IonButtons,
+  IonList,
+  IonItem,
+  IonLabel,
+  IonInput,
+  IonButton,
+  IonAvatar,
+  IonIcon,
+  AlertController,
+  ActionSheetController,
+  IonSelect,
+  IonSelectOption
+} from '@ionic/angular/standalone';
+import { NgIf, NgFor } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { addIcons } from 'ionicons';
+import { 
+  cameraOutline, 
+  imageOutline,
+  chevronBackOutline
+} from 'ionicons/icons';
+import { CloudUser } from 'src/lib/ncloud';
+import Parse from 'parse';
+
+@Component({
+  selector: 'app-profile',
+  templateUrl: './profile.page.html',
+  styleUrls: ['./profile.page.scss'],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonBackButton,
+    IonButtons,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonInput,
+    IonButton,
+    IonAvatar,
+    IonIcon,
+    NgIf,
+    NgFor,
+    FormsModule,
+    IonSelect,
+    IonSelectOption
+  ]
+})
+export class ProfilePage implements OnInit {
+  userProfile = {
+    avatar: 'assets/anime-avatar.png',
+    username: '',
+    email: '',
+    bio: '',
+    level: 'LV.1 新手学习者',
+    gender: '',
+    age: null
+  };
+
+  passwordData = {
+    oldPassword: '',
+    newPassword: '',
+    confirmPassword: ''
+  };
+
+  genderOptions = [
+    { value: 'male', label: '男' },
+    { value: 'female', label: '女' },
+    { value: 'other', label: '其他' }
+  ];
+
+  ageOptions = [
+    { value: 1, label: '1岁', group: '18岁以下' },
+    { value: 2, label: '2岁', group: '18岁以下' },
+    // ... 生成1-17岁
+    ...Array.from({ length: 17 }, (_, i) => ({
+      value: i + 1,
+      label: `${i + 1}岁`,
+      group: '18岁以下'
+    })),
+    // ... 生成18-30岁
+    ...Array.from({ length: 13 }, (_, i) => ({
+      value: i + 18,
+      label: `${i + 18}岁`,
+      group: '18-30岁'
+    })),
+    // ... 生成31-50岁
+    ...Array.from({ length: 20 }, (_, i) => ({
+      value: i + 31,
+      label: `${i + 31}岁`,
+      group: '31-50岁'
+    })),
+    // ... 生成51-100岁
+    ...Array.from({ length: 50 }, (_, i) => ({
+      value: i + 51,
+      label: `${i + 51}岁`,
+      group: '50岁以上'
+    }))
+  ];
+
+  constructor(
+    private alertController: AlertController,
+    private actionSheetController: ActionSheetController
+  ) {
+    addIcons({ cameraOutline, imageOutline, chevronBackOutline });
+  }
+
+  ngOnInit() {
+    this.loadUserProfile();
+  }
+
+  async loadUserProfile() {
+    try {
+      const currentUser = new CloudUser();
+      if (currentUser && currentUser.get('username')) {
+        this.userProfile = {
+          avatar: currentUser.get('avatar') || 'assets/anime-avatar.png',
+          username: currentUser.get('username'),
+          email: currentUser.get('email'),
+          bio: currentUser.get('bio') || '',
+          level: currentUser.get('level') || 'LV.1 新手学习者',
+          gender: currentUser.get('gender') || '',
+          age: currentUser.get('age') || null
+        };
+      }
+    } catch (error) {
+      console.error('加载用户资料失败:', error);
+    }
+  }
+
+  async changeAvatar() {
+    const actionSheet = await this.actionSheetController.create({
+      header: '选择头像来源',
+      buttons: [
+        {
+          text: '拍照',
+          icon: 'camera-outline',
+          handler: () => {
+            // TODO: 实现拍照功能
+            this.showNotImplemented();
+          }
+        },
+        {
+          text: '从相册选择',
+          icon: 'image-outline',
+          handler: () => {
+            // TODO: 实现相册选择功能
+            this.showNotImplemented();
+          }
+        },
+        {
+          text: '取消',
+          role: 'cancel'
+        }
+      ]
+    });
+    await actionSheet.present();
+  }
+
+  async saveProfile() {
+    try {
+      const currentUser = new CloudUser();
+      if (currentUser) {
+        // 基本验证
+        if (!this.userProfile.username || !this.userProfile.email) {
+          this.showAlert('错误', '用户名和邮箱不能为空');
+          return;
+        }
+
+        // 验证邮箱格式
+        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+        if (!emailRegex.test(this.userProfile.email)) {
+          this.showAlert('错误', '请输入正确的邮箱格式');
+          return;
+        }
+
+        // 直接尝试更新用户资料
+        currentUser.set({
+          username: this.userProfile.username,
+          email: this.userProfile.email,
+          bio: this.userProfile.bio,
+          gender: this.userProfile.gender,
+          age: this.userProfile.age
+        });
+        
+        await currentUser.save();
+
+        const alert = await this.alertController.create({
+          header: '成功',
+          message: '个人资料已更新',
+          buttons: ['确定']
+        });
+        await alert.present();
+      }
+    } catch (error: any) {
+      console.error('保存用户资料失败:', error);
+      let errorMessage = '保存失败,请稍后重试';
+      
+      // 处理特定错误
+      if (error.code === 202) {
+        errorMessage = '该用户名已被使用';
+      } else if (error.code === 203) {
+        errorMessage = '该邮箱已被使用';
+      }
+
+      const alert = await this.alertController.create({
+        header: '错误',
+        message: errorMessage,
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
+
+  async changePassword() {
+    const alert = await this.alertController.create({
+      header: '修改密码',
+      inputs: [
+        {
+          name: 'oldPassword',
+          type: 'password',
+          placeholder: '当前密码'
+        },
+        {
+          name: 'newPassword',
+          type: 'password',
+          placeholder: '新密码'
+        },
+        {
+          name: 'confirmPassword',
+          type: 'password',
+          placeholder: '确认新密码'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '确认',
+          handler: async (data) => {
+            if (!data.oldPassword || !data.newPassword || !data.confirmPassword) {
+              this.showAlert('错误', '请填写所有密码字段');
+              return false;
+            }
+
+            if (data.newPassword !== data.confirmPassword) {
+              this.showAlert('错误', '两次输入的新密码不一致');
+              return false;
+            }
+
+            if (data.newPassword.length < 6) {
+              this.showAlert('错误', '新密码长度至少为6位');
+              return false;
+            }
+
+            try {
+              const currentUser = new CloudUser();
+              // 验证旧密码
+              await currentUser.login(currentUser.get('username'), data.oldPassword);
+              // 更新密码
+              currentUser.set({
+                password: data.newPassword
+              });
+              await currentUser.save();
+              
+              this.showAlert('成功', '密码修改成功');
+              return true;
+            } catch (error) {
+              console.error('修改密码失败:', error);
+              this.showAlert('错误', '当前密码错误或修改失败');
+              return false;
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+  private async showAlert(header: string, message: string) {
+    const alert = await this.alertController.create({
+      header,
+      message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+
+  private async showNotImplemented() {
+    const alert = await this.alertController.create({
+      header: '提示',
+      message: '该功能正在开发中',
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+} 

+ 3 - 3
AiStudy-app/src/app/tab3/tab3.page.html

@@ -139,15 +139,15 @@
 
       <div class="stats-grid">
         <div class="stat-item">
-          <div class="stat-value">12</div>
+          <div class="stat-value">{{ userStats.learningDays }}</div>
           <div class="stat-label">学习天数</div>
         </div>
         <div class="stat-item">
-          <div class="stat-value">5</div>
+          <div class="stat-value">{{ userStats.achievementCount }}</div>
           <div class="stat-label">获得勋章</div>
         </div>
         <div class="stat-item">
-          <div class="stat-value">89%</div>
+          <div class="stat-value">{{ userStats.completionRate }}%</div>
           <div class="stat-label">完成率</div>
         </div>
       </div>

+ 49 - 7
AiStudy-app/src/app/tab3/tab3.page.ts

@@ -92,6 +92,13 @@ export class Tab3Page implements OnInit {
   // 是否显示注册表单
   showRegisterForm: boolean = false;
   
+  // 添加用户统计数据属性
+  userStats = {
+    learningDays: 0,
+    achievementCount: 0,
+    completionRate: 0
+  };
+
   constructor(
     private router: Router,
     private alertController: AlertController
@@ -118,13 +125,36 @@ export class Tab3Page implements OnInit {
   ngOnInit() {
     // 检查是否已登录
     const currentUser = new CloudUser();
-    if (currentUser) {
+    if (currentUser && currentUser.get('username')) { // 添加更严格的登录检查
       this.isLoggedIn = true;
       this.userName = currentUser.get('username');
+      this.userLevel = currentUser.get('level') || 'LV.1 新手学习者';
       this.userAvatar = currentUser.get('avatar') || 'assets/anime-avatar.png';
+      
+      // 获取用户统计数据
+      this.userStats = {
+        learningDays: currentUser.get('learningDays') || 0,
+        achievementCount: currentUser.get('achievementCount') || 0,
+        completionRate: currentUser.get('completionRate') || 0
+      };
+    } else {
+      this.isLoggedIn = false;
+      this.resetUserData();
     }
   }
 
+  // 添加重置用户数据的方法
+  private resetUserData() {
+    this.userName = '';
+    this.userAvatar = 'assets/anime-avatar.png';
+    this.userLevel = 'LV.1 新手学习者';
+    this.userStats = {
+      learningDays: 0,
+      achievementCount: 0,
+      completionRate: 0
+    };
+  }
+
   // 切换注册/登录表单
   toggleForm() {
     this.showRegisterForm = !this.showRegisterForm;
@@ -145,8 +175,8 @@ export class Tab3Page implements OnInit {
       }
 
       // 执行登录
-      let user = new CloudUser()
-      user.login(this.loginData.username, this.loginData.password);
+      let user = new CloudUser();
+      await user.login(this.loginData.username, this.loginData.password);
       
       // 更新界面状态
       this.isLoggedIn = true;
@@ -154,6 +184,13 @@ export class Tab3Page implements OnInit {
       this.userLevel = user.get('level') || 'LV.1 新手学习者';
       this.userAvatar = user.get('avatar') || 'assets/anime-avatar.png';
       
+      // 更新用户统计数据
+      this.userStats = {
+        learningDays: user.get('learningDays') || 0,
+        achievementCount: user.get('achievementCount') || 0,
+        completionRate: user.get('completionRate') || 0
+      };
+
       // 显示登录成功提示
       const alert = await this.alertController.create({
         header: '成功',
@@ -324,15 +361,20 @@ export class Tab3Page implements OnInit {
           handler: async () => {
             try {
               let user = new CloudUser();
-              user.logout();
-              // 只清除 Parse token,保留 AI token
+              await user.logout();
               localStorage.removeItem('parseToken');
               
               this.isLoggedIn = false;
-              this.userName = '';
-              this.userAvatar = 'assets/anime-avatar.png';
+              this.resetUserData();
             } catch (error) {
               console.error('退出登录失败:', error);
+              // 添加错误提示
+              const errorAlert = await this.alertController.create({
+                header: '错误',
+                message: '退出登录失败,请稍后重试',
+                buttons: ['确定']
+              });
+              await errorAlert.present();
             }
           }
         }

+ 5 - 0
AiStudy-app/src/app/tabs/tabs.routes.ts

@@ -26,6 +26,11 @@ export const routes: Routes = [
         loadComponent: () =>
           import('../pages/favorite-exercises/favorite-exercises.page').then((m) => m.FavoriteExercisesPage),
       },
+      {
+        path: 'tab3/profile',
+        loadComponent: () =>
+          import('../pages/profile/profile.page').then((m) => m.ProfilePage),
+      },
       {
         path: '',
         redirectTo: '/tabs/tab1',