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

Angular多步骤表单实现D&D角色装备动态选择方案咨询

实现D&D角色表单的动态装备选择功能

需求概述

在Angular构建的D&D角色创建多步骤表单中,当用户选择职业后,需要从data.ts的装备列表中动态加载对应职业的装备选择项,每个选项包含item和alternate两个可选值,且选中item时需禁用alternate选项(反之同理)。

解决方案

不需要修改data.ts的JSON结构,现有结构已满足需求。我们通过Angular响应式表单动态生成装备选择控件,并通过表单状态监听实现互斥禁用逻辑。

步骤1:重构表单结构与职业监听

为每个装备选择项创建FormGroup,记录用户选择的类型和具体值,同时监听职业选择变化,自动加载对应装备选项。

修改creator.component.ts:

import { Component } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  Validators,
  FormArray,
  FormControl,
} from "@angular/forms";
import {
  dndClasses,
  dndRaces,
  dndBackgrounds,
  dndRacialBonuses,
  classArchetypesWithSpells,
  classSkills,
  fightingStyles,
  equipment,
} from "./data";

@Component({
  selector: "app-creator",
  templateUrl: "./creator.component.html",
  styleUrls: ["./creator.component.scss"],
})
export class CreatorComponent {
  currentStep = 1;
  abilityPoints = 27;
  maxAbilityPoints = 27;

  classes = dndClasses;
  races = dndRaces;
  backgrounds = dndBackgrounds;
  racialBonuses = dndRacialBonuses;
  archetype = classArchetypesWithSpells;
  fightingStyles = fightingStyles;
  skills = classSkills;

  characterCreationForm: FormGroup;
  filteredArchetypes: string[] = [];
  classAndBackgroundSkill: string[] = [];
  selectedSkills: string[] = [];
  currentEquipmentChoices: any[] = [];

  constructor(private fb: FormBuilder) {
    this.characterCreationForm = this.fb.group({
      name: ["", [Validators.required, Validators.minLength(1)]],
      race: ["", Validators.required],
      class: ["", Validators.required],
      background: ["", Validators.required],
      abilityScores: this.fb.group({
        strength: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
        dexterity: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
        constitution: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
        intelligence: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
        wisdom: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
        charisma: [8, [Validators.required, Validators.min(1), Validators.max(20)]],
      }),
      archeTypes: ["", Validators.required],
      skills: this.fb.array([], Validators.required),
      equipment_choices: this.fb.array([]),
      standart_equipment: this.fb.array([]),
    });

    // 监听职业选择变化,自动加载装备选项
    this.characterCreationForm.get('class')?.valueChanges.subscribe(selectedClass => {
      this.loadEquipmentChoices(selectedClass);
    });
  }

  get skillsFormArray(): FormArray {
    return this.characterCreationForm.get("skills") as FormArray;
  }

  get equipmentChoicesFormArray(): FormArray {
    return this.characterCreationForm.get("equipment_choices") as FormArray;
  }

  // 加载对应职业的装备选择项
  loadEquipmentChoices(selectedClass: string) {
    const classEquipment = equipment.find(equip => equip.class === selectedClass);
    if (!classEquipment) {
      this.equipmentChoicesFormArray.clear();
      this.currentEquipmentChoices = [];
      return;
    }

    this.currentEquipmentChoices = classEquipment.equipment_choices;
    this.equipmentChoicesFormArray.clear();

    // 为每个装备选项创建FormGroup
    classEquipment.equipment_choices.forEach(() => {
      this.equipmentChoicesFormArray.push(
        this.fb.group({
          selectedType: new FormControl(''),
          selectedValue: new FormControl('', Validators.required)
        })
      );
    });
  }

  // 处理装备选项选择,同步表单值
  onEquipmentOptionSelect(index: number, type: string, value: any) {
    const group = this.equipmentChoicesFormArray.at(index) as FormGroup;
    group.patchValue({
      selectedType: type,
      selectedValue: Array.isArray(value) ? value.join(', ') : value
    });
  }

  // 判断alternate是否禁用
  isAlternateDisabled(index: number): boolean {
    const group = this.equipmentChoicesFormArray.at(index) as FormGroup;
    return group.get('selectedType')?.value === 'item';
  }

  // 判断item是否禁用
  isItemDisabled(index: number): boolean {
    const group = this.equipmentChoicesFormArray.at(index) as FormGroup;
    return group.get('selectedType')?.value === 'alternate';
  }

  // 处理标准装备选择
  onEquipmentChange(event: any, equipment: any) {
    const standardEquipmentArray = this.characterCreationForm.get('standart_equipment') as FormArray;
    if (event.target.checked) {
      standardEquipmentArray.push(new FormControl(equipment));
    } else {
      const index = standardEquipmentArray.controls.findIndex(c => c.value === equipment);
      if (index !== -1) {
        standardEquipmentArray.removeAt(index);
      }
    }
  }

  nextStep() {
    this.selectArchetype();
    this.selectBackground();
    this.getSkills();
    if (this.currentStep < 4) {
      this.currentStep++;
    }
  }

  prevStep() {
    if (this.currentStep > 1) {
      this.currentStep--;
    }
  }

  onSubmit() {
    if (this.characterCreationForm.valid) {
      this.nextStep();
      this.calculateRacialBonus();
      console.log("Form data:", this.characterCreationForm.value);
    } else {
      console.log("请填写所有必填字段。", this.characterCreationForm);
    }
  }

  // 补充缺失的方法避免报错
  selectArchetype() {}
  selectBackground() {}
  getSkills() {}
  calculateRacialBonus() {}
}

步骤2:模板渲染与交互逻辑

修改creator.component.html,在装备步骤中渲染动态生成的装备选择项,实现互斥禁用:

<!-- 装备选择步骤 -->
<div *ngIf="currentStep === 3">
  <h2>装备选择</h2>

  <!-- 标准装备选择 -->
  <h3>标准装备</h3>
  <div *ngIf="characterCreationForm.get('class')?.value">
    <div *ngFor="let equip of equipment.find(e => e.class === characterCreationForm.get('class')?.value)?.standard_equipment">
      <label>
        <input type="checkbox" (change)="onEquipmentChange($event, equip)" />
        {{ equip }}
      </label>
    </div>
  </div>

  <!-- 动态装备选择项 -->
  <h3>可选装备</h3>
  <div *ngFor="let choice of currentEquipmentChoices; let i = index" class="equipment-choice">
    <p>请选择其一:</p>
    <label>
      <input type="radio" 
             name="equipment-{{i}}" 
             (change)="onEquipmentOptionSelect(i, 'item', choice.item)"
             [disabled]="isItemDisabled(i)" />
      {{ choice.item }}
    </label>

    <!-- 处理单个alternate选项 -->
    <div *ngIf="!Array.isArray(choice.alternate)">
      <label>
        <input type="radio" 
               name="equipment-{{i}}" 
               (change)="onEquipmentOptionSelect(i, 'alternate', choice.alternate)"
               [disabled]="isAlternateDisabled(i)" />
        {{ choice.alternate }}
      </label>
    </div>

    <!-- 处理数组类型的alternate选项 -->
    <div *ngIf="Array.isArray(choice.alternate)">
      <p>或选择以下之一:</p>
      <div *ngFor="let alt of choice.alternate">
        <label>
          <input type="radio" 
                 name="equipment-{{i}}" 
                 (change)="onEquipmentOptionSelect(i, 'alternate', alt)"
                 [disabled]="isAlternateDisabled(i)" />
          {{ alt }}
        </label>
      </div>
    </div>
  </div>

  <button (click)="prevStep()">上一步</button>
  <button (click)="nextStep()" [disabled]="!characterCreationForm.valid">下一步</button>
</div>

关键说明

  1. 动态表单生成:通过职业选择的valueChanges事件自动加载对应装备,为每个选项创建独立的FormGroup记录选择状态。
  2. 互斥禁用逻辑:通过isItemDisabled和isAlternateDisabled方法判断控件状态,确保用户只能选择item或alternate中的一类选项。
  3. 数组类型兼容:针对alternate为数组的场景(如Bard的武器选项),模板自动渲染多个单选按钮,适配不同装备结构。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 19:24:57