s202226701043 hai 3 meses
pai
achega
0e0ea27d44

+ 1 - 1
novel-app/package-lock.json

@@ -17,6 +17,7 @@
         "@angular/platform-browser-dynamic": "^18.0.0",
         "@angular/router": "^18.0.0",
         "@capacitor/app": "6.0.2",
+        "@capacitor/camera": "^6.1.1",
         "@capacitor/core": "6.2.0",
         "@capacitor/haptics": "6.0.2",
         "@capacitor/keyboard": "6.0.3",
@@ -2782,7 +2783,6 @@
       "resolved": "https://registry.npmmirror.com/@capacitor/camera/-/camera-6.1.1.tgz",
       "integrity": "sha512-bKBGQS59178KzzBlao2+qaGZ1+7BFTZiEzLpsLbaUnp/qsc1KduSdrqfyEOpHkFLnu6N/YRKmmCzJpuytZTHfQ==",
       "license": "MIT",
-      "peer": true,
       "peerDependencies": {
         "@capacitor/core": "^6.0.0"
       }

+ 1 - 0
novel-app/package.json

@@ -22,6 +22,7 @@
     "@angular/platform-browser-dynamic": "^18.0.0",
     "@angular/router": "^18.0.0",
     "@capacitor/app": "6.0.2",
+    "@capacitor/camera": "^6.1.1",
     "@capacitor/core": "6.2.0",
     "@capacitor/haptics": "6.0.2",
     "@capacitor/keyboard": "6.0.3",

+ 1 - 1
novel-app/src/app/app.component.ts

@@ -6,7 +6,7 @@ import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
   selector: 'app-root',
   templateUrl: 'app.component.html',
   standalone: true,
-  imports: [IonApp, IonRouterOutlet, RouterOutlet],
+  imports: [ RouterOutlet],
 })
 export class AppComponent {
   constructor() { }

+ 26 - 1
novel-app/src/app/app.routes.ts

@@ -1,8 +1,33 @@
-import { Routes } from '@angular/router';
+import { NgModule } from '@angular/core';
+import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
+import { RouteReuseStrategy } from '@angular/router';
+import { HttpClientModule } from '@angular/common/http';
+import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
+
+
 
 export const routes: Routes = [
   {
     path: '',
     loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes),
   },
+  {
+    path: 'register',
+    loadComponent: () => import('./register/register.page').then( m => m.RegisterPage)
+  },
+  {
+    path: 'login',
+    loadComponent: () => import('./login/login.page').then( m => m.LoginPage)
+  },
+
+
 ];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),HttpClientModule],
+  exports: [RouterModule],
+  providers: [
+    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
+  ],
+})
+export class AppRoutingModule {}

+ 46 - 0
novel-app/src/app/login/login.page.html

@@ -0,0 +1,46 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab4"></ion-back-button>
+    </ion-buttons>
+    <ion-title>登录</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
+    <ion-item>
+      <ion-label position="floating">手机号</ion-label>
+      <ion-input 
+        type="tel" 
+        formControlName="phone"
+        (ionFocus)="clearError()"
+      ></ion-input>
+    </ion-item>
+    <div class="error-message" 
+         *ngIf="loginForm.get('phone')?.errors && loginForm.get('phone')?.touched">
+      请输入正确的11位手机号
+    </div>
+
+    <ion-item>
+      <ion-label position="floating">密码</ion-label>
+      <ion-input 
+        type="password" 
+        formControlName="password"
+        (ionFocus)="clearError()"
+      ></ion-input>
+    </ion-item>
+    <div class="error-message" 
+         *ngIf="loginForm.get('password')?.errors && loginForm.get('password')?.touched">
+      密码格式不正确,需要包含字母和数字,且长度不少于8位
+    </div>
+
+    <div class="error-message" *ngIf="loginError">
+      {{ loginError }}
+    </div>
+
+    <ion-button expand="block" type="submit" [disabled]="loginForm.invalid">
+      登录
+    </ion-button>
+  </form>
+</ion-content>

+ 0 - 0
novel-app/src/app/login/login.page.scss


+ 19 - 0
novel-app/src/app/login/login.page.spec.ts

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

+ 67 - 0
novel-app/src/app/login/login.page.ts

@@ -0,0 +1,67 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonButtons, IonContent, IonHeader, IonTitle, IonToolbar,IonBackButton, IonItem, IonLabel, IonButton } from '@ionic/angular/standalone';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { UserService } from '.././services/user.service';
+import { AlertController } from '@ionic/angular';
+
+
+@Component({
+  selector: 'app-login',
+  templateUrl: './login.page.html',
+  styleUrls: ['./login.page.scss'],
+  standalone: true,
+  imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, 
+    FormsModule,IonButtons,IonBackButton,IonItem,IonLabel,IonButton,ReactiveFormsModule]
+})
+export class LoginPage implements OnInit {
+  loginForm: FormGroup;
+  loginError: string = '';
+
+  constructor(
+    private formBuilder: FormBuilder,
+    private router: Router,
+    private userService: UserService,
+    private alertController: AlertController
+  ) {
+    this.loginForm = this.formBuilder.group({
+      phone: ['', [Validators.required, Validators.pattern('^1[3-9]\\d{9}$')]],
+      password: ['', [Validators.required, Validators.minLength(8)]]
+    });
+  }
+
+  ngOnInit() {}
+
+  async onSubmit() {
+    if (this.loginForm.valid) {
+      try {
+        const result = await this.userService.login(this.loginForm.value).toPromise();
+        if (result.success) {
+          this.router.navigate(['/tabs/tab4']);
+        } else {
+          this.loginError = '手机号或密码错误';
+          await this.showErrorAlert('登录失败', '手机号或密码错误');
+        }
+      } catch (error) {
+        console.error('Login error:', error);
+        this.loginError = '登录失败,请稍后重试';
+        await this.showErrorAlert('登录失败', '请稍后重试');
+      }
+    }
+  }
+
+  async showErrorAlert(header: string, message: string) {
+    const alert = await this.alertController.create({
+      header,
+      message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+
+  clearError() {
+    this.loginError = '';
+  }
+} 

+ 4 - 2
novel-app/src/app/page-novel/page-novel.component.ts

@@ -1,12 +1,14 @@
 import { Component, OnInit } from '@angular/core';
-import { IonButton, IonContent, IonInput, IonTextarea } from '@ionic/angular/standalone';
+import { IonContent, IonHeader, IonTitle, IonToolbar, 
+  IonLabel,IonItem,IonInput,IonButton,IonBackButton,IonIcon,
+  IonTextarea} from '@ionic/angular/standalone';
 import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
 
 @Component({
   selector: 'app-page-novel',
   templateUrl: './page-novel.component.html',
   styleUrls: ['./page-novel.component.scss'],
-  imports: [IonButton, IonContent, IonTextarea, IonInput, MarkdownPreviewModule
+  imports: [IonTextarea, IonContent,IonInput,IonButton,MarkdownPreviewModule
   ],
   standalone: true,
 })

+ 51 - 0
novel-app/src/app/register/register.page.html

@@ -0,0 +1,51 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab4"></ion-back-button>
+    </ion-buttons>
+    <ion-title>注册</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
+    <ion-item>
+      <ion-label position="floating">用户名</ion-label>
+      <ion-input type="text" formControlName="username"></ion-input>
+    </ion-item>
+    <div class="error-message" 
+         *ngIf="registerForm.get('username')?.errors && registerForm.get('username')?.touched">
+      请输入用户名
+    </div>
+
+    <ion-item>
+      <ion-label position="floating">手机号</ion-label>
+      <ion-input type="tel" formControlName="phone"></ion-input>
+    </ion-item>
+    <div class="error-message" 
+         *ngIf="registerForm.get('phone')?.errors && registerForm.get('phone')?.touched">
+      请输入正确的11位手机号
+    </div>
+
+    <ion-item>
+      <ion-label position="floating">密码</ion-label>
+      <ion-input type="password" formControlName="password"></ion-input>
+    </ion-item>
+    <div class="error-message" 
+         *ngIf="registerForm.get('password')?.errors && registerForm.get('password')?.touched">
+      密码需要包含字母和数字,且长度不少于8位
+    </div>
+
+    <div class="avatar-upload">
+      <ion-button expand="block" (click)="uploadAvatar()">
+        <ion-icon name="camera-outline" slot="start"></ion-icon>
+        上传头像
+      </ion-button>
+      <img *ngIf="avatarPreview" [src]="avatarPreview" alt="avatar preview">
+    </div>
+
+    <ion-button expand="block" type="submit" [disabled]="registerForm.invalid">
+      注册
+    </ion-button>
+  </form>
+</ion-content> 

+ 0 - 0
novel-app/src/app/register/register.page.scss


+ 17 - 0
novel-app/src/app/register/register.page.spec.ts

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

+ 94 - 0
novel-app/src/app/register/register.page.ts

@@ -0,0 +1,94 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AbstractControl, FormsModule } from '@angular/forms';
+import {Router} from '@angular/router';
+import { AlertController, IonBackButton, IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonInput, IonItem, IonLabel, IonList, IonTitle, IonToolbar,ToastController } from '@ionic/angular/standalone';
+import { FormBuilder, FormGroup, Validators,ReactiveFormsModule } from '@angular/forms';
+import { Camera, CameraResultType } from '@capacitor/camera';
+import { UserService } from '../services/user.service';
+
+@Component({
+  selector: 'app-register',
+  templateUrl: './register.page.html',
+  styleUrls: ['./register.page.scss'],
+  standalone: true,
+  imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule
+    ,IonLabel,IonItem,IonInput,IonButton,IonButtons,IonBackButton,IonIcon,ReactiveFormsModule]
+})
+export class RegisterPage implements OnInit {
+  registerForm: FormGroup = this.formBuilder.group({
+    username: ['', [Validators.required]],
+    phone: ['', [Validators.required, Validators.pattern('^1[3-9]\\d{9}$')]],
+    password: ['', [
+      Validators.required,
+      Validators.pattern('^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$')
+    ]]
+  });
+  
+  avatarPreview: string | undefined;
+
+  constructor(
+    private formBuilder: FormBuilder,
+    private router: Router,
+    private userService: UserService,
+    private alertController: AlertController
+  ) {}
+
+  ngOnInit() {}
+
+  get f(): { [key: string]: AbstractControl } {
+    if (!this.registerForm) {
+      return {};
+    }
+    return this.registerForm.controls;
+  }
+
+  async uploadAvatar() {
+    try {
+      const image = await Camera.getPhoto({
+        quality: 90,
+        allowEditing: true,
+        resultType: CameraResultType.DataUrl
+      });
+      
+      if (image && image.dataUrl) {
+        this.avatarPreview = image.dataUrl;
+      }
+    } catch (error) {
+      console.error('Error uploading avatar:', error);
+    }
+  }
+
+  async onSubmit() {
+    if (this.registerForm && this.registerForm.valid) {
+      try {
+        const userData = {
+          ...this.registerForm.value,
+          avatar: this.avatarPreview || 'assets/default-avatar.png'
+        };
+        
+        this.userService.register(userData).subscribe(
+          response => {
+            if (response.success) {
+              this.router.navigate(['/login']);
+            } else {
+              this.showAlert('错误', response.message || '注册失败');
+            }
+          }
+        );
+      } catch (error) {
+        console.error('Registration error:', error);
+        this.showAlert('错误', '注册失败');
+      }
+    }
+  }
+
+  private async showAlert(header: string, message: string) {
+    const alert = await this.alertController.create({
+      header,
+      message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+} 

+ 28 - 0
novel-app/src/app/services/auth.guard.ts

@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, Router } from '@angular/router';
+import { UserService } from './user.service';
+import { Observable } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthGuard implements CanActivate {
+  constructor(
+    private userService: UserService,
+    private router: Router
+  ) {}
+
+  canActivate(): Observable<boolean> {
+    return this.userService.isLoggedIn$.pipe(
+      take(1),
+      map(isLoggedIn => {
+        if (!isLoggedIn) {
+          this.router.navigate(['/login']);
+          return false;
+        }
+        return true;
+      })
+    );
+  }
+}

+ 42 - 0
novel-app/src/app/services/storage.service.ts

@@ -0,0 +1,42 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class StorageService {
+  constructor() {}
+
+  set(key: string, value: any): void {
+    try {
+      localStorage.setItem(key, JSON.stringify(value));
+    } catch (e) {
+      console.error('Error saving to localStorage', e);
+    }
+  }
+
+  get(key: string): any {
+    try {
+      const item = localStorage.getItem(key);
+      return item ? JSON.parse(item) : null;
+    } catch (e) {
+      console.error('Error reading from localStorage', e);
+      return null;
+    }
+  }
+
+  remove(key: string): void {
+    try {
+      localStorage.removeItem(key);
+    } catch (e) {
+      console.error('Error removing from localStorage', e);
+    }
+  }
+
+  clear(): void {
+    try {
+      localStorage.clear();
+    } catch (e) {
+      console.error('Error clearing localStorage', e);
+    }
+  }
+}

+ 119 - 0
novel-app/src/app/services/user.service.ts

@@ -0,0 +1,119 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable, of } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { map } from 'rxjs/operators';
+
+interface User {
+  username: string;
+  phone: string;
+  avatar: string;
+  password: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UserService {
+  private isLoggedInSubject = new BehaviorSubject<boolean>(false);
+  private currentUserSubject = new BehaviorSubject<User | null>(null);
+  
+  isLoggedIn$ = this.isLoggedInSubject.asObservable();
+  currentUser$ = this.currentUserSubject.asObservable();
+
+  // 模拟用户数据存储
+  private users: { [key: string]: User } = {};
+
+  constructor(private http: HttpClient) {
+    this.checkLoginStatus();
+  }
+
+  checkLoginStatus() {
+    const token = localStorage.getItem('userToken');
+    const userData = localStorage.getItem('userData');
+    if (token && userData) {
+      this.isLoggedInSubject.next(true);
+      this.currentUserSubject.next(JSON.parse(userData));
+    }
+  }
+
+  login(credentials: {phone: string, password: string}): Observable<any> {
+    // 模拟验证逻辑
+    const user = this.users[credentials.phone];
+    if (user && user.password === credentials.password) {
+      localStorage.setItem('userToken', 'dummy-token');
+      localStorage.setItem('userData', JSON.stringify(user));
+      this.isLoggedInSubject.next(true);
+      this.currentUserSubject.next(user);
+      return of({ success: true, user });
+    }
+    return of({ success: false });
+  }
+
+  register(userData: any): Observable<any> {
+    if (this.users[userData.phone]) {
+      return of({ success: false, message: '该手机号已被注册' });
+    }
+    const newUser: User = {
+      username: userData.username,
+      phone: userData.phone,
+      password: userData.password,
+      avatar: userData.avatar || 'assets/default-avatar.png'
+    };
+    this.users[userData.phone] = newUser;
+    return of({ success: true, user: newUser });
+  }
+
+  logout() {
+    localStorage.removeItem('userToken');
+    this.isLoggedInSubject.next(false);
+  }
+
+  updateAvatar(imageData: string): Observable<{success: boolean}> {
+    const currentUser = this.currentUserSubject.value;
+    if (currentUser) {
+      try {
+        // 更新用户数据
+        currentUser.avatar = imageData;
+        this.users[currentUser.phone].avatar = imageData;
+        
+        // 保存到本地存储
+        localStorage.setItem('userData', JSON.stringify(currentUser));
+        
+        // 更新当前用户状态
+        this.currentUserSubject.next({...currentUser});
+        
+        return of({ success: true });
+      } catch (error) {
+        console.error('Error updating avatar:', error);
+        return of({ success: false });
+      }
+    }
+    return of({ success: false });
+  }
+
+  getCurrentUser(): Observable<any> {
+    return this.currentUser$;
+  }
+
+  changePassword(oldPassword: string, newPassword: string): Observable<any> {
+    const currentUser = this.currentUserSubject.value;
+    if (currentUser && this.users[currentUser.phone].password === oldPassword) {
+      this.users[currentUser.phone].password = newPassword;
+      return of({ success: true });
+    }
+    return of({ success: false, message: '原密码错误' });
+  }
+
+  switchAccount(credentials: {phone: string, password: string}): Observable<any> {
+    return this.login(credentials);
+  }
+
+  deleteAccount(): Observable<any> {
+    const currentUser = this.currentUserSubject.value;
+    if (currentUser) {
+      delete this.users[currentUser.phone];
+      return of({ success: true });
+    }
+    return of({ success: false });
+  }
+} 

+ 69 - 0
novel-app/src/app/tab4/tab4.page.html

@@ -0,0 +1,69 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>个人中心</ion-title>
+    <ion-buttons slot="end">
+      <ion-button *ngIf="!isLoggedIn" (click)="goToRegister()">
+        注册
+      </ion-button>
+      <ion-button *ngIf="!isLoggedIn" (click)="goToLogin()">
+        登录
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true" class="ion-padding">
+  <div class="background-pattern"></div>
+  
+  <!-- 用户信息卡片 -->
+  <ion-card class="user-card">
+    <div class="avatar-container">
+      <ion-avatar>
+        <img [src]="user.avatar" alt="avatar"/>
+      </ion-avatar>
+    </div>
+    <ion-card-header>
+      <ion-card-title class="ion-text-center">{{user.username}}</ion-card-title>
+    </ion-card-header>
+  </ion-card>
+
+  <!-- 创作中心 -->
+  <ion-card class="feature-card" (click)="goToCreativeCenter()">
+    <ion-item lines="none">
+      <ion-icon name="create-outline" slot="start"></ion-icon>
+      <ion-label>创作中心</ion-label>
+      <ion-icon name="chevron-forward-outline" slot="end"></ion-icon>
+    </ion-item>
+  </ion-card>
+
+  <!-- 设置列表 -->
+  <ion-card class="settings-card">
+    <ion-list>
+      <ion-item (click)="changeAvatar()">
+        <ion-icon name="image-outline" slot="start"></ion-icon>
+        <ion-label>更换头像</ion-label>
+        <input 
+          type="file" 
+          #fileInput 
+          (change)="uploadAvatarFromFile($event)" 
+          accept="image/*" 
+          style="display: none">
+      </ion-item>
+
+      <ion-item (click)="changePassword()">
+        <ion-icon name="key-outline" slot="start"></ion-icon>
+        <ion-label>修改密码</ion-label>
+      </ion-item>
+
+      <ion-item (click)="switchAccount()">
+        <ion-icon name="swap-horizontal-outline" slot="start"></ion-icon>
+        <ion-label>切换账号</ion-label>
+      </ion-item>
+
+      <ion-item (click)="showLogoutConfirm()">
+        <ion-icon name="log-out-outline" slot="start" color="danger"></ion-icon>
+        <ion-label color="danger">注销账号</ion-label>
+      </ion-item>
+    </ion-list>
+  </ion-card>
+</ion-content> 

+ 69 - 0
novel-app/src/app/tab4/tab4.page.scss

@@ -0,0 +1,69 @@
+ion-content {
+  --background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+}
+
+.background-pattern {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  opacity: 0.1;
+  background-image: 
+    linear-gradient(30deg, #000000 12%, transparent 12.5%, transparent 87%, #000000 87.5%, #000000),
+    linear-gradient(150deg, #000000 12%, transparent 12.5%, transparent 87%, #000000 87.5%, #000000),
+    linear-gradient(30deg, #000000 12%, transparent 12.5%, transparent 87%, #000000 87.5%, #000000),
+    linear-gradient(150deg, #000000 12%, transparent 12.5%, transparent 87%, #000000 87.5%, #000000),
+    linear-gradient(60deg, #77777777 25%, transparent 25.5%, transparent 75%, #77777777 75%, #77777777);
+  background-size: 80px 140px;
+  background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0;
+  pointer-events: none;
+}
+
+.user-card {
+  margin-top: 20px;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 16px;
+  
+  .avatar-container {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+    
+    ion-avatar {
+      width: 100px;
+      height: 100px;
+      border: 3px solid #fff;
+      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+    }
+  }
+}
+
+.feature-card, .settings-card {
+  margin-top: 16px;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 16px;
+  
+  ion-item {
+    --background: transparent;
+    
+    ion-icon {
+      color: var(--ion-color-primary);
+    }
+  }
+}
+
+ion-list {
+  background: transparent;
+  padding: 0;
+}
+
+ion-item {
+  --padding-start: 16px;
+  --inner-padding-end: 16px;
+  --background: transparent;
+  
+  &:last-child {
+    --border-width: 0;
+  }
+} 

+ 18 - 0
novel-app/src/app/tab4/tab4.page.spec.ts

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

+ 314 - 0
novel-app/src/app/tab4/tab4.page.ts

@@ -0,0 +1,314 @@
+import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule,ReactiveFormsModule } from '@angular/forms';
+import{Router} from '@angular/router';
+import { IonAvatar, IonBackButton, IonButton, IonButtons, IonContent, IonHeader, IonIcon, 
+  IonItem, IonLabel, IonList, IonTitle, IonToolbar,ToastController, AlertController,IonCard, IonCardHeader, IonCardTitle} from '@ionic/angular/standalone';
+import { UserService } from '../services/user.service';
+import { ActionSheetController } from '@ionic/angular';
+import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
+ 
+interface User {
+  username: string;
+  avatar: string;
+}
+
+@Component({
+  selector: 'app-tab4',
+  templateUrl: './tab4.page.html',
+  styleUrls: ['./tab4.page.scss'],
+  standalone: true,
+  imports: [IonContent, IonHeader,  IonToolbar, CommonModule, FormsModule,
+    IonAvatar,IonList,IonItem,IonLabel,IonButtons,IonCard,IonIcon,IonTitle,IonButton,IonCardHeader,IonCardTitle]
+})
+export class Tab4Page implements OnInit {
+  isLoggedIn: boolean = false;
+  user: User = {
+    username: 'User_123456',
+    avatar: 'assets/default-avatar.png'
+  };
+
+  @ViewChild('fileInput') fileInput!: ElementRef;
+
+  constructor(
+    private router: Router,
+    private alertController: AlertController,
+    private actionSheetController: ActionSheetController,
+    private userService: UserService
+  ) {}
+
+  ngOnInit() {
+    this.checkLoginStatus();
+    this.subscribeToUserChanges();
+  }
+
+  subscribeToUserChanges() {
+    this.userService.currentUser$.subscribe(user => {
+      if (user) {
+        this.user = user;
+      }
+    });
+  }
+
+  checkLoginStatus() {
+    this.userService.isLoggedIn$.subscribe(
+      isLoggedIn => {
+        this.isLoggedIn = isLoggedIn;
+        if (isLoggedIn) {
+          // 获取用户信息
+          this.userService.getCurrentUser().subscribe(user => {
+            if (user) {
+              this.user = user;
+            }
+          });
+        }
+      }
+    );
+  }
+
+  async changeAvatar() {
+    try {
+      const actionSheet = await this.actionSheetController.create({
+        header: '选择头像',
+        buttons: [
+          {
+            text: '拍照',
+            icon: 'camera',
+            handler: () => {
+              this.takePicture('camera');
+            }
+          },
+          {
+            text: '从相册选择',
+            icon: 'image',
+            handler: () => {
+              this.takePicture('photos');
+            }
+          },
+          {
+            text: '从电脑选择',
+            icon: 'desktop',
+            handler: () => {
+              this.fileInput.nativeElement.click();
+            }
+          },
+          {
+            text: '取消',
+            icon: 'close',
+            role: 'cancel'
+          }
+        ]
+      });
+      await actionSheet.present();
+    } catch (error) {
+      console.error('Error showing action sheet:', error);
+      this.showAlert('错误', '无法打开选择菜单');
+    }
+  }
+
+  private async takePicture(sourceType: 'camera' | 'photos') {
+    try {
+      const image = await Camera.getPhoto({
+        quality: 90,
+        allowEditing: true,
+        resultType: CameraResultType.DataUrl,
+        source: sourceType === 'camera' ? CameraSource.Camera : CameraSource.Photos
+      });
+      
+      if (image && image.dataUrl) {
+        // 先更新本地显示
+        this.user.avatar = image.dataUrl;
+        
+        // 然后保存到服务
+        this.userService.updateAvatar(image.dataUrl).subscribe(
+          response => {
+            if (response.success) {
+              this.showAlert('成功', '头像更新成功');
+            } else {
+              // 如果更新失败,恢复原来的头像
+              this.userService.getCurrentUser().subscribe(user => {
+                if (user) {
+                  this.user.avatar = user.avatar;
+                }
+              });
+              this.showAlert('错误', '头像更新失败');
+            }
+          },
+          error => {
+            console.error('Error updating avatar:', error);
+            this.showAlert('错误', '头像更新失败');
+          }
+        );
+      }
+    } catch (error) {
+      console.error('Error taking photo:', error);
+      this.showAlert('错误', '获取图片失败');
+    }
+  }
+
+  async changePassword() {
+    const alert = await this.alertController.create({
+      header: '修改密码',
+      inputs: [
+        {
+          name: 'oldPassword',
+          type: 'password',
+          placeholder: '请输入原密码'
+        },
+        {
+          name: 'newPassword',
+          type: 'password',
+          placeholder: '请输入新密码'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '确定',
+          handler: (data) => {
+            this.userService.changePassword(data.oldPassword, data.newPassword).subscribe(
+              response => {
+                if (response.success) {
+                  this.showAlert('成功', '密码修改成功');
+                } else {
+                  this.showAlert('错误', response.message || '密码修改失败');
+                }
+              }
+            );
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+
+  async switchAccount() {
+    const alert = await this.alertController.create({
+      header: '切换账号',
+      inputs: [
+        {
+          name: 'phone',
+          type: 'tel',
+          placeholder: '请输入手机号'
+        },
+        {
+          name: 'password',
+          type: 'password',
+          placeholder: '请输入密码'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '确定',
+          handler: (data) => {
+            this.userService.switchAccount(data).subscribe(
+              response => {
+                if (response.success) {
+                  this.showAlert('成功', '账号切换成功');
+                  this.checkLoginStatus();
+                } else {
+                  this.showAlert('错误', '账号或密码错误');
+                }
+              }
+            );
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+
+  async showLogoutConfirm() {
+    const alert = await this.alertController.create({
+      header: '确认注销账号',
+      message: '您确定要注销账号吗?此操作不可逆',
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '确定',
+          handler: () => {
+            this.userService.deleteAccount().subscribe(
+              response => {
+                if (response.success) {
+                  this.userService.logout();
+                  this.router.navigate(['/login']);
+                }
+              }
+            );
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+
+  private async showAlert(header: string, message: string) {
+    const alert = await this.alertController.create({
+      header,
+      message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+
+  goToCreativeCenter() {
+    this.router.navigate(['/tabs/tab1']);
+  }
+
+  goToLogin() {
+    this.router.navigate(['/login']);
+  }
+
+  goToRegister() {
+    this.router.navigate(['/register']);
+  }
+
+  async uploadAvatarFromFile(event: any) {
+    const file = event.target.files[0];
+    if (file) {
+      try {
+        const reader = new FileReader();
+        reader.onload = (e: any) => {
+          const imageData = e.target.result;
+          // 先更新本地显示
+          this.user.avatar = imageData;
+          
+          // 然后保存到服务
+          this.userService.updateAvatar(imageData).subscribe(
+            response => {
+              if (response.success) {
+                this.showAlert('成功', '头像更新成功');
+              } else {
+                // 如果更新失败,恢复原来的头像
+                this.userService.getCurrentUser().subscribe(user => {
+                  if (user) {
+                    this.user.avatar = user.avatar;
+                  }
+                });
+                this.showAlert('错误', '头像更新失败');
+              }
+            },
+            error => {
+              console.error('Error updating avatar:', error);
+              this.showAlert('错误', '头像更新失败');
+            }
+          );
+        };
+        reader.readAsDataURL(file);
+      } catch (error) {
+        console.error('Error reading file:', error);
+        this.showAlert('错误', '文件读取失败');
+      }
+    }
+  }
+} 

+ 5 - 0
novel-app/src/app/tabs/tabs.page.html

@@ -14,5 +14,10 @@
       <ion-icon aria-hidden="true" name="chatbox"></ion-icon>
       <ion-label>论坛</ion-label>
     </ion-tab-button>
+
+    <ion-tab-button tab="tab4" (click)="goPage('/tabs/tab4')">
+      <ion-icon aria-hidden="true" name="person"></ion-icon>
+      <ion-label>个人</ion-label>
+    </ion-tab-button>
   </ion-tab-bar>
 </ion-tabs>

+ 2 - 2
novel-app/src/app/tabs/tabs.page.ts

@@ -2,7 +2,7 @@ import { Component, EnvironmentInjector, inject } from '@angular/core';
 import { Router } from '@angular/router';
 import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, NavController } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
-import { home, reader, chatbox } from 'ionicons/icons';
+import { home, reader, chatbox, person } from 'ionicons/icons';
 
 @Component({
   selector: 'app-tabs',
@@ -18,7 +18,7 @@ export class TabsPage {
     private navCtrl: NavController,
     private router: Router
   ) {
-    addIcons({ home, reader, chatbox });
+    addIcons({ home, reader, chatbox, person });
   }
   goPage(page: string) {
     this.router.navigateByUrl(page)

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

@@ -21,6 +21,11 @@ export const routes: Routes = [
         loadComponent: () =>
           import('../tab3/tab3.page').then((m) => m.Tab3Page),
       },
+      {
+        path: 'tab4',
+        loadComponent: () =>
+          import('../tab4/tab4.page').then((m) => m.Tab4Page),
+      },
       {
         path: 'novel',
         loadComponent: () =>

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "20222670105",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}