【从零构建多智能体 03】用 Codex 搭建 Echo English AI 前端工程:Vite 初始化、移动端骨架、路由布局与 Mock 数据

【从零构建多智能体 03】用 Codex 搭建 Echo English AI 前端工程:Vite 初始化、移动端骨架、路由布局与 Mock 数据

系列总名称:从零构建多智能体:Harness & Hermes 项目实战系列

项目方向:Echo English AI:AI 英语陪练学习站

本文是系列第 3 篇。上一篇我们先用几个独立 HTML 示例理解了 Vue3.5 的响应式、列表渲染、事件绑定和组件通信。这一篇开始进入真实工程:我们不是一行一行手敲页面,而是学习如何把需求、UI 设计图和开发规范交给 Codex,让 AI 辅助我们搭建一个移动端英语学习站的前端骨架。

一、这一篇要完成什么

这一篇不是单纯讲 Vite 命令,也不是简单生成一个默认 Vue 页面。

我们要做的是把 Echo English AI 的前端项目从 0 搭起来,并且形成后续文章都能继续扩展的工程基础。

本文完成后,项目会具备这些基础能力:

目标 作用
Vite + Vue3.5 + TypeScript 项目 作为前端工程底座
移动端页面方向 项目面向手机网页,不是传统后台管理系统
工程目录结构 后续功能不会全部堆在 App.vue
路由系统 支持首页、学习室、口语、单词、阅读、写作等页面切换
全局布局 顶部导航、底部 Tab、内容区保持统一
占位页面 先把页面结构搭起来,后面逐步填充真实功能
Mock 数据 没有后端时,也能模拟接口数据做页面渲染
Codex 工作流 学会如何让 AI 拆任务、写代码、修错误、做验收

二、项目蓝图:Echo English AI 要做什么

在正式写代码之前,先把项目目标说清楚。

这一套教程最终要完成的是:

text 复制代码
Echo English AI:AI 英语陪练学习站

它是一个面向手机网页的英语学习项目,用户不需要安装 App,只要用浏览器打开网页,就可以进行英语学习和 AI 陪练。

后续我们会逐步完成这些页面:

页面 功能定位
首页 今日学习入口、学习进度、快捷功能
学习室 AI 对话学习、场景练习、学习历史
口语陪练 录音练习、发音评分、句子反馈
单词记忆 单词卡片、熟悉度标记、复习进度
阅读训练 上传文本、词汇解析、长难句分析
写作批改 作文提交、语法纠错、表达优化

这一篇先不追求把所有页面做完,而是先把项目的前端骨架搭起来。

你可以把它理解成盖房子的第一步:

text 复制代码
先打地基
再搭框架
最后再装修每个房间

前端项目也是一样。

我们先完成 Vite 工程、路由、全局布局、页面占位和 Mock 数据,后面再一篇一篇把首页、学习室、口语、单词、阅读、写作做细。

三、AI 辅助开发的核心闭环

使用 Codex 开发项目时,不建议一上来就说:

text 复制代码
帮我写一个完整网站

这样的提示太大,AI 很容易一次性生成大量代码,但你看不懂,也不好改。

更好的开发闭环是:

text 复制代码
需求整理
-> UI 方案
-> 页面拆分
-> 组件拆分
-> 生成代码
-> 本地运行
-> 人工检查
-> 继续让 Codex 修正

可以把它理解成 4 个动作:

动作 说明
拆结构 先让 Codex 把页面拆成模块,而不是直接写代码
生成骨架 先生成静态页面,不急着接真实接口
做交互 逐个补充按钮、表单、列表、状态变化
验收优化 运行项目,发现问题,再让 Codex 按错误修复

这个顺序很重要。

如果不先拆结构,后面代码很容易变成一个超大的 App.vue;如果不先搭静态骨架,后面接接口时就会同时面对布局、数据、状态、错误处理,学习成本很高。

四、Vite 是什么?为什么前端项目要用它

在创建 Vue 项目前,先理解 Vite 的定位。

Vite 可以简单理解成:

text 复制代码
前端项目创建工具 + 本地开发服务器 + 打包构建工具

它主要解决几个问题:

问题 Vite 的作用
不知道怎么创建标准 Vue 项目 用命令快速生成项目骨架
项目启动慢 开发服务器启动速度快
修改代码后刷新慢 支持 HMR,也就是热更新
最后怎么上线 提供生产环境构建能力

1. 什么是本地开发服务器

当你运行:

bash 复制代码
npm run dev

Vite 会在本机启动一个开发服务器。

浏览器可以通过类似下面的地址访问项目:

text 复制代码
http://localhost:5173/

这个页面不是一个普通 HTML 文件直接打开,而是由 Vite 帮你处理了 Vue、TypeScript、样式、模块导入等内容。

2. 什么是 HMR

HMR 的完整名称是 Hot Module Replacement,中文一般叫"热更新"。

意思是:

text 复制代码
你修改了某个 Vue 文件
浏览器里的页面可以快速更新
不用每次手动重启项目

这对前端开发非常重要。

比如你改了首页标题,从:

text 复制代码
Echo English AI

改成:

text 复制代码
每天 15 分钟,用 AI 提升英语能力

保存文件后,浏览器通常会自动刷新或局部更新。

五、创建 Vite + Vue3.5 项目

1. 检查 Node.js 和 npm

先打开 PowerShell 或终端,输入:

bash 复制代码
node -v
npm -v

你看到类似下面的版本号就说明环境已经安装:

text 复制代码
v24.17.0
11.13.0

一般来说,Vue + Vite 项目建议使用较新的 Node.js。本文中你的电脑已经是 v24.17.0,可以继续。

2. Windows 路径建议

这里有一个很容易踩坑的点。

如果你的项目路径中包含:

text 复制代码
&
中文空格
非常长的目录
特殊符号

某些 npm 脚本在 Windows 下可能会解析出错。

比如路径里有:

text 复制代码
Harness&Hermes

& 在命令行里有特殊含义,可能导致 npm run dev 找不到 Vite。

所以建议正式开发时,把项目放到一个更干净的目录,例如:

text 复制代码
C:\ai-projects\echo-english-ai

如果只是学习,也可以继续在当前目录下操作;但如果遇到奇怪的 npm 报错,第一优先级就是把项目移动到短路径。

3. PowerShell 无法运行 npm 的解决方式

如果你输入:

bash 复制代码
npm create vite@latest

出现类似错误:

text 复制代码
无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本

这不是 Vite 的问题,而是 PowerShell 的脚本执行策略限制。

最简单的处理方式是使用:

bash 复制代码
npm.cmd create vite@latest

也就是把 npm 换成 npm.cmd

如果只是创建项目,这个方式最省事。

4. 创建项目

执行:

bash 复制代码
npm.cmd create vite@latest

交互选择可以这样填:

text 复制代码
Project name: agent-frontend-project
Select a framework: Vue
Select a variant: TypeScript
Install with npm and start now? Yes

六、认识 Vite 默认项目结构

创建完成后,目录大概长这样:

text 复制代码
echo-english-ai/
├─ index.html
├─ package.json
├─ tsconfig.json
├─ tsconfig.app.json
├─ tsconfig.node.json
├─ vite.config.ts
├─ public/
└─ src/
   ├─ App.vue
   ├─ main.ts
   ├─ style.css
   ├─ assets/
   └─ components/

每个文件先有一个基本印象:

文件或目录 作用
index.html 浏览器最先加载的 HTML 入口
package.json 项目依赖、脚本命令都在这里
vite.config.ts Vite 工程配置
src/main.ts Vue 应用入口
src/App.vue 根组件
src/components/ 放可复用组件
src/assets/ 放图片、图标等静态资源
public/ 放无需打包处理的静态文件

1. package.json 重点看 scripts

默认项目里通常会有:

json 复制代码
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  }
}

解释一下:

text 复制代码
npm run dev      启动本地开发服务器
npm run build    先做 TypeScript 检查,再打包生产代码
npm run preview  本地预览打包后的生产版本

开发时最常用的是:

bash 复制代码
npm run dev

写完一个阶段后,建议执行:

bash 复制代码
npm run build

因为有些错误在开发服务器里不一定立刻暴露,但构建时会暴露出来。

七、让 Codex 先帮我们配置工程

默认 Vite 配置很简单:

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
})

这能跑,但还不够适合真实项目。

真实项目通常需要:

text 复制代码
路径别名
开发服务器端口
局域网访问
接口代理
环境变量

1. 给 Codex 的提示词

可以这样对 Codex 说:

text 复制代码
请帮我配置当前 Vue3.5 + Vite + TypeScript 项目的工程基础:
1. 配置 @ 指向 src 目录;
2. 开发服务器端口使用 8080;
3. 允许局域网访问;
4. 配置 /api 代理到环境变量 VITE_API_BASE_URL;
5. 新增 .env.development 和 .env.production 示例;
6. 修改 tsconfig,让 TypeScript 能识别 @ 路径别名;
7. 每个关键配置后面写中文注释,方便理解。

注意这个提示不是"帮我写完整项目",而是非常明确地列出任务。

Codex 更适合处理这种边界清晰的请求。

2. 推荐的 vite.config.ts

可以改成下面这样:

ts 复制代码
import { fileURLToPath, URL } from 'node:url' // 从 Node.js 内置模块中导入路径转换工具
import { defineConfig, loadEnv } from 'vite' // defineConfig 用于获得配置类型提示,loadEnv 用于读取环境变量
import vue from '@vitejs/plugin-vue' // 导入 Vue 插件,让 Vite 能解析 .vue 单文件组件

export default defineConfig(({ mode }) => { // mode 表示当前运行模式,例如 development 或 production
  const env = loadEnv(mode, process.cwd(), '') // 根据当前模式读取对应的 .env 文件

  return { // 返回真正的 Vite 配置对象
    plugins: [vue()], // 启用 Vue 插件

    resolve: { // 配置模块解析规则
      alias: { // 配置路径别名
        '@': fileURLToPath(new URL('./src', import.meta.url)), // 让 @ 代表 src 目录
      },
    },

    server: { // 本地开发服务器配置
      host: '0.0.0.0', // 允许局域网设备访问,例如手机访问电脑上的开发页面
      port: 8080, // 固定开发服务器端口,避免每次端口变化

      proxy: { // 配置接口代理
        '/api': { // 当前端请求 /api 开头的地址时,交给代理处理
          target: env.VITE_API_BASE_URL || 'http://localhost:3000', // 后端接口地址,优先读取环境变量
          changeOrigin: true, // 修改请求来源,避免部分后端拒绝跨域请求
          rewrite: (path) => path.replace(/^\/api/, ''), // 把 /api 前缀去掉后再转发给后端
        },
      },
    },
  }
})

3. 为什么要配置 @ 别名

没有别名时,导入文件可能要这样写:

ts 复制代码
import HomeView from '../../views/HomeView.vue'

层级一多,就很难看。

配置 @ 后,可以写成:

ts 复制代码
import HomeView from '@/views/HomeView.vue'

这里的 @ 不是 Vue 固定语法,而是我们自己在 Vite 里配置的别名。

它的意思是:

text 复制代码
@ 等于 src 目录

4. 环境变量文件

新建 .env.development

ini 复制代码
# 开发环境后端接口地址
VITE_API_BASE_URL=http://localhost:3000

新建 .env.production

ini 复制代码
# 生产环境后端接口地址
VITE_API_BASE_URL=https://api.echoenglish.example.com

注意:Vite 中想让前端代码读取环境变量,变量名必须以:

text 复制代码
VITE_

开头。

例如:

ts 复制代码
console.log(import.meta.env.VITE_API_BASE_URL)

如果写成:

ini 复制代码
API_BASE_URL=http://localhost:3000

前端默认读不到。

5. 让 TypeScript 识别 @

如果只改 vite.config.ts,运行时可能能找到文件,但 TypeScript 仍然可能报错。

所以还要检查 tsconfig.app.json,加入:

json 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

解释一下:

配置 作用
baseUrl 告诉 TypeScript 从哪里开始解析路径
paths 告诉 TypeScript @/* 对应 src/*

如果你遇到:

text 复制代码
Cannot find module '@/components/xxx.vue'

先检查这两处:

text 复制代码
vite.config.ts 是否配置 alias
tsconfig.app.json 是否配置 paths

八、给项目准备一份 AI 编码规范

用 Codex 开发项目时,最好不要每次都临时告诉它"组件怎么命名""目录怎么放""样式怎么写"。

更好的方式是先准备一份项目规范文件。

例如新建:

text 复制代码
docs/rules/agent-frontend-rule.md

如果你的编辑器或 AI 工具支持 rules 目录,也可以放到对应规则目录中。

重点不是目录名称,而是这份规范后续要被 Codex 读取或引用。

1. 给 Codex 的提示词

text 复制代码
帮我生成一个这个项目的编码规范,然后放到合适的位置,保证在每次生成代码时都按照这个编码规范

AGENTS.md 就是给 Codex 这类代码生成工具看的项目级规则。

后续只要在 agent-frontend-project 这个项目范围内生成或修改代码,就会先按 AGENTS.md 的要求执行,并参考 docs/coding-standards.md。

九、规划 Echo English AI 的目录结构

默认 Vite 项目很简单,但我们后面要做完整网站,建议提前整理目录。

目标结构:

text 复制代码
src/
├─ api/                 # 请求函数,后面接真实后端时也放这里
├─ assets/              # 图片、图标等静态资源
├─ components/          # 通用组件
├─ layouts/             # 全局布局组件
├─ mock/                # Mock 数据
├─ router/              # 路由配置
├─ styles/              # 全局样式
├─ types/               # TypeScript 类型
├─ views/               # 页面组件
├─ App.vue              # 根组件
└─ main.ts              # 应用入口

给 Codex 的提示词生成整体项目框架

这个ui图片是chatgpt生成的

text 复制代码
这地方可以先使用codex的plan模式,检查后让codex生成代码
1. 把ui图片发给codex
2. 提示词:需要你帮我拆分这个页面结构,进行组件化拆分,并列出交互逻辑清单。然后帮我高度还原这个页面,只预留轻量化交互功能,组件使用element plus,样式使用scss预编译

使用codex的计划模式,这里我回答了的问题可以作为参考:

  1. 这次你希望高度还原的是哪一种页面形态?真实手机端应用
  2. 首页和建议卡片里的机器人视觉,你希望怎么处理?生成新素材 (Recommended)
  3. 6 个手机页面之间的跳转,v1 你希望采用哪种方式?Vue Router

十、使用 Codex 计划模式生成项目骨架

前面我们已经把 Vite、路径别名、环境变量、代理和编码规范准备好了。

接下来就可以开始让 Codex 进入真正的页面开发阶段。

这一步我使用的是 Codex 的计划模式。计划模式的好处是:Codex 不会马上写代码,而是先问几个关键问题,确认项目方向后再生成方案。

这次我回答了 3 个关键问题:

Codex 问题 我的选择 为什么这样选
这次希望高度还原哪一种页面形态? 真实手机端应用 我们的项目是移动端网页,页面应该更像手机 App,而不是 PC 后台
首页和建议卡片里的机器人视觉怎么处理? 生成新素材 让项目有自己的视觉资产,后续教程也更统一
6 个手机页面之间的跳转怎么做? Vue Router 这是 Vue 单页应用最常用的页面跳转方案

确认计划后,Codex 在项目里生成了一套移动端前端骨架。

项目目录是:

text 复制代码
C:\echo english ai\agent-frontend-project

当前核心文件结构如下:

text 复制代码
agent-frontend-project/
├─ AGENTS.md
├─ docs/
│  └─ coding-standards.md
├─ .env.development
├─ .env.production
├─ package.json
├─ vite.config.ts
├─ tsconfig.app.json
└─ src/
   ├─ main.ts
   ├─ App.vue
   ├─ router/
   │  └─ index.ts
   ├─ data/
   │  └─ mock.ts
   ├─ types/
   │  └─ app.ts
   ├─ styles/
   │  └─ main.scss
   ├─ assets/
   │  ├─ hero.png
   │  └─ robot-mascot.png
   ├─ views/
   │  ├─ HomePage.vue
   │  ├─ LearningRoomPage.vue
   │  ├─ SpeakingPracticePage.vue
   │  ├─ VocabularyPage.vue
   │  ├─ ReadingTrainingPage.vue
   │  └─ WritingReviewPage.vue
   └─ components/
      ├─ common/
      ├─ layout/
      ├─ home/
      ├─ learning/
      ├─ speaking/
      ├─ vocabulary/
      ├─ reading/
      └─ writing/

先不要被文件数量吓到。

这套结构其实很清晰,可以分成 7 类:

类型 目录或文件 作用
项目规则 AGENTS.mddocs/coding-standards.md 告诉 Codex 后续生成代码时遵守哪些规范
工程配置 vite.config.tstsconfig.app.json.env.* 配置端口、别名、代理、环境变量、TypeScript
应用入口 src/main.tssrc/App.vue 负责启动 Vue 应用和挂载路由
页面路由 src/router/index.ts 定义 URL 和页面组件之间的关系
模拟数据 src/data/mock.ts 暂时代替后端接口,给页面提供演示数据
类型声明 src/types/app.ts 定义数据长什么样,提升 TypeScript 提示和安全性
页面与组件 src/viewssrc/components 组成 6 个手机端页面

这一节开始,我们不再用"想象中的示例代码",而是直接按照当前生成出来的代码讲解。

十一、先看项目执行顺序

很多新手拿到 Vue 项目后,第一反应是:

text 复制代码
这么多文件,浏览器到底先执行哪一个?

当前项目的运行链路是这样的:

text 复制代码
index.html
  ↓
src/main.ts
  ↓
src/App.vue
  ↓
src/router/index.ts
  ↓
src/views/某个页面.vue
  ↓
src/components/页面用到的组件.vue
  ↓
src/data/mock.ts 提供展示数据
  ↓
src/styles/main.scss 控制全局样式

再换成更贴近代码的流程:

text 复制代码
1. 浏览器打开 http://localhost:8080
2. Vite 返回 index.html
3. index.html 加载 /src/main.ts
4. main.ts 创建 Vue 应用
5. main.ts 注册 Vue Router 和 Element Plus
6. App.vue 显示 router-view
7. router-view 根据当前 URL 渲染对应页面
8. 页面组件再调用自己的子组件和 mock 数据

1. src/main.ts:应用真正的启动入口

当前 main.ts 是这样写的:

ts 复制代码
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import '@/styles/main.scss'
import App from './App.vue'
import { router } from '@/router'

createApp(App).use(router).use(ElementPlus).mount('#app')

逐行解释:

ts 复制代码
import { createApp } from 'vue'

从 Vue 中导入 createApp,它用来创建 Vue 应用。

ts 复制代码
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

导入 Element Plus 组件库和它的默认样式。

项目里用到了 el-buttonel-drawerElMessage 等能力,所以这里要先注册 Element Plus。

ts 复制代码
import '@/styles/main.scss'

导入全局样式。

因为这里使用了 @,所以前面配置路径别名是有用的。

@/styles/main.scss 实际指向:

text 复制代码
src/styles/main.scss
ts 复制代码
import App from './App.vue'

导入根组件。

ts 复制代码
import { router } from '@/router'

导入路由实例。

因为 src/router/index.ts 导出了 router,所以这里可以直接从 @/router 导入。

最后这一行最关键:

ts 复制代码
createApp(App).use(router).use(ElementPlus).mount('#app')

它的含义是:

text 复制代码
创建 Vue 应用
-> 安装路由插件
-> 安装 Element Plus
-> 挂载到 index.html 的 #app 节点上

如果少了:

ts 复制代码
.use(router)

页面路由就不能正常工作。

如果少了:

ts 复制代码
.use(ElementPlus)

页面中的 Element Plus 组件可能无法正常显示。

2. src/App.vue:根组件只负责放路由出口

当前 App.vue 非常简单:

vue 复制代码
<template>
  <router-view />
</template>

router-view 是 Vue Router 提供的组件。

它的作用是:

text 复制代码
当前 URL 匹配到哪个页面,就把哪个页面显示在这里。

比如:

URL router-view 显示的页面
/ HomePage.vue
/learning-room LearningRoomPage.vue
/speaking-practice SpeakingPracticePage.vue
/vocabulary VocabularyPage.vue
/reading-training ReadingTrainingPage.vue
/writing-review WritingReviewPage.vue

所以当前项目不是把所有页面都写进 App.vue,而是让 App.vue 做一个"页面出口"。

这是一种很常见的 Vue 项目结构。

十二、路由配置:6 个手机页面怎么跳转

路由文件在:

text 复制代码
src/router/index.ts

当前代码如下:

ts 复制代码
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/HomePage.vue'),
  },
  {
    path: '/learning-room',
    name: 'learning-room',
    component: () => import('@/views/LearningRoomPage.vue'),
  },
  {
    path: '/speaking-practice',
    name: 'speaking-practice',
    component: () => import('@/views/SpeakingPracticePage.vue'),
  },
  {
    path: '/vocabulary',
    name: 'vocabulary',
    component: () => import('@/views/VocabularyPage.vue'),
  },
  {
    path: '/reading-training',
    name: 'reading-training',
    component: () => import('@/views/ReadingTrainingPage.vue'),
  },
  {
    path: '/writing-review',
    name: 'writing-review',
    component: () => import('@/views/WritingReviewPage.vue'),
  },
]

export const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior() {
    return { top: 0 }
  },
})

1. routes 是页面地图

这段代码可以理解成一张页面地图:

text 复制代码
/                 -> 首页
/learning-room    -> 学习室
/speaking-practice -> 口语陪练
/vocabulary       -> 单词记忆
/reading-training -> 阅读训练
/writing-review   -> 写作批改

每个路由对象都有 3 个核心字段:

字段 作用
path 浏览器地址栏里的路径
name 路由名称,方便代码识别
component 这个路径要显示哪个 Vue 页面

比如:

ts 复制代码
{
  path: '/learning-room',
  name: 'learning-room',
  component: () => import('@/views/LearningRoomPage.vue'),
}

意思是:

text 复制代码
当浏览器访问 /learning-room 时,加载并显示 LearningRoomPage.vue。

2. 为什么 component 后面是函数

你可能会疑惑,为什么不是这样写:

ts 复制代码
import LearningRoomPage from '@/views/LearningRoomPage.vue'

而是写成:

ts 复制代码
component: () => import('@/views/LearningRoomPage.vue')

这种写法叫"懒加载"。

意思是:

text 复制代码
用户访问到这个页面时,才加载这个页面组件。

对于页面比较多的项目,这样可以减少首页第一次加载的压力。

当前项目虽然还不大,但从一开始就用这种写法,是比较好的习惯。

3. createWebHistory() 是什么

ts 复制代码
history: createWebHistory()

表示使用 HTML5 History 模式。

这样地址栏看起来会比较自然:

text 复制代码
/learning-room
/vocabulary
/writing-review

而不是:

text 复制代码
#/learning-room
#/vocabulary

4. scrollBehavior 的作用

ts 复制代码
scrollBehavior() {
  return { top: 0 }
}

意思是:每次切换页面后,滚动条回到顶部。

这对手机页面很重要。

比如你在阅读训练页面滚到下面,然后切换到写作批改页面,如果不重置滚动位置,用户可能一进新页面就停在中间位置,体验不好。

5. 页面里怎么主动跳转

首页里有一段代码:

ts 复制代码
const router = useRouter()

这表示拿到路由控制器。

然后可以这样跳转:

ts 复制代码
router.push('/learning-room')

意思是:跳转到学习室页面。

首页按钮就是这样用的:

vue 复制代码
<HomeHero
  @start="router.push('/learning-room')"
  @tasks="ElMessage.info('今日任务功能待接入')"
/>

这里的逻辑是:

text 复制代码
点击"开始学习"
-> HomeHero 发出 start 事件
-> HomePage 接收到 start
-> 执行 router.push('/learning-room')
-> 页面切换到学习室

十三、移动端全局布局:所有页面为什么看起来像手机

当前每个页面最外层都包了一层:

vue 复制代码
<MobileLayout>
  页面内容
</MobileLayout>

比如首页:

vue 复制代码
<template>
  <MobileLayout>
    <div class="home-page page-stack">
      ...
    </div>
  </MobileLayout>
</template>

学习室页面也是:

vue 复制代码
<template>
  <MobileLayout>
    <div class="learning-room-page page-stack">
      ...
    </div>
  </MobileLayout>
</template>

这说明 MobileLayout.vue 是所有页面共享的手机壳布局。

1. MobileLayout.vue 做了什么

文件位置:

text 复制代码
src/components/layout/MobileLayout.vue

核心代码如下:

ts 复制代码
const route = useRoute()
const router = useRouter()
const menuVisible = ref(false)

const activeTitle = computed(() => {
  return routeEntries.find((entry) => entry.path === route.path)?.title ?? 'Echo English AI'
})

function navigate(path: string) {
  menuVisible.value = false
  router.push(path)
}

逐个解释。

ts 复制代码
const route = useRoute()

拿到当前路由信息。

比如当前是 /vocabulary,那 route.path 就是:

text 复制代码
/vocabulary
ts 复制代码
const router = useRouter()

拿到路由跳转能力。

后面点击菜单时要用它跳转页面。

ts 复制代码
const menuVisible = ref(false)

控制右侧抽屉菜单是否显示。

false 表示隐藏,true 表示显示。

ts 复制代码
const activeTitle = computed(() => {
  return routeEntries.find((entry) => entry.path === route.path)?.title ?? 'Echo English AI'
})

这是根据当前路径计算页面标题。

例如:

text 复制代码
当前 route.path = /reading-training
routeEntries 里找到对应项
标题就是 阅读训练

computed 的特点是:依赖的数据变化时,它会自动重新计算。

所以当路由变化时,顶部标题也会跟着变化。

ts 复制代码
function navigate(path: string) {
  menuVisible.value = false
  router.push(path)
}

点击抽屉菜单中的某个页面时:

text 复制代码
先关闭菜单
再跳转页面

2. slot 是页面内容插槽

模板里有一段:

vue 复制代码
<div class="phone-content">
  <slot />
</div>

slot 是 Vue 的插槽。

它的作用是:

text 复制代码
父组件包进来的内容,会显示在 slot 这个位置。

比如首页写:

vue 复制代码
<MobileLayout>
  <div class="home-page page-stack">
    首页内容
  </div>
</MobileLayout>

那么 首页内容 最终会显示到 MobileLayout 的:

vue 复制代码
<slot />

这个位置。

所以 MobileLayout 负责统一外壳,具体页面负责填充内容。

3. AppHeader.vue 负责顶部栏

文件位置:

text 复制代码
src/components/layout/AppHeader.vue

核心代码:

vue 复制代码
<AppHeader
  :title="activeTitle === '首页' ? 'Echo English AI' : activeTitle"
  @open-menu="menuVisible = true"
/>

这里有两个知识点:

vue 复制代码
:title="..."

这是给子组件传 title 属性。

vue 复制代码
@open-menu="menuVisible = true"

这是监听子组件发出的 open-menu 事件。

AppHeader.vue 中有:

ts 复制代码
const emit = defineEmits<{
  openMenu: []
}>()

按钮点击时:

vue 复制代码
<el-button circle class="menu-button" :icon="Menu" @click="emit('openMenu')" />

完整流程是:

text 复制代码
用户点击右上角菜单按钮
-> AppHeader 发出 openMenu 事件
-> MobileLayout 接收 open-menu
-> menuVisible = true
-> el-drawer 打开右侧导航菜单

4. el-drawer 是右侧抽屉导航

模板里有:

vue 复制代码
<el-drawer v-model="menuVisible" size="78%" direction="rtl" title="学习导航">
  ...
</el-drawer>

el-drawer 是 Element Plus 的抽屉组件。

vue 复制代码
v-model="menuVisible"

表示抽屉的显示和隐藏由 menuVisible 控制。

vue 复制代码
direction="rtl"

表示从右向左弹出。

菜单项来自:

ts 复制代码
routeEntries

也就是 src/data/mock.ts 里的路由导航数据。

十四、Mock 数据:没有后端时页面为什么也有内容

当前项目还没有真正接后端。

但是页面已经能显示学习进度、快捷入口、聊天消息、单词、阅读词汇、写作评分。

这些数据来自:

text 复制代码
src/data/mock.ts

这个文件就是当前阶段的模拟数据中心。

1. 为什么要先写 Mock 数据

项目开发时,前端和后端不一定同步完成。

如果前端一直等后端接口,页面开发就会卡住。

所以我们先用 Mock 数据模拟真实接口返回。

这样可以先完成:

text 复制代码
页面结构
组件拆分
列表渲染
点击交互
状态切换

等后端做好后,再把 Mock 数据替换成真实接口。

2. types/app.ts 定义数据长什么样

Mock 数据不是随便写的,它有类型约束。

类型文件在:

text 复制代码
src/types/app.ts

比如页面 key:

ts 复制代码
export type PageKey =
  | 'home'
  | 'learning-room'
  | 'speaking-practice'
  | 'vocabulary'
  | 'reading-training'
  | 'writing-review'
  | 'profile'

这表示页面 key 只能是这些固定字符串。

再比如路由导航项:

ts 复制代码
export type RouteEntry = {
  key: PageKey
  title: string
  path: string
  description: string
  icon: Component
}

这表示一个导航项必须有:

字段 含义
key 页面唯一标识
title 页面标题
path 跳转路径
description 简短说明
icon Element Plus 图标组件

再比如聊天消息:

ts 复制代码
export type ChatMessage = {
  id: number
  role: 'ai' | 'user'
  content: string
  translation?: string
}

这里的 role 只能是:

text 复制代码
ai
user

translation?: string 后面有一个 ?,表示这个字段可有可无。

3. mock.ts 统一管理演示数据

mock.ts 中有很多导出数据。

比如路由导航:

ts 复制代码
export const routeEntries: RouteEntry[] = [
  { key: 'home', title: '首页', path: '/', description: '学习概览', icon: House },
  { key: 'learning-room', title: '学习室', path: '/learning-room', description: 'AI 场景对话', icon: ChatDotRound },
  { key: 'speaking-practice', title: '口语陪练', path: '/speaking-practice', description: '发音与流利度', icon: Headset },
  { key: 'vocabulary', title: '单词记忆', path: '/vocabulary', description: '智能复习', icon: Files },
  { key: 'reading-training', title: '阅读训练', path: '/reading-training', description: '文章精读', icon: Reading },
  { key: 'writing-review', title: '写作批改', path: '/writing-review', description: 'AI 批改', icon: EditPen },
]

它会被两个地方使用:

text 复制代码
MobileLayout.vue:生成右侧抽屉导航
HomePage.vue:根据快捷入口找到目标路径并跳转

学习进度:

ts 复制代码
export const progressSummary: ProgressSummary = {
  percent: 73,
  studyMinutes: 11,
  targetMinutes: 15,
  completedTasks: 3,
  totalTasks: 4,
  streakDays: 7,
}

它会传给首页的学习进度组件:

vue 复制代码
<StudyProgressCard :summary="progressSummary" />

聊天消息:

ts 复制代码
export const initialMessages: ChatMessage[] = [
  {
    id: 1,
    role: 'ai',
    content: 'Hi! Welcome to the restaurant. What would you like to order?',
    translation: '您好!欢迎光临餐厅。您想点些什么?',
  },
  {
    id: 2,
    role: 'user',
    content: "I'd like a grilled chicken salad, please.",
    translation: '请给我一份烤鸡肉沙拉。',
  },
]

它会被学习室页面作为初始聊天记录。

单词数据:

ts 复制代码
export const vocabularyWords: VocabularyWord[] = [
  {
    word: 'sophisticated',
    phonetic: "/səˈfɪstɪkeɪtɪd/",
    meaning: 'adj. 复杂的;精密的;老练的',
    example: 'The software uses sophisticated algorithms.',
    translation: '这款软件使用了复杂的算法。',
  },
]

它会被单词记忆页面使用。

写作类型:

ts 复制代码
export const writingTypes: WritingType[] = [
  { id: 'essay', label: '作文', placeholder: '请输入你的英语作文内容...' },
  { id: 'email', label: '邮件', placeholder: '请输入需要润色或批改的英文邮件...' },
]

它会被写作批改页面用来渲染顶部类型按钮。

4. Mock 数据和真实接口的关系

当前阶段:

text 复制代码
页面 -> mock.ts -> 静态数据

后面接后端后会变成:

text 复制代码
页面 -> api 请求函数 -> /api 接口 -> 后端服务 -> 数据库或 AI 服务

所以现在先把数据结构设计清楚,后面替换成真实接口会更顺。

十五、首页:页面负责调度,组件负责展示

首页文件是:

text 复制代码
src/views/HomePage.vue

当前代码结构:

ts 复制代码
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import MobileLayout from '@/components/layout/MobileLayout.vue'
import HomeHero from '@/components/home/HomeHero.vue'
import QuickAccessGrid from '@/components/home/QuickAccessGrid.vue'
import StudyProgressCard from '@/components/home/StudyProgressCard.vue'
import TodaySuggestionCard from '@/components/home/TodaySuggestionCard.vue'
import { progressSummary, quickAccessEntries, routeEntries } from '@/data/mock'
import type { QuickAccessEntry } from '@/types/app'

const router = useRouter()

这说明首页不是把所有 HTML 都写在一个文件里,而是拆成了几个组件:

组件 作用
HomeHero.vue 首页顶部宣传区和机器人视觉
StudyProgressCard.vue 今日学习进度
QuickAccessGrid.vue 快捷入口宫格
TodaySuggestionCard.vue 今日学习建议
MobileLayout.vue 手机外壳布局

1. 首页模板怎么组合组件

vue 复制代码
<template>
  <MobileLayout>
    <div class="home-page page-stack">
      <HomeHero
        @start="router.push('/learning-room')"
        @tasks="ElMessage.info('今日任务功能待接入')"
      />
      <StudyProgressCard :summary="progressSummary" @detail="ElMessage.info('学习统计详情待接入')" />
      <QuickAccessGrid :entries="quickAccessEntries" @select="navigateToEntry" />
      <TodaySuggestionCard @open="router.push('/learning-room')" />
    </div>
  </MobileLayout>
</template>

这里可以看出页面组件主要做 3 件事:

text 复制代码
1. 决定页面用哪些子组件
2. 把数据传给子组件
3. 接收子组件事件并处理跳转或提示

2. :summary="progressSummary" 是什么意思

vue 复制代码
<StudyProgressCard :summary="progressSummary" />

:summaryv-bind:summary 的简写。

意思是:

text 复制代码
把 progressSummary 这个变量传给 StudyProgressCard 组件的 summary 属性。

progressSummary 来自:

ts 复制代码
import { progressSummary } from '@/data/mock'

所以数据流是:

text 复制代码
mock.ts
-> HomePage.vue
-> StudyProgressCard.vue

3. @select="navigateToEntry" 是什么意思

vue 复制代码
<QuickAccessGrid :entries="quickAccessEntries" @select="navigateToEntry" />

@select 是监听子组件发出的 select 事件。

QuickAccessGrid.vue 里有:

ts 复制代码
const emit = defineEmits<{
  select: [entry: QuickAccessEntry]
}>()

当用户点击某个入口时,它会执行:

vue 复制代码
@select="emit('select', entry)"

父组件 HomePage.vue 接收到后,执行:

ts 复制代码
function navigateToEntry(entry: QuickAccessEntry) {
  if (entry.id === 'profile') {
    ElMessage.info('我的档案功能待接入')
    return
  }

  const target = routeEntries.find((routeEntry) => routeEntry.key === entry.id)
  if (target) {
    router.push(target.path)
  }
}

这段逻辑可以翻译成:

text 复制代码
如果点击的是"我的档案",暂时弹出提示;
否则根据 entry.id 去 routeEntries 中找到对应路由;
找到后用 router.push 跳转过去。

这就是典型的父子组件通信。

4. 机器人素材在哪

首页的机器人视觉由组件:

text 复制代码
src/components/common/RobotAvatar.vue

负责显示。

它导入了图片:

ts 复制代码
import robotMascot from '@/assets/robot-mascot.png'

模板里使用:

vue 复制代码
<img :src="robotMascot" alt="Echo English AI 机器人助手" />

所以机器人图片实际放在:

text 复制代码
src/assets/robot-mascot.png

后续如果你想换机器人形象,只需要替换这个图片,或者修改 RobotAvatar.vue

十六、学习室页面:场景切换和聊天消息怎么实现

学习室页面文件:

text 复制代码
src/views/LearningRoomPage.vue

它负责 AI 对话学习场景。

核心状态如下:

ts 复制代码
const activeScenario = ref<Scenario>(learningScenarios[0])
const messages = ref<ChatMessage[]>([...initialMessages])

这两个状态分别表示:

状态 作用
activeScenario 当前选择的练习场景
messages 当前聊天消息列表

1. 切换场景

ts 复制代码
function changeScenario(scenario: Scenario) {
  activeScenario.value = scenario
  messages.value = [
    {
      id: Date.now(),
      role: 'ai',
      content: scenario.prompt,
      translation: scenario.description,
    },
  ]
}

当用户切换场景时:

text 复制代码
1. 更新 activeScenario
2. 清空旧消息
3. 用新场景的 prompt 创建一条 AI 开场消息

比如从"点餐练习"切换到"旅行出行",页面里的 AI 提示语也会变。

2. 发送消息

ts 复制代码
function sendMessage(value: string) {
  const content = value.trim()
  if (!content) {
    ElMessage.warning('请输入想练习的英文句子')
    return
  }

  messages.value.push({ id: Date.now(), role: 'user', content })
  messages.value.push({
    id: Date.now() + 1,
    role: 'ai',
    content: 'Nice expression. Try adding one more detail to make it more natural.',
    translation: '表达不错。可以再补充一个细节,让句子更自然。',
  })
}

这段代码做了 3 件事:

text 复制代码
1. 去掉输入内容前后的空格
2. 如果为空,就弹出提示
3. 如果不为空,就先追加用户消息,再追加一条模拟 AI 回复

注意这里还没有真正接 AI 接口。

当前只是用一条固定回复模拟 AI 反馈。

后面接后端时,可以把这里替换成:

text 复制代码
调用 /api/chat
等待后端返回 AI 回复
把后端返回内容 push 到 messages

3. 组件拆分

学习室页面用到了这些组件:

组件 作用
ScenarioSwitcher.vue 场景切换
ChatMessageList.vue 聊天消息列表
ChatInputBar.vue 底部输入框
BaseCard.vue 卡片容器

这就是比较合理的拆法。

页面 LearningRoomPage.vue 负责状态和业务逻辑,子组件负责显示和触发事件。

十七、口语、单词、阅读、写作页面的轻量交互

除了首页和学习室,Codex 还生成了 4 个功能页面。

这 4 个页面目前不是完整业务,只是 v1 阶段的轻量交互。

这样设计是对的。

因为第一版项目要先完成"页面能看、能点、能切换",不应该一上来就接语音识别、AI 批改、文件解析这些复杂能力。

1. 口语陪练页面

文件:

text 复制代码
src/views/SpeakingPracticePage.vue

核心状态:

ts 复制代码
const activeScene = ref(speakingScenes[0])
const status = ref<'idle' | 'recording' | 'scored'>('idle')
const originalVisible = ref(false)

含义:

状态 作用
activeScene 当前口语练习场景
status 当前录音状态
originalVisible 是否显示原文

录音按钮逻辑:

ts 复制代码
function toggleRecord() {
  if (status.value === 'idle') {
    status.value = 'recording'
    return
  }

  status.value = 'scored'
  ElMessage.success('已生成模拟评分')
}

第一次点击:

text 复制代码
idle -> recording

再次点击:

text 复制代码
recording -> scored

然后显示模拟评分。

这就是一个很好的前端原型:先用状态切换模拟录音流程,后面再接真实语音能力。

2. 单词记忆页面

文件:

text 复制代码
src/views/VocabularyPage.vue

核心状态:

ts 复制代码
const currentIndex = ref(0)
const showExample = ref(false)
const known = ref(96)
const unclear = ref(34)
const unknown = ref(20)

含义:

状态 作用
currentIndex 当前显示第几个单词
showExample 是否显示例句
known 已掌握数量
unclear 模糊数量
unknown 不认识数量

当前单词通过 computed 计算:

ts 复制代码
const currentWord = computed(() => vocabularyWords[currentIndex.value] ?? vocabularyWords[vocabularyWords.length - 1])

意思是:

text 复制代码
根据 currentIndex 从 vocabularyWords 中取当前单词。
如果越界,就兜底取最后一个单词。

选择熟悉度:

ts 复制代码
function choose(status: 'known' | 'unclear' | 'unknown') {
  if (status === 'known') known.value += 1
  if (status === 'unclear') unclear.value += 1
  if (status === 'unknown') unknown.value += 1

  showExample.value = false
  if (currentIndex.value < vocabularyWords.length - 1) {
    currentIndex.value += 1
  } else {
    ElMessage.success('今日单词复习已完成')
  }
}

这段逻辑就是:

text 复制代码
点击"熟悉/模糊/不认识"
-> 对应数量加 1
-> 隐藏例句
-> 切换到下一个单词
-> 如果已经是最后一个单词,提示复习完成

3. 阅读训练页面

文件:

text 复制代码
src/views/ReadingTrainingPage.vue

核心状态:

ts 复制代码
const activeTab = ref('words')
const text = ref('')

activeTab 表示当前分析模式,例如:

text 复制代码
词汇解析
长难句分析
文章摘要

预览内容通过 computed 生成:

ts 复制代码
const previewContent = computed(() => {
  if (activeTab.value === 'sentences') {
    return 'Artificial intelligence is transforming education by providing personalized learning experiences and automating routine tasks.'
  }

  if (activeTab.value === 'summary') {
    return '本文介绍了人工智能如何通过个性化学习、自动化任务和数据反馈提升教育效率。'
  }

  return text.value || 'Artificial intelligence is transforming education by providing personalized learning experiences and automating routine tasks.'
})

这段代码的意思是:

text 复制代码
如果选择长难句,就显示英文长句;
如果选择摘要,就显示中文摘要;
否则显示用户输入的文本;
如果用户没输入,就显示默认文本。

模板里有一个条件渲染:

vue 复制代码
<VocabularyParseList v-if="activeTab === 'words'" :words="readingWords" />

意思是:

text 复制代码
只有当前 Tab 是 words 时,才显示词汇解析列表。

4. 写作批改页面

文件:

text 复制代码
src/views/WritingReviewPage.vue

核心状态:

ts 复制代码
const activeType = ref<WritingType>(writingTypes[0])
const content = ref("With the development of technology, people's life has become more convenient. However, some people think it also brings some problems.")
const hasResult = ref(false)

含义:

状态 作用
activeType 当前写作类型,例如作文、邮件、简历
content 用户输入的英文内容
hasResult 是否已经生成批改结果

提交逻辑:

ts 复制代码
function submitReview() {
  if (!content.value.trim()) {
    ElMessage.warning('请先输入需要批改的英文内容')
    return
  }

  hasResult.value = true
  ElMessage.success('已生成模拟批改结果')
}

这段代码的意思是:

text 复制代码
如果输入为空,提示用户先输入;
如果有内容,就把 hasResult 改成 true,并提示生成模拟批改结果。

页面里根据 hasResult 控制结果区域显示:

vue 复制代码
<WritingScorePanel v-if="hasResult" :scores="writingScores" />
<WritingComparePanel :visible="hasResult" />
<CorrectionReportEntry v-if="hasResult" @open="ElMessage.info('完整批改报告待接入')" />

所以写作批改页面的流程是:

text 复制代码
输入英文内容
-> 点击提交批改
-> hasResult = true
-> 显示评分、修改对比、批改报告入口

十八、通用组件:为什么要拆 common

当前项目有一个目录:

text 复制代码
src/components/common

这里放的是跨页面可复用组件。

比如:

组件 作用
BaseCard.vue 统一卡片样式
BaseButtonGroup.vue 统一双按钮组合
FeatureGridItem.vue 首页快捷入口卡片
MetricItem.vue 指标展示项
ProgressRing.vue 环形进度
RobotAvatar.vue 机器人头像或视觉图
SectionHeader.vue 区块标题

1. BaseCard.vue 的作用

BaseCard.vue 很简单:

vue 复制代码
<template>
  <section class="base-card">
    <slot />
  </section>
</template>

它没有业务逻辑,只负责提供统一卡片样式。

后面其他页面想要一个卡片,就可以写:

vue 复制代码
<BaseCard>
  <h2>学习进度</h2>
  <p>今天已完成 73%</p>
</BaseCard>

这样所有卡片的圆角、边框、阴影都会统一。

2. FeatureGridItem.vue 的作用

FeatureGridItem.vue 用来展示一个快捷入口。

它接收这些 props:

ts 复制代码
defineProps<{
  title: string
  description: string
  icon: Component
  accent: string
}>()

也就是说,父组件传给它:

text 复制代码
标题
描述
图标
强调色

它自己负责显示成一个按钮。

点击时发出事件:

ts 复制代码
const emit = defineEmits<{
  select: []
}>()

模板里:

vue 复制代码
<button class="feature-grid-item" type="button" @click="emit('select')">

这就是组件封装的价值:

text 复制代码
子组件不关心点击后跳到哪里;
它只告诉父组件:我被点击了。

真正的跳转逻辑放在父组件 HomePage.vue 里。

3. RobotAvatar.vue 的作用

RobotAvatar.vue 负责统一显示机器人素材:

ts 复制代码
import robotMascot from '@/assets/robot-mascot.png'

模板:

vue 复制代码
<figure class="robot-avatar" :class="{ compact }">
  <img :src="robotMascot" alt="Echo English AI 机器人助手" />
</figure>

它还支持一个可选属性:

ts 复制代码
compact?: boolean

如果传入 compact,机器人会显示成小头像样式。

这种设计方便后面在不同页面复用同一个机器人视觉。

十九、全局样式和 Element Plus

当前项目全局样式文件是:

text 复制代码
src/styles/main.scss

它在 main.ts 里被导入:

ts 复制代码
import '@/styles/main.scss'

所以它会影响整个项目。

1. 全局 CSS 变量

文件开头定义了一些变量:

scss 复制代码
:root {
  --echo-primary: #3154ff;
  --echo-primary-soft: #edf2ff;
  --echo-text: #17215b;
  --echo-muted: #7b86b6;
  --echo-border: rgba(72, 97, 230, 0.12);
  --echo-card: rgba(255, 255, 255, 0.92);
}

这些变量相当于项目的设计语言。

比如主色统一使用:

scss 复制代码
var(--echo-primary)

以后如果想调整品牌蓝色,只要改:

scss 复制代码
--echo-primary: #3154ff;

很多组件都会一起变化。

2. page-stack 统一页面间距

scss 复制代码
.page-stack {
  display: grid;
  gap: 14px;
}

每个页面最外层都有:

vue 复制代码
<div class="xxx-page page-stack">

所以页面内部模块之间会自动有统一间距。

3. Element Plus 样式微调

全局样式里还改了一些 Element Plus 组件:

scss 复制代码
.el-drawer {
  border-radius: 22px 0 0 22px;
}

.el-input__wrapper,
.el-textarea__inner {
  border-radius: 14px;
  box-shadow: 0 0 0 1px var(--echo-border) inset;
}

这说明项目虽然用了 Element Plus,但没有完全使用默认风格,而是做了移动端视觉适配。

4. 手机端适配

scss 复制代码
@media (max-width: 430px) {
  .mobile-stage {
    padding: 0;
  }

  .phone-frame {
    width: 100%;
    min-height: 100svh;
    border: 0;
    border-radius: 0;
  }
}

这段代码表示:

text 复制代码
当屏幕宽度小于等于 430px 时,页面直接铺满手机屏幕。

在电脑浏览器里看,它像一个手机壳;在真实手机里看,它会铺满屏幕。

二十、项目级规范:为什么不用每次都重复提示 Codex

当前项目已经生成了:

text 复制代码
AGENTS.md
docs/coding-standards.md

这两个文件的作用不同。

1. AGENTS.md 给 Codex 看

AGENTS.md 是项目级指令文件。

它告诉 Codex:

text 复制代码
当前项目是 Vue 3.5 + Vite + TypeScript;
生成或修改代码前,要先阅读 docs/coding-standards.md;
组件用 <script setup lang="ts">;
路径优先使用 @/;
API 请求统一用 /api;
修改后优先运行 npm.cmd run build。

所以以后你不需要每次都在提示词里写:

text 复制代码
请遵守编码规范。

只要 Codex 在这个项目根目录下工作,它就会优先参考 AGENTS.md

2. docs/coding-standards.md 给人和 AI 一起看

这个文件是详细编码规范,覆盖:

text 复制代码
基础原则
目录规范
命名规范
Vue 组件规范
TypeScript 规范
Vite 与环境变量规范
API 请求规范
样式规范
注释规范
依赖规范
验证规范
代码生成回复规范

简单说:

text 复制代码
AGENTS.md = 简短强制入口
coding-standards.md = 详细规范说明

这个设计比每次复制一大段提示词更好。

二十一、当前项目从打开网页到页面显示的完整流程

现在把整套项目串起来。

1. 启动项目

在终端进入项目目录:

bash 复制代码
cd "C:\echo english ai\agent-frontend-project"

启动开发服务器:

bash 复制代码
npm.cmd run dev

因为 vite.config.ts 里配置了:

ts 复制代码
server: {
  port: 8080,
  host: '0.0.0.0',
}

所以浏览器访问:

text 复制代码
http://localhost:8080/

2. 页面执行顺序

完整顺序如下:

text 复制代码
浏览器访问 http://localhost:8080/
  ↓
Vite 返回 index.html
  ↓
index.html 加载 /src/main.ts
  ↓
main.ts 创建 Vue 应用
  ↓
main.ts 注册 router 和 ElementPlus
  ↓
App.vue 显示 router-view
  ↓
router/index.ts 根据 URL 找页面
  ↓
HomePage.vue 或其他页面被渲染
  ↓
页面导入 components 子组件
  ↓
页面从 data/mock.ts 获取模拟数据
  ↓
styles/main.scss 控制整体视觉

3. 用户点击菜单时发生什么

以点击"单词记忆"为例。

流程是:

text 复制代码
用户点击右上角菜单
-> AppHeader 发出 openMenu
-> MobileLayout 打开 el-drawer
-> 抽屉里循环 routeEntries 生成菜单项
-> 用户点击"单词记忆"
-> navigate('/vocabulary')
-> router.push('/vocabulary')
-> router-view 渲染 VocabularyPage.vue

4. 页面显示数据时发生什么

以首页学习进度为例:

text 复制代码
mock.ts 定义 progressSummary
-> HomePage.vue 导入 progressSummary
-> HomePage.vue 通过 :summary 传给 StudyProgressCard
-> StudyProgressCard 根据 summary 渲染进度

以学习室聊天为例:

text 复制代码
mock.ts 定义 initialMessages
-> LearningRoomPage.vue 初始化 messages
-> ChatMessageList 接收 :messages
-> 用户发送消息后 sendMessage 修改 messages
-> Vue 响应式系统自动更新消息列表

这就是 Vue 项目的核心思想:

text 复制代码
数据变化 -> 页面自动更新

二十二、运行与验收

完成代码生成后,建议按下面顺序验收。

1. 安装依赖

如果你是第一次打开项目,先执行:

bash 复制代码
cd "C:\echo english ai\agent-frontend-project"
npm.cmd install

2. 启动开发服务器

bash 复制代码
npm.cmd run dev

浏览器访问:

text 复制代码
http://localhost:8080/

3. 检查 6 个页面

依次访问:

text 复制代码
http://localhost:8080/
http://localhost:8080/learning-room
http://localhost:8080/speaking-practice
http://localhost:8080/vocabulary
http://localhost:8080/reading-training
http://localhost:8080/writing-review

也可以点击右上角菜单,通过抽屉导航切换页面。

4. 检查轻量交互

页面 检查点
首页 点击"开始学习"能进入学习室
首页 快捷入口能跳转到对应页面
学习室 切换场景后消息会变化
学习室 输入英文句子后会追加用户消息和模拟 AI 回复
口语陪练 点击录音按钮,状态能从待录音变成录音中,再变成已评分
单词记忆 点击熟悉度按钮能切换到下一个单词
阅读训练 切换分析 Tab,预览内容会变化
写作批改 输入内容后点击提交,会显示模拟评分和批改结果

5. 构建验证

执行:

bash 复制代码
npm.cmd run build

如果构建通过,说明当前 TypeScript、路由、组件导入、样式编译基本没问题。

如果构建报错,不要只截图一小段,最好把完整报错复制给 Codex,例如:

text 复制代码
我执行 npm.cmd run build 报错,完整错误如下:

【粘贴完整报错】

请你先分析原因,再做最小修改,不要重构无关文件。

二十三、本篇总结

这一篇我们从 Codex 计划模式生成的真实代码出发,梳理了 Echo English AI 前端骨架。

现在项目已经具备:

text 复制代码
Vue3.5 + Vite + TypeScript 工程基础
Element Plus 组件库
Sass 全局样式
Vue Router 六页面跳转
移动端手机壳布局
首页、学习室、口语、单词、阅读、写作六个页面
Mock 数据中心
TypeScript 类型声明
项目级 AGENTS.md 规范
轻量交互原型

当前阶段最重要的不是功能多复杂,而是项目结构已经清晰:

text 复制代码
main.ts 负责启动
App.vue 负责路由出口
router/index.ts 负责页面映射
views 负责页面
components 负责组件
data/mock.ts 负责模拟数据
types/app.ts 负责类型
styles/main.scss 负责全局样式

后续继续开发时,就可以一页一页往下做:

text 复制代码
首页精细化
学习室真实聊天接口
口语录音与评分
单词复习算法
阅读文本解析
写作批改接口

到这里,我们已经从"创建一个 Vite 项目",进入到了"用 Codex 搭建一个可继续迭代的移动端 AI 英语学习站"。