用 Vibe Coding 方式写项目的第一个落地目标,我选择了后台管理模板。原因很简单:后台管理模板几乎包含了一个业务系统最常见的工程点,布局、权限、动态路由、表单、CRUD、接口对接,再加上低代码配置和 AI 中心,就是一个能实际用于内部系统的起点。这篇文章会围绕这个模板说明 Vibe Coding 的核心工作方式、低代码 Schema 设计、AI 中心的接口实现,以及真正把项目跑起来后最容易踩的坑。
模板的定位不是简单换肤,而是一个可以继续扩展的中后台基础工程:前端负责动态渲染和交互,后端提供配置下发和 AI 会话能力。低代码部分通过 JSON Schema 驱动表单和页面,让新增业务页面时不再重复写表单组件;AI 中心则统一封装大模型接口,提供会话、技能和轻量知识库能力。最终目标是让后续业务项目直接基于这套模板生长,而不是每次都从零开始。
1. Vibe Coding 是什么,为什么先做后台管理模板
1.1 Vibe Coding 的核心理念
Vibe Coding 不是某个特定工具,而是一种更接近“和 AI 结对编程”的开发方式:开发者用自然语言描述需求,AI 辅助生成代码、修改逻辑、解释报错,甚至帮助重构。重点不在于让 AI 一次性生成整个系统,而在于把需求拆成可验证的小步骤,让 AI 在每一小步里产出可运行、可审查的代码。
这种方式的效率来自两点。第一,重复性编码速度明显更快,比如表格页、表单页、增删改查接口、类型定义,这些样板代码对 AI 来说几乎没有难度。第二,调试过程更自然,把报错信息贴给 AI,让 AI 先给出定位思路,再决定是否让 AI 直接修改。
但 Vibe Coding 不等于“随便描述几句就交付”。越复杂的项目,越需要开发者先把架构边界想清楚:哪些模块是固定的、哪些配置是动态的、数据存在哪里、权限怎么控制。AI 适合完成已知方案的执行,不适合在需求模糊时替人类做产品决策。因此,这套工作流可以归纳为:人类定边界,AI 写实现,测试做验证,审查守质量。
1.2 为什么第一个项目选择后台管理模板
后台管理模板适合作为 Vibe Coding 系列的第一个项目,因为它覆盖面广,但风险可控。一个典型的模板至少涉及登录认证、布局、权限、菜单、动态路由、表格页、表单页、接口联调,这些都是中后台系统的通用能力;同时它又不像电商秒杀、实时音视频那样对性能和并发有苛刻要求,比较容易在本地跑通。
从复用角度看,模板的收益也最直接。做完第一套后,后续内部系统、运营后台、数据看板都可以直接复制改造。模板中的低代码表单引擎,能显著减少重复页面开发;AI 中心则是一个独立模块,可以承担智能问答、文本生成、报表解读等场景。
这个项目特别适合演示 Vibe Coding 的完整流程:
- 第一步,给 AI 一个清晰的业务目标,让它生成可运行脚手架。
- 第二步,逐轮补充权限、路由、低代码、AI 中心等细节。
- 第三步,把生成的代码放入真实页面验证,发现问题后再通过提示词驱动修复。
- 第四步,把每次有效提示词沉淀成可复用模版。
因此,这个项目的技术价值并不仅仅在模板本身,还在于它示范了“如何用自然语言驱动一个多模块项目落地”。
1.3 低代码、AI 中心与后台模板的关系
这里需要先区分三个概念,避免后续实现时混淆。
低代码是一种开发模式,指的是用可视化配置或配置化数据替代部分手写代码。在本项目中,低代码的核心是:把表单字段、页面布局、校验规则、接口映射保存为 JSON 配置,前端根据配置动态渲染,后端根据配置动态处理数据。这样,新增一个“项目信息页”时可以只配置数据,不再改前端页面代码。
AI 中心是一个业务模块,它把大模型能力封装成统一服务。在本项目中,AI 中心包含会话管理、消息存储、技能编排、知识库检索等能力。它和后台模板的其他模块不一样,因为它的核心输入是自然语言,输出是模型推理结果,所以接口设计要考虑流式返回、超时、上下文长度限制等问题。
后台管理模板则是承载两者的壳。它提供统一布局、登录认证、权限菜单、操作日志等基础设施;低代码模块和 AI 中心作为模板里的两个独立业务域存在。这样设计的好处是:模板的通用能力可以服务于低代码页面,也可以服务于 AI 中心的运维管理页面,形成互相支撑的结构。
2. 项目设计:后台模板的模块边界和数据模型
2.1 业务模块划分
这套模板建议拆成三个端到端业务域,而不是把所有功能堆在同一个页面集合里。
| 模块 | 主要职责 | 典型页面 |
|---|---|---|
| 系统管理 | 登录认证、用户管理、角色权限、菜单管理 | 登录页、用户列表、角色分配、菜单配置 |
| 低代码中心 | 表单配置、页面配置、接口配置、预览发布 | 表单设计器、页面配置列表、预览页 |
| AI 中心 | 会话管理、Prompt 管理、技能编排、知识库管理 | 对话页、技能配置、知识库上传、会话历史 |
三个模块之间的数据关系需要提前约定。低代码模块通过page_config和form_schema存储配置;AI 中心通过ai_session、ai_message、ai_skill存储会话和技能;系统管理提供用户和权限边界。
2.2 核心数据模型
先看基础表结构。以下 SQL 示例用于说明字段设计思路,实际项目需要根据数据库类型调整。
-- 菜单表 CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, name VARCHAR(64) NOT NULL, path VARCHAR(128) NOT NULL, component VARCHAR(128), icon VARCHAR(64), sort INT DEFAULT 0, visible TINYINT DEFAULT 1 ); -- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, nickname VARCHAR(64), status TINYINT DEFAULT 1 ); -- 页面配置表 CREATE TABLE lowcode_page_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, page_code VARCHAR(64) NOT NULL UNIQUE, page_name VARCHAR(128) NOT NULL, route_path VARCHAR(128), schema_config JSON, status TINYINT DEFAULT 0 ); -- AI 会话表 CREATE TABLE ai_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, title VARCHAR(128), skill_code VARCHAR(64), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- AI 消息表 CREATE TABLE ai_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, role VARCHAR(16) NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这里有两个设计点值得注意。第一,schema_config使用 JSON 类型存储,是为了保留表单配置的灵活性;但灵活性也意味着校验责任,保存前必须做 JSON 结构和字段名合法性校验。第二,AI 消息表使用role区分用户消息和模型回复,便于后续按会话维度重放上下文。
2.3 技术选型考虑
技术选型应以团队熟悉度和生态成熟度为准,不需要追求最新版本。一个常见组合如下。
| 层 | 选型 | 职责 |
|---|---|---|
| 前端框架 | Vue 3 + TypeScript | 组件化和类型安全 |
| 构建工具 | Vite | 快速启动、热更新 |
| 状态管理 | Pinia | 用户状态、菜单、缓存配置 |
| 路由 | Vue Router | 静态路由 + 动态注册 |
| UI 组件 | Element Plus | 表格、表单、弹窗等 |
| 后端 | Node.js + Express | 提供 API 服务 |
| 数据库 | PostgreSQL / MySQL / SQLite | 开发环境可用 SQLite |
需要说明的是,本文示例后端使用 Node.js 是为了让整个项目在同一个语言环境里跑通。如果团队技术栈以 Java 为主,可以把控制器层替换为 Spring Boot,前端接口约定不变。
2.4 目录结构
一个便于扩展的前后端分离目录如下:
admin-template/ ├── frontend/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ │ ├── auth.ts │ │ │ ├── lowcode.ts │ │ │ └── ai.ts │ │ ├── components/ │ │ │ └── schema-renderer/ # 低代码渲染器 │ │ ├── layouts/ # 后台布局 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # Pinia stores │ │ ├── views/ │ │ │ ├── login/ │ │ │ ├── system/ │ │ │ ├── lowcode/ │ │ │ └── ai-center/ │ │ ├── types/ # 全局类型定义 │ │ └── utils/ # 权限、请求等工具 │ ├── package.json │ └── vite.config.ts └── server/ ├── src/ │ ├── routes/ # 接口路由 │ ├── services/ # 业务逻辑 │ ├── models/ # 数据模型 │ ├── middleware/ # 鉴权、日志中间件 │ ├── config/ # 环境配置 │ └── index.ts # 服务入口 ├── .env.example └── package.json目录结构是由 Vibe Coding 第一轮提示词生成后人工调整得到的。生成目录后,不要急着写业务代码,先确认入口文件、环境变量、依赖关系是否正确。
3. 用 Vibe Coding 搭脚手架:从需求描述到可运行项目
3.1 第一轮提示词:初始化项目
Vibe Coding 的第一条提示词很关键,它决定了生成结果的整体形态。初始提示词可以这样写:
请使用 Vue 3 + TypeScript + Vite 创建一个后台管理模板前端项目,要求: 1. 包含登录页、主页、404 页面,使用 Vue Router。 2. 使用 Pinia 管理用户状态,登录后保存 token 和用户信息。 3. 布局采用顶部导航 + 左侧菜单 + 内容区域。 4. 使用 Element Plus 作为 UI 组件库。 5. 提供请求工具封装,统一处理 baseURL、token 注入和错误提示。 6. 目录结构按 api、components、router、stores、views、utils 组织。生成完成后,进入项目目录执行安装和启动,确认最基础的页面能打开。
cd frontend npm install npm run dev这一轮的核心检查点是:依赖版本是否互相兼容、路由是否正确配置、登录页是否引用正确的组件。不要因为页面能打开就跳过检查,后续所有业务都建立在这套脚手架上。
3.2 第二轮提示词:补布局与权限
脚手架跑通后,进入第二轮提示词,目标是把登录流程、路由守卫、动态菜单补完整。
请基于现有 Vue 3 项目补充权限功能: 1. 登录接口返回 token、userId、username、menus。 2. 在 localStorage 保存 token,在 Pinia 保存用户信息和菜单。 3. 增加全局前置守卫:没有 token 时跳转登录页,已有 token 时放行。 4. 根据菜单数据生成侧边栏菜单。 5. 提供退出登录方法,清除 token 和用户状态。这里会生成一段路由守卫代码,建议人工再确认一次。典型实现如下:
// src/router/index.ts 片段 import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('@/views/login/index.vue') }, { path: '/', component: () => import('@/layouts/index.vue'), children: [] }, { path: '/:pathMatch(.*)*', component: () => import('@/views/error/404.vue') }, ], }) router.beforeEach((to) => { const userStore = useUserStore() if (to.path === '/login') { return true } if (!userStore.token) { return { path: '/login', query: { redirect: to.fullPath } } } if (userStore.menus.length === 0) { return userStore.fetchMenus().then(() => true).catch(() => false) } return true })这里有几个关键点。第一,获取菜单的逻辑放在守卫里,是为了解决刷新后用户状态丢失的问题。第二,redirect参数保留原始目标路径,登录成功后可以直接跳回。第三,不要在前端直接判断角色编码来决定菜单显示,而应该由后端返回菜单列表,前端只做渲染。
3.3 如何审查 AI 生成的代码
Vibe Coding 最容易犯的错误是全盘接受 AI 输出。无论生成速度多快,都应该按下表做一次快速审查。
| 审查项 | 检查点 |
|---|---|
| 依赖安全 | 是否引入了未知第三方包,版本是否匹配 |
| 登录安全 | token 是否保存在可被 XSS 读取的位置,是否使用了 HttpOnly Cookie |
| 路由守卫 | 是否有死循环跳转,是否处理了菜单拉取失败 |
| 权限逻辑 | 是否只判断了菜单存在,而没有判断接口级权限 |
| 错误处理 | 接口失败时是否有统一错误提示,登录态失效是否会跳登录 |
| 环境配置 | baseURL 是否通过环境变量控制,是否把密钥写进了前端代码 |
审查时建议直接查看生成的package.json、路由文件、store 文件和请求工具。AI 生成的代码往往“看起来完整”,但可能缺少异常分支或类型定义。
注意:Vibe Coding 的价值在于把样板代码的生成速度加快,不意味着可以跳过代码评审。越是自动生成的代码,越需要人工确认边界条件。
4. 低代码能力实现:Schema 驱动的表单与页面
4.1 为什么用 Schema 驱动
后台系统里表单页数量通常很多。如果每个表单都手写一遍,会发现大量代码只是字段名称不同。低代码方案的核心思路是:把“字段定义、校验规则、组件类型、布局信息”从代码中抽离成数据,用同一个渲染器去渲染。
这样做有三个好处:
- 新增页面只需要新增一条配置,不需要写新的 .vue 页面。
- 配置可以存数据库,运营和非研发人员也可以在受控范围内维护页面。
- 配置可以版本化,对比代码变更更直观。
当然也有代价:渲染器需要维护更多组件类型,配置错误时需要做更完善的提示。因此,Schema 驱动更适合规则稳定的内部管理页面,不适合交互非常复杂、状态非常多变的页面。
4.2 表单 Schema 结构
下面是一个最小表单 Schema 示例。
{ "type": "form", "fields": [ { "name": "projectName", "label": "项目名称", "component": "input", "placeholder": "请输入项目名称", "rules": [ { "required": true, "message": "请输入项目名称" } ] }, { "name": "owner", "label": "负责人", "component": "select", "options": [ { "label": "张三", "value": "zhangsan" }, { "label": "李四", "value": "lisi" } ] }, { "name": "startDate", "label": "开始日期", "component": "date-picker", "valueFormat": "YYYY-MM-DD" } ] }字段参数可以按功能分组,常用参数如下表。
| 参数 | 含义 | 示例值 |
|---|---|---|
| name | 字段名,提交数据时的 key | projectName |
| label | 表单标签 | 项目名称 |
| component | 渲染组件类型 | input / select / date-picker |
| placeholder | 占位提示 | 请输入项目名称 |
| options | 下拉选项 | [{ label: '张三', value: 'zhangsan' }] |
| rules | 校验规则 | [{ required: true, message: '请输入项目名称' }] |
| span | 栅格占位宽度 | 12 表示半行 |
4.3 渲染器实现示例(Vue 3)
渲染器的目标很明确:输入 Schema,输出表单。先定义类型。
// src/types/schema.ts export interface FieldRule { required?: boolean message?: string pattern?: RegExp } export interface SchemaField { name: string label: string component: 'input' | 'select' | 'date-picker' | 'number' | 'switch' placeholder?: string options?: Array<{ label: string; value: string | number }> rules?: FieldRule[] valueFormat?: string span?: number } export interface FormSchema { type: 'form' fields: SchemaField[] }然后实现渲染组件。核心是使用动态组件component :is,根据field.component渲染对应表单组件。
<!-- src/components/schema-renderer/SchemaForm.vue --> <template> <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" > <el-row> <el-col v-for="field in schema.fields" :key="field.name" :span="field.span || 8" > <el-form-item :label="field.label" :prop="field.name"> <component :is="resolveComponent(field.component)" v-model="formData[field.name]" v-bind="componentProps(field)" > <el-option v-if="field.component === 'select'" v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" /> </component> </el-form-item> </el-col> </el-row> </el-form> </template> <script setup lang="ts"> import { reactive, computed } from 'vue' import type { FormInstance, FormRules } from 'element-plus' import type { FormSchema, SchemaField } from '@/types/schema' const props = defineProps<{ schema: FormSchema modelValue: Record<string, any> }>() const emit = defineEmits<{ (e: 'update:modelValue', value: Record<string, any>): void }>() const formData = reactive(props.modelValue) const formRules = computed<FormRules>(() => { const rules: FormRules = {} props.schema.fields.forEach((field: SchemaField) => { if (field.rules) { rules[field.name] = field.rules } }) return rules }) function resolveComponent(component: string) { const map: Record<string, string> = { input: 'el-input', select: 'el-select', 'date-picker': 'el-date-picker', number: 'el-input-number', switch: 'el-switch', } return map[component] || 'el-input' } function componentProps(field: SchemaField) { const props: Record<string, any> = { placeholder: field.placeholder || `请输入${field.label}`, } if (field.valueFormat) { props.valueFormat = field.valueFormat } return props } const formRef = ref<FormInstance>() async function validate() { if (!formRef.value) return false return formRef.value.validate() } defineExpose({ validate, formData }) </script>这个渲染器的关键点有两个。第一,动态组件名必须注册或通过全局组件方式引入,否则会报“Failed to resolve component”;第二,prop绑定使用字段名,字段名改变时校验规则才能关联到对应表单项。
4.4 动态路由与菜单
低代码页面通常需要动态注册路由。常见做法是:后端在登录时返回菜单列表,前端把菜单转换成 Vue Router 的可注册路由,并在布局加载时完成注册。
// src/utils/dynamic-router.ts export function buildAsyncRoutes(menus: MenuItem[]) { return menus .filter((menu) => menu.path && menu.component) .map((menu) => ({ path: menu.path, name: menu.name, component: loadComponent(menu.component), meta: { title: menu.name, icon: menu.icon }, })) } function loadComponent(componentPath: string) { // componentPath 示例:lowcode/page-preview/index return () => import(`@/views/${componentPath}.vue`) }这里要注意,动态导入路径在 Vite 中不能使用完全动态变量,可以通过import.meta.glob预先注册所有页面组件,再按映射关系取组件。生成代码时,AI 经常会忽略这一点,导致动态路由无法打包。
5. AI 中心实现:会话、Prompt 与技能编排
5.1 业务闭环
AI 中心的完整流程可以拆成五步:
- 用户发起提问,前端创建或选择会话。
- 前端根据技能选择,把请求发送到
/api/ai/chat。 - 后端读取会话历史、技能配置和知识库检索结果。
- 后端调用大模型接口,返回流式数据或完整结果。
- 前端展示消息,并把消息保存到数据库。
这五步中,最容易出问题的是第 4 步。流式返回能显著提升对话体验,但它要求前后端使用 ReadableStream 或 SSE 协议;如果只是做最小可运行版本,也可以先让后端一次性返回完整结果,后续再升级为流式。
5.2 后端接口设计
接口设计要保持稳定,前端和低代码配置都依赖它。推荐的最小接口集如下。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/ai/session | 创建会话 |
| GET | /api/ai/session | 查询当前用户会话列表 |
| GET | /api/ai/message?sessionId=1 | 查询会话消息 |
| POST | /api/ai/chat | 发送消息并获取回复 |
| POST | /api/ai/knowledge/upload | 上传知识库文档 |
Express 后端示例:
// server/src/routes/ai.ts import { Router } from 'express' import { createSession, listSessions } from '../services/ai-session' import { chat } from '../services/ai-chat' const router = Router() router.post('/session', async (req, res) => { const userId = req.user.id const session = await createSession({ userId, title: req.body.title }) res.json({ code: 0, data: session }) }) router.get('/session', async (req, res) => { const sessions = await listSessions(req.user.id) res.json({ code: 0, data: sessions }) }) router.post('/chat', async (req, res) => { const { sessionId, content, skillCode } = req.body const reply = await chat({ sessionId, content, skillCode, userId: req.user.id }) res.json({ code: 0, data: { sessionId, reply } }) }) export default router服务层的chat函数是核心,它负责组装请求参数。
// server/src/services/ai-chat.ts import { getSessionHistory } from '../models/ai-message' import { getSkill } from '../models/ai-skill' import { searchKnowledge } from './knowledge' export async function chat({ sessionId, content, skillCode, userId }) { const history = await getSessionHistory(sessionId) const skill = await getSkill(skillCode) const knowledge = await searchKnowledge(content, { limit: 3 }) const messages = [ { role: 'system', content: buildSystemPrompt(skill, knowledge), }, ...history.map((m) => ({ role: m.role, content: m.content })), { role: 'user', content }, ] const modelReply = await callModel({ messages }) await saveMessage({ sessionId, role: 'user', content }) await saveMessage({ sessionId, role: 'assistant', content: modelReply }) return modelReply }注意,密钥必须保存在后端,不能出现在前端代码中。前端只传递content和skillCode。
5.3 前端会话交互
前端对话页的核心是消息列表 + 输入框 + 发送按钮。发送后先把用户消息插入列表,再调用接口获取回复,最后把回复追加到消息列表。
<!-- src/views/ai-center/Conversation.vue 片段 --> <template> <div class="conversation"> <div class="message-list"> <div v-for="message in messages" :key="message.id" class="message-item" :class="message.role" > {{ message.content }} </div> </div> <el-input v-model="draft" type="textarea" placeholder="输入你的问题" @keydown.enter.prevent="send" /> <el-button type="primary" @click="send">发送</el-button> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { chat } from '@/api/ai' const messages = ref<MessageItem[]>([]) const draft = ref('') async function send() { const content = draft.value.trim() if (!content) return messages.value.push({ id: Date.now(), role: 'user', content }) draft.value = '' const res = await chat({ sessionId, content, skillCode }) messages.value.push({ id: Date.now() + 1, role: 'assistant', content: res.data.reply }) } </script>这段代码只实现了最小闭环。实际项目还需要处理:加载历史消息、接口异常提示、请求中禁用发送按钮、流式输出时的光标和滚动问题。
5.4 Prompt 与知识库最小实现
AI 中心如果只有对话,价值有限;加入“技能”和“知识库”后,才更像一个可编排的智能中心。技能可以理解为一组预设的系统提示词和调用参数。
{ "skillCode": "project-summary", "skillName": "项目总结生成", "systemPrompt": "你是一名资深项目经理,请根据用户提供的项目信息生成结构清晰的总结报告。", "temperature": 0.3, "maxTokens": 1000 }知识库的最小实现可以不做向量检索,先用关键词匹配文档片段。比如后端把上传的文本切分成段落,根据用户问题中的关键词匹配最相关的片段,再把片段拼进 system prompt。这样在内容和数据量都不大的内部系统中,已经能提供可用的“引用来源”能力。
注意:大模型输出存在不确定性。AI 中心的回复应标记为“AI 生成”,涉及重要决策时不能直接代替人工审核。
6. 运行验证:从本地启动到接口连通
6.1 本地启动步骤
前端和后端需要分别启动。先复制环境变量文件,再启动后端,最后启动前端。
# 后端 cd server cp .env.example .env npm install npm run dev# 前端 cd frontend npm install npm run dev.env.example中至少要包含数据库连接、JWT 密钥、大模型接口地址等配置,结构如下。
# server/.env.example PORT=3000 DATABASE_URL=sqlite:./dev.db JWT_SECRET=please-change-me AI_API_URL=https://your-model-service.example.com/v1/chat/completions AI_API_KEY=your-model-api-key AI_MODEL=default-model-name真实项目中,AI_API_KEY必须保存在服务端环境变量里,不要提交到 Git 仓库。
6.2 验证清单
启动后按功能模块依次验证,不要只看页面有没有打开。
| 功能 | 操作 | 预期结果 |
|---|---|---|
| 登录 | 输入账号密码并登录 | 跳转到首页,菜单正常显示 |
| 路由守卫 | 未登录直接访问/ | 跳转到登录页 |
| 动态菜单 | 用不同角色登录 | 看到不同菜单 |
| 低代码表单 | 打开表单配置页并提交 | 校验规则生效,提交数据正确 |
| AI 对话 | 创建会话并发送问题 | 得到模型回复,消息写入数据库 |
| 知识库 | 上传文档后提问相关关键词 | 回复中引用知识库片段 |
验证时重点看浏览器 Network 面板。关注请求是否发出、状态码是否 2xx、返回体是否符合预期。
6.3 预期错误与确认方式
如果某个功能不通过,按先后顺序确认:
- 前端页面的 Console 是否有报错。
- Network 面板请求是否发出,请求地址是否正确。
- 后端日志是否打印了对应的路由和处理结果。
- 数据库中的数据是否写入,字段是否符合预期。
例如 AI 对话失败时,可以先看请求体里的skillCode是否为空,再看后端日志里调用模型接口的响应状态,最后看数据库里是否有用户消息写入。
7. 常见问题排查
7.1 动态路由刷新后白屏或菜单丢失
现象:登录后正常,刷新页面后白屏,或菜单全部消失。
原因:Vibe Coding 生成的路由守卫通常会在刷新后重新获取菜单,但如果顺序不对,路由注册发生在页面渲染之后,就会白屏。另一种常见原因是 Pinia 状态只在内存中,刷新后用户信息和菜单为空,而守卫里没有重新拉取。
排查路径:
- 在
beforeEach里打印to.path、userStore.menus和已注册路由。 - 确认刷新后是否执行了
fetchMenus。 - 确认动态路由是否在菜单拉取完成后才执行
router.addRoute。
解决方案:
- 将菜单拉取放在全局守卫的
await中。 - 动态路由注册完成后,使用
next({ ...to, replace: true })重新进入目标路由。 - 不要依赖刷新前的内存状态,每次刷新都从后端获取菜单是最稳妥的方式。
7.2 AI 请求跨域或请求失败
现象:前端调用/api/ai/chat报 CORS 错误,或返回 500。
原因:前端开发服务器端口是 5173,后端端口是 3000,跨域问题在本地环境很常见。请求失败也可能是AI_API_URL配置错误,或者模型接口返回了不支持的格式。
排查路径:
- 看浏览器 Network 中请求是否真正发出。
- 看后端日志是否收到请求。
- 看后端调用模型接口时返回了什么错误码。
解决方案:
- 在 Express 中配置 CORS 中间件,或者通过 Vite proxy 转发
/api请求。 - 确认
.env中AI_API_URL能被后端访问。 - 在后端捕获模型异常时,把错误状态和响应体记录到日志,不要只返给前端“服务器错误”。
7.3 Schema 渲染器更新不生效或表单项状态错乱
现象:修改 Schema 后页面没有变化;切换表单类型后,表单项的值残留。
原因:渲染器把modelValue转成响应式对象时,数据来源不统一。如果组件内部直接修改了props.modelValue的引用,可能绕过父组件更新;v-for的 key 使用字段名时,字段删除后旧状态仍可能保留。
排查路径:
- 检查父组件是否把新 Schema 传入渲染器。
- 检查渲染器是否用
watch监听props.schema变化。 - 检查重置表单时是否对整个
formData做重新赋值。
解决方案:
- 使用
watch(() => props.schema, () => { Object.assign(formData, emptyForm()) })在 Schema 变化时重建表单数据。 - 渲染器的
v-for使用field.name作为 key,但删除字段时会丢失对应状态,因此重置时需主动清理。 - 对外暴露
resetFields方法,让调用方明确控制表单重置。
7.4 Vibe Coding 生成代码依赖版本冲突
现象:安装依赖后启动报错,例如 Element Plus 与 Vue 版本不匹配,或 Vite 插件版本要求更高的 Node.js。
原因:Vibe Coding 生成的package.json往往使用“最新版本”或某个示例版本,不会自动适配本地 Node 环境。
排查路径:
- 运行
node -v和npm -v查看环境。 - 查看
npm install的警告日志。 - 访问依赖官网或 npm 页面确认版本兼容要求。
解决方案:
- 不要盲目升级所有依赖,先固定一个已知可用的组合。
- 生成代码后立即查看
package.json,把主版本号确认到位。 - 使用
npm ls检查无效依赖树。
7.5 综合排查顺序
遇到实际问题时,建议按以下顺序排查,避免在错误层反复打转。
- 输入是否符合预期,例如用户输入的 prompt、表单数据、请求参数。
- 文件路径和命名是否正确,包括路由路径、组件路径、导入路径。
- 依赖版本是否匹配,重点看前端框架和 UI 库。
- 配置是否生效,例如环境变量、代理配置、路由守卫。
- 权限、端口、网络、存储是否正确。
- 日志中的异常信息和堆栈位置,确定是前端还是后端问题。
- 框架或工具本身是否存在版本限制,例如 Vite 对动态 import 的限制。
8. Vibe Coding 项目的最佳实践与下一步方向
8.1 提示词模板
把有效提示词沉淀成模板,能明显提高后续项目的启动效率。一个可复用的 Vibe Coding 提示词模板如下:
请基于 {技术栈} 实现 {模块名称},要求: 1. 功能边界:{列出核心功能,不要使用模糊的“尽量完善”} 2. 数据结构:{字段名、类型、关联关系} 3. 接口约定:{路径、方法、请求参数、返回格式} 4. 异常处理:{需要覆盖的错误场景} 5. 代码风格:{类型定义、命名规则、注释要求} 6. 完成后运行 {验证命令},并说明启动步骤。提示词越具体,生成结果越可控。“请你写一个用户管理页面”这样的描述会得到泛泛的代码;而“请实现用户列表接口,字段 id、username、nickname、status,支持分页和关键词搜索,返回 { code, data, message } 结构”才会得到可运行的结果。
8.2 代码审查清单
每次 AI 生成代码后,至少按以下清单检查一遍。
| 审查维度 | 检查内容 |
|---|---|
| 类型安全 | 是否省略了接口返回类型,是否使用any绕过类型检查 |
| 接口一致性 | 前端字段和后端字段是否对齐,是否缺少序列化处理 |
| 权限控制 | 是否只做了前端菜单隐藏,而没有做后端接口鉴权 |
| 密钥安全 | 是否有密钥、Token 被硬编码到前端或提交到 Git |
| 异常处理 | 接口失败、模型超时、数据库连接失败是否有兜底 |
| 日志 | 关键业务操作是否留下可检索日志,敏感信息是否脱敏 |
| 回滚 | 配置变更是否可以通过版本管理回滚 |
其中权限控制最容易出错。前端隐藏按钮并不等于安全,真正的权限判断必须发生在后端。
8.3 从模板到内部平台
这个模板可以继续扩展成更完整的中后台平台,推荐按这个顺序逐步增加能力。
第一个方向是可视化页面设计器。当前版本通过 JSON Schema 配置表单,但配置过程仍然在写 JSON。可以做一个拖拽式设计器,组件拖拽后自动生成 Schema,再把 Schema 保存到数据库。这样,非研发人员也能维护简单页面。
第二个方向是 AI 技能市场。把技能配置从代码中抽离,做成可被人为启用的模块。每个技能拥有独立系统提示词、知识库、参数模板和调用权限,不同部门可以维护自己的技能。
第三个方向是多租户支持。如果后续要服务多个内部团队,用户表、菜单表、配置表都要增加tenant_id字段,并在查询时强制带上租户过滤条件。做多租户时要特别注意数据隔离,避免通过修改链接参数访问其他租户数据。
8.4 学习环境与生产环境的差异
开发环境能跑通,不等于生产环境能稳定运行。两者之间还存在大量工程差异。
| 维度 | 本地开发 | 生产环境 |
|---|---|---|
| 配置 | 使用本地.env | 配置中心或容器环境变量统一管理 |
| 数据库 | SQLite 便于启动 | MySQL / PostgreSQL 且需要备份 |
| 日志 | 控制台输出 | 结构化日志 + 集中采集 |
| 鉴权 | 可省略或简化 | 必须校验 token 过期和权限 |
| 模型调用 | 可选 mock | 需要考虑超时、限流、费用告警 |
| 部署 | 前后端分别启动 | 构建产物 + CI/CD + 容器化 |
| 回滚 | 本地改代码 | 版本发布策略 + 数据库迁移回滚 |
在生产环境接入大模型时,还需要额外关注模型调用的超时时间、输入长度限制、敏感信息过滤和费用控制。不要把模型服务厂商的配置写在代码里,也不要在配置库中保存明文密钥。
这个系列把后台管理模板作为第一个项目,是因为它足够通用,也足够复杂。完成这个项目后,再写业务系统时就可以把精力集中在真正的业务字段和规则上,而不再重复处理布局、权限、路由、表单等基础工程问题。
如果只记住一个核心判断,那就是:Vibe Coding 不是把架构设计也交给 AI,而是让 AI 快速执行你确认过边界和方案的编码任务。低代码引擎和 AI 中心只是手段,这套模板真正沉淀下来的,是数据模型、接口约定和一套稳定的开发流程。