acgti-anime-persona-quiz

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ACGTI Anime Persona Quiz

ACGTI动漫人格测试

Skill by ara.so — Daily 2026 Skills collection.
ACGTI (ACG Type Indicator) is a purely client-side Vue 3 + TypeScript quiz that maps 39 seven-point Likert-scale questions onto four MBTI dimensions (E/I, S/N, T/F, J/P), matches the result to one of 8 anime archetypes, and then selects a specific anime character from a 40+ entry database. No backend, no user data collection — everything runs in the browser.

来自ara.so的技能项目——Daily 2026技能合集。
ACGTI(ACG类型指示器)是一个纯客户端的Vue 3 + TypeScript测试应用,它将39道七级李克特量表题目对应到MBTI的四个维度(E/I、S/N、T/F、J/P),把测试结果匹配到8种动漫原型之一,再从包含40+条条目的数据库中选出对应的特定动漫角色。无需后端,不收集用户数据——所有操作均在浏览器中完成。

Installation & Local Development

安装与本地开发

bash
undefined
bash
undefined

Clone the repo

克隆仓库

Install dependencies (Node 18+ recommended)

安装依赖(推荐Node 18+版本)

npm install
npm install

Start dev server (Vite, hot-reload)

启动开发服务器(Vite,支持热重载)

npm run dev
npm run dev

Type-check

类型检查

npx tsc --noEmit
npx tsc --noEmit

Production build → dist/

生产构建 → 输出到dist/目录

npm run build
npm run build

Preview production build locally

本地预览生产构建产物

npm run preview

The `dist/` folder uses `base: './'` (relative paths), so it deploys directly to any static host.

---
npm run preview

`dist/`文件夹使用`base: './'`(相对路径),因此可以直接部署到任何静态托管平台。

---

Project Architecture

项目架构

src/
├── components/          # Reusable UI (QuestionCard, ResultSummary, SharePoster …)
├── composables/
│   ├── useQuiz.ts       # Quiz state machine & answer logic
│   └── useShare.ts      # PNG poster export
├── data/                # ALL content lives here as JSON
│   ├── questions.json
│   ├── archetypes.json
│   ├── characters.json
│   ├── characterVisuals.json
│   └── characterProbabilities.json
├── pages/               # Vue route-level components
├── types/quiz.ts        # Shared TypeScript types
├── utils/
│   ├── quizEngine.ts    # Score → archetype → character pipeline
│   ├── characterVisuals.ts
│   ├── characterProbability.ts
│   └── storage.ts       # localStorage helpers
└── router/index.ts

src/
├── components/          # 可复用UI组件(QuestionCard、ResultSummary、SharePoster等)
├── composables/
│   ├── useQuiz.ts       # 测试状态机与答题逻辑
│   └── useShare.ts      # PNG海报导出功能
├── data/                # 所有内容以JSON格式存储于此
│   ├── questions.json
│   ├── archetypes.json
│   ├── characters.json
│   ├── characterVisuals.json
│   └── characterProbabilities.json
├── pages/               # Vue路由级组件
├── types/quiz.ts        # 共享TypeScript类型定义
├── utils/
│   ├── quizEngine.ts    # 得分→原型→角色的处理流程
│   ├── characterVisuals.ts
│   ├── characterProbability.ts
│   └── storage.ts       # localStorage工具函数
└── router/index.ts

Core Types (
src/types/quiz.ts
)

核心类型(
src/types/quiz.ts

Understanding these types is essential before touching any data file or engine logic.
typescript
// MBTI dimension keys
export type Dimension = 'EI' | 'SN' | 'TF' | 'JP';

// One question entry
export interface Question {
  id: number;
  text: string;
  dimension: Dimension;
  archetypeWeights: Record<string, number>; // archetype id → weight (-3..+3)
  tags?: string[];
}

// One of 8 archetypes
export interface Archetype {
  id: string;           // e.g. "glowing-protagonist"
  name: string;
  mbtiTypes: string[];  // e.g. ["ENFJ","ENFP"]
  description: string;
  strengths: string[];
  weaknesses: string[];
  color: string;        // hex
}

// Anime character entry
export interface Character {
  id: string;           // unique slug, becomes the "character code"
  name: string;
  series: string;
  mbtiType: string;     // e.g. "ENFJ"
  archetypeId: string;
  tags: string[];
  stats: {              // 0–100 six-axis radar
    energy: number;
    intuition: number;
    empathy: number;
    logic: number;
    order: number;
    chaos: number;
  };
}

// Visual theming per character
export interface CharacterVisual {
  characterId: string;
  portraitUrl: string;
  backgroundUrl: string;
  primaryColor: string;
  accentColor: string;
}

// Final computed result passed to ResultPage
export interface QuizResult {
  mbtiType: string;               // e.g. "INFP"
  dimensionScores: Record<Dimension, number>; // 50–100, direction-normalised
  archetypeId: string;
  characterId: string;
}

在修改任何数据文件或引擎逻辑之前,理解这些类型定义至关重要。
typescript
// MBTI维度键名
export type Dimension = 'EI' | 'SN' | 'TF' | 'JP';

单道题目条目
export interface Question {
  id: number;
  text: string;
  dimension: Dimension;
  archetypeWeights: Record<string, number>; // 原型ID → 权重值(-3..+3)
  tags?: string[];
}

// 8种原型之一
export interface Archetype {
  id: string;           // 示例:"glowing-protagonist"
  name: string;
  mbtiTypes: string[];  // 示例:["ENFJ","ENFP"]
  description: string;
  strengths: string[];
  weaknesses: string[];
  color: string;        // 十六进制颜色值
}

// 动漫角色条目
export interface Character {
  id: string;           // 唯一短标识,作为“角色代码”
  name: string;
  series: string;
  mbtiType: string;     // 示例:"ENFJ"
  archetypeId: string;
  tags: string[];
  stats: {              // 0–100的六轴雷达图数据
    energy: number;
    intuition: number;
    empathy: number;
    logic: number;
    order: number;
    chaos: number;
  };
}

// 角色专属视觉主题
export interface CharacterVisual {
  characterId: string;
  portraitUrl: string;
  backgroundUrl: string;
  primaryColor: string;
  accentColor: string;
}

// 传递给ResultPage的最终计算结果
export interface QuizResult {
  mbtiType: string;               // 示例:"INFP"
  dimensionScores: Record<Dimension, number>; // 50–100,方向归一化后的值
  archetypeId: string;
  characterId: string;
}

Scoring Engine (
src/utils/quizEngine.ts
)

评分引擎(
src/utils/quizEngine.ts

The engine is a pure function pipeline — ideal extension point.
typescript
import questions from '@/data/questions.json';
import archetypes from '@/data/archetypes.json';
import characters from '@/data/characters.json';
import type { Dimension, QuizResult } from '@/types/quiz';

type Answers = Record<number, number>; // questionId → -3..+3

/** Step 1: Sum raw signed scores per MBTI dimension */
function calcDimensionRaw(answers: Answers): Record<Dimension, number> {
  const raw: Record<Dimension, number> = { EI: 0, SN: 0, TF: 0, JP: 0 };
  for (const q of questions) {
    const val = answers[q.id] ?? 0;
    raw[q.dimension as Dimension] += val;
  }
  return raw;
}

/** Step 2: Normalise to 50–100 (50 = perfectly balanced) */
function normaliseDimension(raw: number, questionCount: number): number {
  const max = questionCount * 3;           // maximum possible absolute value
  const clamped = Math.max(-max, Math.min(max, raw));
  return Math.round(50 + (Math.abs(clamped) / max) * 50);
}

/** Step 3: Derive MBTI letter for one dimension */
function mbtiLetter(dimension: Dimension, raw: number): string {
  const positive: Record<Dimension, string> = { EI: 'E', SN: 'N', TF: 'T', JP: 'J' };
  const negative: Record<Dimension, string> = { EI: 'I', SN: 'S', TF: 'F', JP: 'P' };
  return raw >= 0 ? positive[dimension] : negative[dimension];
}

/** Full pipeline */
export function computeResult(answers: Answers): QuizResult {
  const dims: Dimension[] = ['EI', 'SN', 'TF', 'JP'];
  const raw = calcDimensionRaw(answers);

  // Count questions per dimension for normalisation
  const countPerDim = dims.reduce((acc, d) => {
    acc[d] = questions.filter(q => q.dimension === d).length;
    return acc;
  }, {} as Record<Dimension, number>);

  const dimensionScores = dims.reduce((acc, d) => {
    acc[d] = normaliseDimension(raw[d], countPerDim[d]);
    return acc;
  }, {} as Record<Dimension, number>);

  const mbtiType = dims.map(d => mbtiLetter(d, raw[d])).join('');

  // Match archetype (archetypes list mbtiTypes they cover)
  const archetype = archetypes.find(a => a.mbtiTypes.includes(mbtiType))
    ?? archetypes[0];

  // Pick best-fit character within archetype
  const candidates = characters.filter(c => c.archetypeId === archetype.id);
  // Default: first match; extendable with probability weighting
  const character = candidates[0];

  return {
    mbtiType,
    dimensionScores,
    archetypeId: archetype.id,
    characterId: character.id,
  };
}

该引擎是一个纯函数处理流程——是理想的扩展点。
typescript
import questions from '@/data/questions.json';
import archetypes from '@/data/archetypes.json';
import characters from '@/data/characters.json';
import type { Dimension, QuizResult } from '@/types/quiz';

type Answers = Record<number, number>; // 题目ID → -3..+3的答案值

/** 步骤1:计算每个MBTI维度的原始带符号得分 */
function calcDimensionRaw(answers: Answers): Record<Dimension, number> {
  const raw: Record<Dimension, number> = { EI: 0, SN: 0, TF: 0, JP: 0 };
  for (const q of questions) {
    const val = answers[q.id] ?? 0;
    raw[q.dimension as Dimension] += val;
  }
  return raw;
}

/** 步骤2:将得分归一化到50–100区间(50代表完全平衡) */
function normaliseDimension(raw: number, questionCount: number): number {
  const max = questionCount * 3;           // 最大可能的绝对值
  const clamped = Math.max(-max, Math.min(max, raw));
  return Math.round(50 + (Math.abs(clamped) / max) * 50);
}

/** 步骤3:推导单个维度对应的MBTI字母 */
function mbtiLetter(dimension: Dimension, raw: number): string {
  const positive: Record<Dimension, string> = { EI: 'E', SN: 'N', TF: 'T', JP: 'J' };
  const negative: Record<Dimension, string> = { EI: 'I', SN: 'S', TF: 'F', JP: 'P' };
  return raw >= 0 ? positive[dimension] : negative[dimension];
}

/** 完整处理流程 */
export function computeResult(answers: Answers): QuizResult {
  const dims: Dimension[] = ['EI', 'SN', 'TF', 'JP'];
  const raw = calcDimensionRaw(answers);

  // 统计每个维度的题目数量,用于归一化
  const countPerDim = dims.reduce((acc, d) => {
    acc[d] = questions.filter(q => q.dimension === d).length;
    return acc;
  }, {} as Record<Dimension, number>);

  const dimensionScores = dims.reduce((acc, d) => {
    acc[d] = normaliseDimension(raw[d], countPerDim[d]);
    return acc;
  }, {} as Record<Dimension, number>);

  const mbtiType = dims.map(d => mbtiLetter(d, raw[d])).join('');

  // 匹配原型(原型列表包含其覆盖的mbtiTypes)
  const archetype = archetypes.find(a => a.mbtiTypes.includes(mbtiType))
    ?? archetypes[0];

  // 在对应原型中选择最匹配的角色
  const candidates = characters.filter(c => c.archetypeId === archetype.id);
  // 默认选择第一个匹配项;可扩展为基于概率权重的选择
  const character = candidates[0];

  return {
    mbtiType,
    dimensionScores,
    archetypeId: archetype.id,
    characterId: character.id,
  };
}

Adding a New Character

添加新角色

Edit
src/data/characters.json
— append one object following the schema:
json
{
  "id": "hatsune-miku",
  "name": "初音ミク",
  "series": "VOCALOID",
  "mbtiType": "ENFP",
  "archetypeId": "chaotic-spark",
  "tags": ["vocaloid", "energetic", "creative"],
  "stats": {
    "energy": 90,
    "intuition": 85,
    "empathy": 75,
    "logic": 50,
    "order": 40,
    "chaos": 80
  }
}
Then add the matching visual entry to
src/data/characterVisuals.json
:
json
{
  "characterId": "hatsune-miku",
  "portraitUrl": "https://your-cdn.example.com/miku-portrait.webp",
  "backgroundUrl": "https://your-cdn.example.com/miku-bg.webp",
  "primaryColor": "#39C5BB",
  "accentColor": "#86EFDF"
}
And an optional prior probability in
src/data/characterProbabilities.json
:
json
{
  "characterId": "hatsune-miku",
  "baseProbability": 0.15
}
Rules:
id
must be unique and kebab-case.
mbtiType
must be one of the 16 standard types.
archetypeId
must match an
id
in
archetypes.json
.
stats
values are integers 0–100.

编辑
src/data/characters.json
——按照以下模式追加一个对象:
json
{
  "id": "hatsune-miku",
  "name": "初音ミク",
  "series": "VOCALOID",
  "mbtiType": "ENFP",
  "archetypeId": "chaotic-spark",
  "tags": ["vocaloid", "energetic", "creative"],
  "stats": {
    "energy": 90,
    "intuition": 85,
    "empathy": 75,
    "logic": 50,
    "order": 40,
    "chaos": 80
  }
}
然后在
src/data/characterVisuals.json
中添加对应的视觉条目:
json
{
  "characterId": "hatsune-miku",
  "portraitUrl": "https://your-cdn.example.com/miku-portrait.webp",
  "backgroundUrl": "https://your-cdn.example.com/miku-bg.webp",
  "primaryColor": "#39C5BB",
  "accentColor": "#86EFDF"
}
还可以在
src/data/characterProbabilities.json
中添加可选的基础概率:
json
{
  "characterId": "hatsune-miku",
  "baseProbability": 0.15
}
规则:
id
必须唯一且采用短横线命名法(kebab-case)。
mbtiType
必须是16种标准MBTI类型之一。
archetypeId
必须与
archetypes.json
中的某个
id
完全匹配。
stats
的值为0–100之间的整数。

Adding New Quiz Questions

添加新测试题目

Edit
src/data/questions.json
— append to the array:
json
{
  "id": 40,
  "text": "在一个陌生的聚会上,你更倾向于主动找人搭话还是等别人来找你?",
  "dimension": "EI",
  "archetypeWeights": {
    "glowing-protagonist": 2,
    "ice-observer": -2,
    "oath-captain": 1,
    "agile-spinner": 1,
    "gentle-healer": 0,
    "shadow-strategist": -1,
    "chaotic-spark": 2,
    "moonlit-guardian": -1
  },
  "tags": ["social", "introvert-extrovert"]
}
Guidelines:
  • id
    must be unique and increment sequentially.
  • dimension
    is one of
    "EI" | "SN" | "TF" | "JP"
    .
  • archetypeWeights
    keys must match all 8 archetype
    id
    values; weights range -3 to +3.
  • Positive weight = answer "strongly agree" nudges toward that archetype.
  • Keep question text in Chinese (Simplified) to match existing copy.

编辑
src/data/questions.json
——向数组中追加题目:
json
{
  "id": 40,
  "text": "在一个陌生的聚会上,你更倾向于主动找人搭话还是等别人来找你?",
  "dimension": "EI",
  "archetypeWeights": {
    "glowing-protagonist": 2,
    "ice-observer": -2,
    "oath-captain": 1,
    "agile-spinner": 1,
    "gentle-healer": 0,
    "shadow-strategist": -1,
    "chaotic-spark": 2,
    "moonlit-guardian": -1
  },
  "tags": ["social", "introvert-extrovert"]
}
指南:
  • id
    必须唯一且按顺序递增。
  • dimension
    必须是
    "EI" | "SN" | "TF" | "JP"
    之一。
  • archetypeWeights
    的键必须与全部8种原型的
    id
    值匹配;权重范围为**-3至+3**。
  • 正权重=选择“非常同意”会向该原型倾斜。
  • 题目文本请使用简体中文,与现有内容保持一致。

Modifying Archetypes (
src/data/archetypes.json
)

修改原型(
src/data/archetypes.json

json
{
  "id": "glowing-protagonist",
  "name": "发光主角位",
  "mbtiTypes": ["ENFJ", "ENFP"],
  "description": "天生的领袖与感召者,能点燃周围人的热情。",
  "strengths": ["感召力强", "共情深刻", "行动力高"],
  "weaknesses": ["容易过度承担", "情绪波动大"],
  "color": "#FF6B6B"
}
Each MBTI type (16 total) should appear in exactly one archetype's
mbtiTypes
array. The engine uses a first-match lookup — gaps cause a fallback to
archetypes[0]
.

json
{
  "id": "glowing-protagonist",
  "name": "发光主角位",
  "mbtiTypes": ["ENFJ", "ENFP"],
  "description": "天生的领袖与感召者,能点燃周围人的热情。",
  "strengths": ["感召力强", "共情深刻", "行动力高"],
  "weaknesses": ["容易过度承担", "情绪波动大"],
  "color": "#FF6B6B"
}
每种MBTI类型(共16种)应恰好出现在一个原型的
mbtiTypes
数组中。引擎采用首次匹配查找机制——若存在遗漏,会回退到
archetypes[0]

useQuiz
Composable (state management)

useQuiz
组合式函数(状态管理)

typescript
// src/composables/useQuiz.ts — typical usage from a page component
import { useQuiz } from '@/composables/useQuiz';

const {
  currentQuestion,   // Ref<Question>
  currentIndex,      // Ref<number>
  totalQuestions,    // number (39)
  progress,          // ComputedRef<number> 0–100
  answer,            // (value: number) => void  — records -3..+3 and advances
  goBack,            // () => void
  result,            // Ref<QuizResult | null>
  isComplete,        // ComputedRef<boolean>
  resetQuiz,         // () => void
} = useQuiz();

typescript
// src/composables/useQuiz.ts —— 页面组件中的典型用法
import { useQuiz } from '@/composables/useQuiz';

const {
  currentQuestion,   // Ref<Question> 当前题目
  currentIndex,      // Ref<number> 当前题目索引
  totalQuestions,    // number 题目总数(39道)
  progress,          // ComputedRef<number> 完成进度0–100
  answer,            // (value: number) => void 记录答案(-3..+3)并跳转到下一题
  goBack,            // () => void 返回上一题
  result,            // Ref<QuizResult | null> 测试结果
  isComplete,        // ComputedRef<boolean> 测试是否完成
  resetQuiz,         // () => void 重置测试
} = useQuiz();

Share / Export Poster (
useShare
)

分享/导出海报(
useShare

typescript
import { useShare } from '@/composables/useShare';

const { exportPNG, shareNative } = useShare();

// exportPNG wraps html2canvas on the #share-poster element
await exportPNG('#share-poster', 'my-acgti-result.png');

// shareNative uses Web Share API with fallback to clipboard copy
await shareNative({
  title: 'My ACGTI Result',
  text: `I got ${result.value?.characterId}!`,
  url: 'https://acgti.tianxingleo.top',
});

typescript
import { useShare } from '@/composables/useShare';

const { exportPNG, shareNative } = useShare();

// exportPNG基于html2canvas处理#share-poster元素
await exportPNG('#share-poster', 'my-acgti-result.png');

// shareNative使用Web Share API, fallback到剪贴板复制
await shareNative({
  title: 'My ACGTI Result',
  text: `I got ${result.value?.characterId}!`,
  url: 'https://acgti.tianxingleo.top',
});

Routing (
src/router/index.ts
)

路由(
src/router/index.ts

typescript
// Five named routes
const routes = [
  { path: '/',          name: 'home',       component: HomePage },
  { path: '/intro',     name: 'intro',      component: IntroPage },
  { path: '/quiz',      name: 'quiz',       component: QuizPage },
  { path: '/result',    name: 'result',     component: ResultPage },
  { path: '/characters',name: 'characters', component: CharactersPage },
  { path: '/about',     name: 'about',      component: AboutPage },
];
Navigate programmatically after quiz completion:
typescript
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ name: 'result' });

typescript
// 五个命名路由
const routes = [
  { path: '/',          name: 'home',       component: HomePage },
  { path: '/intro',     name: 'intro',      component: IntroPage },
  { path: '/quiz',      name: 'quiz',       component: QuizPage },
  { path: '/result',    name: 'result',     component: ResultPage },
  { path: '/characters',name: 'characters', component: CharactersPage },
  { path: '/about',     name: 'about',      component: AboutPage },
];
测试完成后编程式导航:
typescript
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ name: 'result' });

localStorage Utilities (
src/utils/storage.ts
)

localStorage工具函数(
src/utils/storage.ts

typescript
import { saveResult, loadResult, clearResult } from '@/utils/storage';
import type { QuizResult } from '@/types/quiz';

// Persist result across page refreshes
saveResult(result);

// Restore on ResultPage mount
const saved: QuizResult | null = loadResult();

// Reset for retake
clearResult();

typescript
import { saveResult, loadResult, clearResult } from '@/utils/storage';
import type { QuizResult } from '@/types/quiz';

// 持久化结果,页面刷新后依然保留
saveResult(result);

// 在ResultPage挂载时恢复结果
const saved: QuizResult | null = loadResult();

// 重置结果以重新测试
clearResult();

Deployment

部署

Cloudflare Pages (recommended)

Cloudflare Pages(推荐)

  1. Connect GitHub repo → Cloudflare Pages dashboard.
  2. Build command:
    npm run build
  3. Build output directory:
    dist
  4. No environment variables required (pure frontend).
  1. 连接GitHub仓库到Cloudflare Pages控制台。
  2. 构建命令:
    npm run build
  3. 构建输出目录:
    dist
  4. 无需环境变量(纯前端应用)。

GitHub Actions CI

GitHub Actions持续集成

The repo includes a workflow that runs on every push to
main
/
dev
and on PRs:
yaml
undefined
仓库包含一个工作流,会在每次推送到
main
/
dev
分支以及PR时运行:
yaml
undefined

.github/workflows/ci.yml (existing)

.github/workflows/ci.yml(已存在)

  • run: npm ci
  • run: npm run build
undefined
  • run: npm ci
  • run: npm run build
undefined

Release a version

发布版本

bash
git tag v1.2.0
git push origin v1.2.0
bash
git tag v1.2.0
git push origin v1.2.0

GitHub Actions auto-builds dist/, zips it, creates a Release

GitHub Actions会自动构建dist/,打包并创建Release


---

---

Common Patterns & Tips

通用模式与技巧

Filtering characters by archetype in a component

在组件中按原型筛选角色

typescript
import characters from '@/data/characters.json';
import type { Character } from '@/types/quiz';

const archetypeId = 'glowing-protagonist';
const subset: Character[] = characters.filter(
  (c) => c.archetypeId === archetypeId
);
typescript
import characters from '@/data/characters.json';
import type { Character } from '@/types/quiz';

const archetypeId = 'glowing-protagonist';
const subset: Character[] = characters.filter(
  (c) => c.archetypeId === archetypeId
);

Accessing visuals by character ID

通过角色ID获取视觉资源

typescript
import visuals from '@/data/characterVisuals.json';
import { enrichCharacterVisuals } from '@/utils/characterVisuals';

const enriched = enrichCharacterVisuals(characters, visuals);
// enriched[i] = { ...Character, ...CharacterVisual }
typescript
import visuals from '@/data/characterVisuals.json';
import { enrichCharacterVisuals } from '@/utils/characterVisuals';

const enriched = enrichCharacterVisuals(characters, visuals);
// enriched[i] = { ...Character, ...CharacterVisual }

Reactive dimension label (E vs I, etc.)

响应式维度标签(E vs I等)

typescript
function dimensionLabel(dim: Dimension, score: number): string {
  const labels: Record<Dimension, [string, string]> = {
    EI: ['E 外向', 'I 内向'],
    SN: ['N 直觉', 'S 实感'],
    TF: ['T 思考', 'F 情感'],
    JP: ['J 判断', 'P 知觉'],
  };
  // score > 50 means positive pole; score === 50 means balanced (show both)
  return score >= 50 ? labels[dim][0] : labels[dim][1];
}

typescript
function dimensionLabel(dim: Dimension, score: number): string {
  const labels: Record<Dimension, [string, string]> = {
    EI: ['E 外向', 'I 内向'],
    SN: ['N 直觉', 'S 实感'],
    TF: ['T 思考', 'F 情感'],
    JP: ['J 判断', 'P 知觉'],
  };
  // 得分>50代表正向维度;得分===50代表平衡状态(显示两者)
  return score >= 50 ? labels[dim][0] : labels[dim][1];
}

Troubleshooting

故障排查

SymptomLikely causeFix
npm run build
fails with type errors
New JSON data doesn't match typesRun
npx tsc --noEmit
and fix mismatches in
src/types/quiz.ts
Character not appearing in results
archetypeId
mismatch between
characters.json
and
archetypes.json
Ensure
archetypeId
exactly matches an archetype
id
New question not affecting scores
dimension
key is wrong
Must be exactly
"EI"
,
"SN"
,
"TF"
, or
"JP"
Poster export is blank
html2canvas
can't load cross-origin images
Host character images on a CORS-enabled CDN or use base64 data URIs
Route returns 404 on Cloudflare PagesSPA fallback not configuredAdd
_redirects
file:
/* /index.html 200
in
public/
Dev server errors on
@/
imports
Vite alias not resolvingCheck
vite.config.ts
has
resolve: { alias: { '@': '/src' } }
症状可能原因修复方法
npm run build
因类型错误失败
新增的JSON数据与类型定义不匹配运行
npx tsc --noEmit
并修复
src/types/quiz.ts
中的不匹配项
角色未出现在测试结果中
characters.json
archetypes.json
中的
archetypeId
不匹配
确保
archetypeId
与某个原型的
id
完全一致
新题目不影响得分
dimension
键值错误
必须是
"EI"
"SN"
"TF"
"JP"
之一
海报导出为空
html2canvas
无法加载跨域图片
将角色图片托管在支持CORS的CDN上,或使用base64数据URI
Cloudflare Pages上路由返回404未配置SPA回退规则
public/
目录添加
_redirects
文件:
/* /index.html 200
开发服务器在
@/
导入时出错
Vite别名未解析检查
vite.config.ts
中是否包含
resolve: { alias: { '@': '/src' } }