【从零构建多智能体 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的计划模式,这里我回答了的问题可以作为参考:
- 这次你希望高度还原的是哪一种页面形态?真实手机端应用
- 首页和建议卡片里的机器人视觉,你希望怎么处理?生成新素材 (Recommended)
- 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.md、docs/coding-standards.md |
告诉 Codex 后续生成代码时遵守哪些规范 |
| 工程配置 | vite.config.ts、tsconfig.app.json、.env.* |
配置端口、别名、代理、环境变量、TypeScript |
| 应用入口 | src/main.ts、src/App.vue |
负责启动 Vue 应用和挂载路由 |
| 页面路由 | src/router/index.ts |
定义 URL 和页面组件之间的关系 |
| 模拟数据 | src/data/mock.ts |
暂时代替后端接口,给页面提供演示数据 |
| 类型声明 | src/types/app.ts |
定义数据长什么样,提升 TypeScript 提示和安全性 |
| 页面与组件 | src/views、src/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-button、el-drawer、ElMessage 等能力,所以这里要先注册 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" />
:summary 是 v-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 英语学习站"。