You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular页面重载后新增玩家致旧玩家消失问题求助

Angular + Firebase 游戏页面重载后添加新玩家丢失旧数据问题

问题详情

我用Angular结合Firebase开发Ring of Fire克隆游戏,此前功能运行正常。新增显示玩家姓名与头像的功能后,出现页面重载后添加新玩家时旧玩家数据消失的问题,确定该问题与游戏数据的初始加载逻辑有关,但作为Angular新手无法修复。


DialogAddPlayerComponent 代码

import { Component, OnInit, inject } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NgFor } from '@angular/common';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { GameComponent } from '../game/game.component';
import { AddService } from '../add-service/add.service';
import { ActivatedRoute } from '@angular/router';

interface Avatar {
  value: number;
  viewValue: any;
  name: string;
}
@Component({
  selector: 'app-dialog-add-player',
  templateUrl: './dialog-add-player.component.html',
  styleUrls: ['./dialog-add-player.component.scss'],
})
export class DialogAddPlayerComponent implements OnInit {
  name: string = '';
  avatars: Avatar[] = [];
  selectedAvatar: any;
  currentId!: any;

  constructor(
    public dialogRef: MatDialogRef<DialogAddPlayerComponent>,
    private route: ActivatedRoute,
    public gameComponent: GameComponent
  ) {}

  async ngOnInit(): Promise<void> {
    await this.getCurrentId();
    this.getAvatarImages();
  }

  onNoClick() {
    this.dialogRef.close();
  }

  async sendPlayerData() {
    let newPlayer = { name: this.name, avatars: this.selectedAvatar };
    let stringData = JSON.stringify(newPlayer);
    await this.gameComponent.addPlayer(stringData, this.currentId);
  }

  async getCurrentId() {
    this.currentId = localStorage.getItem('id');
  }

  getAvatarImages() {
    for (let i = 1; i < 51; i++) {
      this.avatars.push({
        value: i,
        viewValue: '/assets/img/avatars_withbg/' + i + '.png',
        name: this.names[i - 1],
      });
    }
  }

  names = [
    'Elowen',
    'Aldric',
    'Thorn',
    'Maelis',
    'Caelan',
    'Brynn',
    'Eowyn',
    'Lysander',
    'Rowena',
    'Crispin',
    'Seraphina',
    'Oswin',
    'Lorelei',
    'Cassian',
    'Elysia',
    'Aurelius',
    'Ilythia',
    'Orin',
    'Morgana',
    'Finnian',
    'Genevieve',
    'Peregrine',
    'Elara',
    'Quinlan',
    'Seraphim',
    'Rhiannon',
    'Cedric',
    'Ondine',
    'Eadric',
    'Isolde',
    'Thaddeus',
    'Aurora',
    'Gawain',
    'Artemis',
    'Calista',
    'Gareth',
    'Thalassa',
    'Cyprian',
    'Eulalia',
    'Drystan',
    'Morgause',
    'Ezrael',
    'Eirian',
    'Faelan',
    'Sylvaine',
    'Lorcan',
    'Morrigan',
    'Fiora',
    'Finlo',
    'Danilo',
  ];
}

GameComponent 代码

import { Component, Injectable, OnInit, inject } from '@angular/core';
import {
  Firestore,
  collectionData,
  collection,
  doc,
  addDoc,
  updateDoc,
} from '@angular/fire/firestore';
import { Game } from 'src/models/game';
import {
  MatDialog,
  MAT_DIALOG_DATA,
  MatDialogRef,
  MatDialogModule,
} from '@angular/material/dialog';
import { NgIf } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { DialogAddPlayerComponent } from '../dialog-add-player/dialog-add-player.component';
import { BehaviorSubject, Observable } from 'rxjs';
import { onSnapshot } from '@firebase/firestore';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-game',
  templateUrl: './game.component.html',
  styleUrls: ['./game.component.scss'],
})
@Injectable()
export class GameComponent implements OnInit {
  game: Game = new Game();

  unsubGamesList;
  unsubSingleDoc;
  currentId: string = '';
  firestore: Firestore = inject(Firestore);

  constructor(private route: ActivatedRoute, public dialog: MatDialog) {
    this.unsubGamesList = onSnapshot(this.getGamesRef(), (list: any) => {
      list.forEach((element: any) => {
        //console.log(element.data());
      });
    });

    this.unsubSingleDoc = onSnapshot(
      this.getSingleDocRef('games', '1234'),
      (doc) => {}
    );
  }

  ngOnInit(): void {
    //route gibt url-rest zurück --> dieser ist gleichzeitig die firebase-id und kann
    // so über einen snapshot-listener das zugehörige spiel anzeigen und updaten
    this.route.params.subscribe((params) => {
      this.unsubSingleDoc = onSnapshot(
        this.getSingleDocRef('games', params['id']),
        (gameDoc) => {
          const gameData = gameDoc.data();
          this.currentId = params['id'];
          localStorage.setItem('id', this.currentId);
          if (gameData) {
            this.game.currentPlayer = gameData['currentPlayer'];
            this.game.playedCards = gameData['playedCards'];
            this.game.players = this.getNames(gameData['players']);
            this.game.stack = gameData['stack'];
            this.game.pickCardAnimation = gameData['pickCardAnimation'];
            this.game.currentCard = gameData['currentCard'];
            this.game.avatarId = this.getAvatars(gameData['players']);;
          }
        }
      );
    });
  }

  getNames(playersList: any) {
    const namesArray = playersList.map((jsonString: string) => {
      const jsonObject = JSON.parse(jsonString);
      return jsonObject.name;
    });
    return namesArray;
  }

  getAvatars(playersList: any){
    const avatarArray = playersList.map((jsonString: string) => {
      const jsonObject = JSON.parse(jsonString);
      return jsonObject.avatars;
    });
    return avatarArray;
  }

  ngOnDestroy(): void {
    this.unsubGamesList();
    this.unsubSingleDoc();
  }

  getGamesRef() {
    return collection(this.firestore, 'games');
  }

  getSingleDocRef(colId: string, docId: string) {
    return doc(collection(this.firestore, colId), docId);
  }

  async addGame(item: {}) {
    await addDoc(this.getGamesRef(), item)
      .catch((err) => {
        console.log('Adding document failed: ' + err);
      })
      .then((docRef: any) => {
        this.currentId = docRef.id;
        console.log('Document written with id: ' + docRef.id);
      });
  }

  async updateGame() {
    if (this.currentId) {
      try {
        let gameDocRef = this.getSingleDocRef('games', this.currentId);
        await updateDoc(gameDocRef, this.game.toJson());
        console.log('Game successfully updated');
      } catch (error) {
        console.error('Error while update:', error);
      }
    } else if (!this.currentId) {
      console.error('currentId is not available');
    }
  }

  takeCard() {
    if (!this.game.pickCardAnimation) {
      this.game.currentCard = this.game.stack.pop()!;
      this.game.pickCardAnimation = true;
      //console.log(this.game.playedCards);

      this.game.currentPlayer++;
      this.game.currentPlayer =
        this.game.currentPlayer % this.game.players.length;

      this.updateGame();

      setTimeout(() => {
        this.game.playedCards.push(this.game.currentCard);
        this.game.pickCardAnimation = false;
        this.updateGame();
      }, 1000);
    }
  }

  async addPlayer(playerData: string, id: string) {
    this.currentId = id;
    console.log('addPlayer executed, playerData:', playerData);
    console.log('currentID:', this.currentId);
    this.game.players.push(playerData);

    await this.updateGame();
  }

  openDialog(): void {
    const dialogRef = this.dialog.open(DialogAddPlayerComponent);

    dialogRef.afterClosed().subscribe(() => {
      //name in subscription!?
      /* if (name && name.length > 0) {
        this.game.players.push(name);
        this.updateGame();
      } */
    });
  }
}

Game 模型代码

export class Player {
    public id: number;
    public name: string;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
}
export class Game{
    //public players: Player[] = [];
    public players: string[] = []; 
    public stack: string[] = [];
    public playedCards: string[] = [];
    public currentPlayer: number = 0;
    public pickCardAnimation = false;
    public currentCard: string = '';
    public avatarId: string = '';


    constructor(){
        for (let i = 1; i < 14; i++) {
            this.stack.push('hearts_'+i);
            this.stack.push('ace_'+i);
            this.stack.push('clubs_'+i);
            this.stack.push('diamonds_'+i);
        }
        shuffle(this.stack);
    }

    public toJson(){
        return {
            players: this.players,
            stack: this.stack,
            playedCards: this.playedCards,
            currentPlayer: this.currentPlayer,
            pickCardAnimation: this.pickCardAnimation,
            currentCard: this.currentCard,
            avatarId: this.avatarId,
        }
    }
  
}

function shuffle(array:string[]) {
    let currentIndex = array.length,  randomIndex;
  
    // While there remain elements to shuffle.
    while (currentIndex > 0) {
  
      // Pick a remaining element.
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex--;
  
      // And swap it with the current element.
      [array[currentIndex], array[randomIndex]] = [
        array[randomIndex], array[currentIndex]];
    }
  
    return array;
  }

问题根源与修复方案

核心问题

在GameComponent的ngOnInit中,加载Firebase数据时,你把game.players替换成了解析后的纯姓名数组,而非保留Firebase存储的原始JSON字符串数组:

// 错误代码
this.game.players = this.getNames(gameData['players']);

这会导致:

  1. 页面重载后,game.players变为纯姓名字符串数组,破坏了原本的JSON字符串数组结构
  2. 新增玩家时,往数组中push玩家JSON字符串,导致数组同时存在两种格式数据
  3. 再次更新Firebase时数据结构混乱,重载后解析失败,旧玩家数据丢失

同时,game.avatarId被赋值为头像数组,但模型中它是单个字符串类型,存在数据类型不匹配的隐患。

修复步骤

  1. 修正GameComponent的数据加载逻辑
    在组件中新增两个属性用于存储解析后的姓名和头像(仅用于页面展示),保留game.players为原始JSON字符串数组(用于Firebase更新):

    // 在GameComponent类中新增属性
    playerNames: string[] = [];
    playerAvatars: any[] = [];
    
    // 修改ngOnInit中的数据加载部分
    ngOnInit(): void {
      this.route.params.subscribe((params) => {
        this.unsubSingleDoc = onSnapshot(
          this.getSingleDocRef('games', params['id']),
          (gameDoc) => {
            const gameData = gameDoc.data();
            this.currentId = params['id'];
            localStorage.setItem('id', this.currentId);
            if (gameData) {
              this.game.currentPlayer = gameData['currentPlayer'];
              this.game.playedCards = gameData['playedCards'];
              // 保留原始JSON字符串数组,用于后续Firebase更新
              this.game.players = gameData['players'];
              // 解析出姓名和头像,仅用于页面展示
              this.playerNames = this.getNames(gameData['players']);
              this.playerAvatars = this.getAvatars(gameData['players']);
              this.game.stack = gameData['stack'];
              this.game.pickCardAnimation = gameData['pickCardAnimation'];
              this.game.currentCard = gameData['currentCard'];
              // 删除错误的avatarId数组赋值(模型中为单个字符串)
              // this.game.avatarId = this.getAvatars(gameData['players']);
            }
          }
        );
      });
    }
    
  2. 修正Game模型的avatarId属性
    如果不需要单个头像ID,直接删除该属性;如果需要(比如存储当前玩家头像),明确其用途为单个值而非数组:

    export class Game{
      public players: string[] = []; 
      public stack: string[] = [];
      public playedCards: string[] = [];
      public currentPlayer: number = 0;
      public pickCardAnimation = false;
      public currentCard: string = '';
      // 删除无用的avatarId,或改为当前玩家头像ID
      // public avatarId: string = '';
    }
    
  3. 修改页面模板绑定
    页面中显示玩家姓名和头像的部分,改为绑定新增的playerNames和playerAvatars数组:

    <!-- 示例:循环展示玩家 -->
    <div *ngFor="let name of playerNames; let i = index">
      <img [src]="playerAvatars[i]" alt="{{name}}">
      <span>{{name}}</span>
    </div>
    
  4. 验证addPlayer逻辑
    addPlayer方法中往game.playerspush玩家JSON字符串的逻辑是正确的,无需修改,确保新增玩家时数据结构一致。


内容的提问来源于stack exchange,提问作者HolyPoly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:05:53