2026 山东大学软件学院项目实训(八)——可视化修改模块
目录
团队信息
-
组号:69 组
-
项目:AI 零代码应用生成平台
-
负责人:樊伟彤
-
成员:者亚杰、蒋宇轩、张旭、李重昊
一、本期核心任务
本期重点开发平台可视化修改核心能力,解决纯文字描述修改定位不准、易改错、沟通成本高的痛点,经过本次开发做到:
点击网页元素 + 输入一句话=AI精准修改,大幅降低用户使用门槛,完成从生成到精细化定制的闭环。
二、需求分析
在前面迭代中,平台已经支持对话记忆、上下文迭代优化。但用户修改网站时,仍存在明显问题:
-
只能靠文字描述位置,比如“改第二个卡片标题”
-
描述稍模糊,AI就容易改错位置、改错内容、甚至改崩页面
-
元素多时,用户很难说清是哪个元素
因此,我们需要实现让用户能够直接点击网页上的元素,然后输入修改需求,AI就能精确知道要修改的是哪个部分:可视化选中元素 + AI 提示词修改。
三、方案设计
1.方案选择
我们梳理出几种实现方案:
方案一:手动可视化编辑方案
想要实现该方案,需要前端搭建完整的可视化代码编辑器,还需要自行制定一套DSL规范,用来规范代码修改逻辑。
整体开发难度大,中间存在很多未知问题,开发周期长、成本偏高,对于本项目而言实用性不强,有些舍本逐末。
方案二:全量文件传输后端处理方案
即将项目所有文件统一传递到后端进行处理。
这种方式虽然稳定性较高,但会产生大量网络数据传输,同时加重后端运算压力,资源消耗大,并不适合我们目前的服务器配置条件。
方案三:精准定位源码位置编辑方案
该思路是获取页面元素后,直接锁定元素在项目源码里的具体位置进行修改。
但实际开发中局限性很大,尤其是经过打包编译的Vue项目,很难精准匹配源码位置,极易引发各类程序错误,稳定性较差。
2.最终选定方案
综合以上三种方案存在的各类问题,我们最终确定采用可视化选择+AI提示词编辑的实现方式。
不做复杂手动编辑器,专注:可视化选中+ AI 提示词修改 流程:
-
用户开启「编辑模式」
-
点击预览页(iframe)上任意元素
-
前端自动抓取元素信息:标签、id、class、CSS选择器、文本、位置
-
把元素信息自动拼接到用户输入
-
后端交给AI,精准修改对应元素
-
前端刷新预览,完成修改
优点:成本低、逻辑清晰、好维护。
权衡:修改精度依赖AI,但对零代码平台完全够用。
3.关键技术
-
父子页面通信:
postMessage -
同源问题:Vite代理 + 相对路径 API
-
元素定位:自动生成精准 CSS 选择器
-
修改策略:
-
原生 HTML / 多文件:全量返回完整文件
-
Vue 项目:增量工具调用修改
-
四、前端开发:可视化编辑交互
1.同源配置
在进入可视化编辑模式时,父网站会动态向子网站注入代码,但这一操作必须要求父子网站同源才能正常执行。
为了解决同源限制问题,我们在前端通过配置代理实现父子网站同源:
本地开发环境使用 Vite 配置代理后端接口,线上环境则通过 Nginx 实现相同效果,具体修改 vite.config.ts 即可完成配置。
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
},
},
})
修改环境配置 .env.development 中本地请求后端的地址为相对目录:
VITE_DEPLOY_DOMAIN=http://localhost
VITE_API_BASE_URL=/api
这样一来,前端请求后端接口时,因为没有使用绝对路径,会自动请求与前端同域名、同端口的地址,再通过 Vite 代理转发到真实的后端服务地址。
这套方案完美解决了父子网站同源限制问题,让父网站可以正常向子网站动态注入代码,也不需要让 AI 额外生成兼容代码,整体实现更简洁稳定。
2.可视化编辑器工具类
可视化编辑文件负责定义子父网站通讯事件,由父组件向子组件注入代码。这个文件极其复杂,封装了所有与可视化编辑相关的逻辑,包括脚本注入、事件监听、元素选择等功能。
/**
* 可视化编辑器工具类
* 负责管理iframe内的可视化编辑功能
*/
export interface ElementInfo {
tagName: string
id: string
className: string
textContent: string
selector: string
pagePath: string
rect: {
top: number
left: number
width: number
height: number
}
}
export interface VisualEditorOptions {
onElementSelected?: (elementInfo: ElementInfo) => void
onElementHover?: (elementInfo: ElementInfo) => void
}
export class VisualEditor {
private iframe: HTMLIFrameElement | null = null
private isEditMode = false
private options: VisualEditorOptions
constructor(options: VisualEditorOptions = {}) {
this.options = options
}
/**
* 初始化编辑器
*/
init(iframe: HTMLIFrameElement) {
this.iframe = iframe
}
/**
* 开启编辑模式
*/
enableEditMode() {
if (!this.iframe) {
return
}
this.isEditMode = true
setTimeout(() => {
this.injectEditScript()
}, 300)
}
/**
* 关闭编辑模式
*/
disableEditMode() {
this.isEditMode = false
this.sendMessageToIframe({
type: 'TOGGLE_EDIT_MODE',
editMode: false,
})
// 清除所有编辑状态
this.sendMessageToIframe({
type: 'CLEAR_ALL_EFFECTS',
})
}
/**
* 切换编辑模式
*/
toggleEditMode() {
if (this.isEditMode) {
this.disableEditMode()
} else {
this.enableEditMode()
}
return this.isEditMode
}
/**
* 强制同步状态并清理
*/
syncState() {
if (!this.isEditMode) {
this.sendMessageToIframe({
type: 'CLEAR_ALL_EFFECTS',
})
}
}
/**
* 清除选中的元素
*/
clearSelection() {
this.sendMessageToIframe({
type: 'CLEAR_SELECTION',
})
}
/**
* iframe 加载完成时调用
*/
onIframeLoad() {
if (this.isEditMode) {
setTimeout(() => {
this.injectEditScript()
}, 500)
} else {
// 确保非编辑模式时清理状态
setTimeout(() => {
this.syncState()
}, 500)
}
}
/**
* 处理来自 iframe 的消息
*/
handleIframeMessage(event: MessageEvent) {
const { type, data } = event.data
switch (type) {
case 'ELEMENT_SELECTED':
if (this.options.onElementSelected && data.elementInfo) {
this.options.onElementSelected(data.elementInfo)
}
break
case 'ELEMENT_HOVER':
if (this.options.onElementHover && data.elementInfo) {
this.options.onElementHover(data.elementInfo)
}
break
}
}
/**
* 向 iframe 发送消息
*/
private sendMessageToIframe(message: Record<string, any>) {
if (this.iframe?.contentWindow) {
this.iframe.contentWindow.postMessage(message, '*')
}
}
/**
* 注入编辑脚本到 iframe
*/
private injectEditScript() {
if (!this.iframe) return
const waitForIframeLoad = () => {
try {
if (this.iframe!.contentWindow && this.iframe!.contentDocument) {
// 检查是否已经注入过脚本
if (this.iframe!.contentDocument.getElementById('visual-edit-script')) {
this.sendMessageToIframe({
type: 'TOGGLE_EDIT_MODE',
editMode: true,
})
return
}
const script = this.generateEditScript()
const scriptElement = this.iframe!.contentDocument.createElement('script')
scriptElement.id = 'visual-edit-script'
scriptElement.textContent = script
this.iframe!.contentDocument.head.appendChild(scriptElement)
} else {
setTimeout(waitForIframeLoad, 100)
}
} catch {
// 静默处理注入失败
}
}
waitForIframeLoad()
}
/**
* 生成编辑脚本内容
*/
private generateEditScript() {
//...省略
}
}
3.对话页面集成
选中元素后,自动把元素信息拼接到prompt
let message = userInput.value.trim()
// 有选中元素,自动追加上下文
if (selectedElementInfo.value) {
const info = selectedElementInfo.value
message += `
选中元素信息:
- 标签:${info.tagName.toLowerCase()}
- 选择器:${info.selector}
- 页面路径:${info.pagePath}
- 当前内容:${info.textContent || '无'}`
}
前端交互效果:
-
悬浮:蓝色虚线框
-
选中:绿色实线框
-
顶部提示:编辑模式已开启
五、工程项目增量修改
1. 方案设计
在 Vue 工程项目中,生成的代码量通常非常大,如果每次修改都让 AI 从零返回完整文件内容,效率极低且不现实。因此我们采用AI 工具调用能力,让大模型通过工具实现精准的增量修改,而不是全量重写。
为此,我们为 AI 提供了一系列专用工具,每个工具独立封装为一个类,方便调用:
-
读取单个文件,让 AI 能够查看项目中已有的代码内容
-
递归获取目录下的所有文件结构,帮助 AI 理解项目整体组织
-
删除单个文件,用于清理无用或错误的文件
-
修改单个文件,支持通过匹配旧内容、替换新内容的方式实现精准更新
-
创建单个文件(该功能已提前实现)
同时,我们还需要优化提示词,在提示词底部补充修改规则,明确告诉 AI 如何使用这些工具完成精确的代码修改:
## 特别注意
在生成代码后,用户可能会提出修改要求并给出要修改的元素信息。
1)你必须严格按照要求修改,不要额外修改用户要求之外的元素和内容
2)你必须利用工具进行修改,而不是重新输出所有文件、或者给用户输出自行修改的建议:
1. 首先使用【目录读取工具】了解当前项目结构
2. 使用【文件读取工具】查看需要修改的文件内容
3. 根据用户需求,使用对应的工具进行修改:- 【文件修改工具】:修改现有文件的部分内容- 【文件写入工具】:创建新文件或完全重写文件- 【文件删除工具】:删除不需要的文件
2.工具开发
为了提升用户体验,每个 AI 工具的入参和输出格式都需要做区分。例如修改文件这个工具,就需要清晰展示文件相对路径、被替换的旧内容、以及更新后的新内容。
如果直接在处理 AI 流式返回的代码里用大量 if else 判断工具类型,逻辑会变得非常混乱且难以维护。因此我们采用了策略模式 + 工厂模式结合的设计思路:把每个工具类当作一个独立策略,各自实现专属的信息展示与输出方法;再通过一个工厂类统一创建、管理所有工具,让代码结构更清晰、扩展性更强,也方便后续新增工具。

1、工具基类
定义所有工具必须实现的方法:
/**
* 工具基类
* 定义所有工具的通用接口
*/
public abstract class BaseTool {
/**
* 获取工具的英文名称(对应方法名)
*
* @return 工具英文名称
*/
public abstract String getToolName();
/**
* 获取工具的中文显示名称
*
* @return 工具中文名称
*/
public abstract String getDisplayName();
/**
* 生成工具请求时的返回值(显示给用户)
*
* @return 工具请求显示内容
*/
public String generateToolRequestResponse() {
return String.format("\n\n[选择工具] %s\n\n", getDisplayName());
}
/**
* 生成工具执行结果格式(保存到数据库)
*
* @param arguments 工具执行参数
* @return 格式化的工具执行结果
*/
public abstract String generateToolExecutedResult(JSONObject arguments);
}
2、工具管理类
创建工具管理类,自动注册所有的工具 Bean,并且提供了根据名称获取工具的方法。
本质上就是维护一个工具 Map,代码如下:
/**
* 工具管理器
* 统一管理所有工具,提供根据名称获取工具的功能
*/
@Slf4j
@Component
public class ToolManager {
/**
* 工具名称到工具实例的映射
*/
private final Map<String, BaseTool> toolMap = new HashMap<>();
/**
* 自动注入所有工具
*/
@Resource
private BaseTool[] tools;
/**
* 初始化工具映射
*/
@PostConstruct
public void initTools() {
for (BaseTool tool : tools) {
toolMap.put(tool.getToolName(), tool);
log.info("注册工具: {} -> {}", tool.getToolName(), tool.getDisplayName());
}
log.info("工具管理器初始化完成,共注册 {} 个工具", toolMap.size());
}
/**
* 根据工具名称获取工具实例
*
* @param toolName 工具英文名称
* @return 工具实例
*/
public BaseTool getTool(String toolName) {
return toolMap.get(toolName);
}
/**
* 获取已注册的工具集合
*
* @return 工具实例集合
*/
public BaseTool[] getAllTools() {
return tools;
}
}
3、修改流处理逻辑
最后修改流处理逻辑,从 AI 响应中获取到执行的工具名称,然后通过T oolManager获取到对应的工具实例,并通过调用方法来输出信息:
@Resource
private ToolManager toolManager;
case TOOL_REQUEST -> {
ToolRequestMessage toolRequestMessage = JSONUtil.toBean(chunk, ToolRequestMessage.class);
String toolId = toolRequestMessage.getId();
String toolName = toolRequestMessage.getName();
// 检查是否是第一次看到这个工具 ID
if (toolId != null && !seenToolIds.contains(toolId)) {
// 第一次调用这个工具,记录 ID 并返回工具信息
seenToolIds.add(toolId);
// 根据工具名称获取工具实例
BaseTool tool = toolManager.getTool(toolName);
// 返回格式化的工具调用信息
return tool.generateToolRequestResponse();
} else {
// 不是第一次调用这个工具,直接返回空
return "";
}
}
case TOOL_EXECUTED -> {
ToolExecutedMessage toolExecutedMessage = JSONUtil.toBean(chunk, ToolExecutedMessage.class);
String toolName = toolExecutedMessage.getName();
JSONObject jsonObject = JSONUtil.parseObj(toolExecutedMessage.getArguments());
// 根据工具名称获取工具实例并生成相应的结果格式
BaseTool tool = toolManager.getTool(toolName);
String result = tool.generateToolExecutedResult(jsonObject);
// 输出前端和要持久化的内容
String output = String.format("\n\n%s\n\n", result);
chatHistoryStringBuilder.append(output);
return output;
}
六、总结
本期聚焦平台可视化修改核心能力开发,在原有AI零代码生成基础上,补齐了从生成到精细化定制的关键闭环,主要收获如下:
-
可视化交互落地:通过父子页面通信、同源配置与精准CSS选择器生成,实现了「点击选中元素+AI提示词修改」的核心交互,解决了纯文字描述修改定位不准、易改错的痛点,大幅降低用户使用门槛。
-
跨域问题解决:通过Vite代理(本地)+Nginx配置(线上)统一父子网站域名端口,完美解决同源限制,保障元素选择、脚本注入等核心功能的稳定运行。
七、后续计划
1.精度提升:优化CSS选择器生成逻辑,结合元素层级与唯一标识减少选择器冲突,进一步提升AI修改的精准度。
2.AI 工作流:统一 AI 工作流,支持原生 HTML、多文件项目、Vue 工程等不同类型项目的无缝修改。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)