2026山东大学软件学院项目实训(六)——可视化修改功能开发
摘要
本期为AI零代码应用生成平台开发可视化修改核心功能,解决纯文字描述修改定位不准、易出错、沟通成本高的痛点。通过iframe父子页面通信、元素精准捕获、AI提示词拼接、原生全量修改、Vue增量工具调用等技术,实现“点击元素+输入一句话=AI精准修改”,补齐平台从生成、预览、修改、下载到部署的全流程闭环,大幅降低用户使用门槛。
封面图建议
推荐使用:平台可视化编辑操作截图(含编辑模式按钮、元素选中高亮、提示词输入框、预览区效果),突出“所见即所得、点哪改哪”的直观体验。
标签
#山东大学 #项目实训 #AI零代码 #LangChain4j #可视化编辑 #低代码平台
正文
一、本期核心任务
本期重点攻坚平台可视化修改能力,彻底告别“纯文字描述位置”的低效修改方式,核心目标:用户点击网页任意元素,输入修改需求,AI精准定位并完成修改,无需用户描述复杂元素位置,降低操作门槛,提升修改准确率。
二、需求分析
在前期迭代中,平台已实现对话历史记忆、上下文迭代优化功能,但用户修改网站时仍存在明显痛点:
- 定位模糊:仅靠文字描述(如“修改第二个卡片标题”),AI易理解偏差;
- 修改易错:描述不精准时,AI可能修改所有同类元素、改错顺序,甚至破坏页面结构;
- 成本较高:元素复杂时,用户难以清晰描述目标位置,沟通效率低。
典型场景:生成多卡片作品展示页,仅需修改某一张卡片标题。文字描述无法精准定位,而可视化修改可直接点击目标卡片,一步锁定修改范围。
三、竞品调研与方案设计
1. 竞品调研
美团NoCode
- 支持手动编辑、AI提示词编辑双模式;
- 手动编辑:点击元素直接添加临时style,绿色框标记范围;
- AI编辑:拼接元素信息与提示词,传递全量文件;
- 缺点:选择器不精准、部分元素不可编辑、全量文件传输浪费资源。
百度秒哒
- 侧重AI提示词编辑,手动编辑仅支持基础样式;
- 交互实时,支持截图辅助定位,参数传递精简;
- 缺点:同样存在部分元素无法选中、修改范围受限问题。
2. 最终方案(成本与效果最优)
放弃高成本手动编辑器,专注「可视化选中+AI提示词修改」核心模式,流程简洁高效:
- 用户开启「编辑模式」;
- 点击iframe预览页目标元素;
- 前端自动捕获元素信息(标签、ID、类名、CSS选择器、文本、位置);
- 前端将元素信息自动拼接到用户输入的修改需求中;
- 后端接收提示词,调用AI精准修改对应元素;
- 前端刷新预览,实时展示修改结果。
核心优势:开发成本低、逻辑清晰、易维护;
合理权衡:修改精度依赖AI能力,完全适配零代码平台轻量化需求。
3. 关键技术选型
- 跨页面通信:
postMessage实现父页面(平台)与子页面(iframe预览页)数据传递; - 同源处理:Vite配置代理+修改环境变量,解决父子页面同源问题,支持动态注入编辑脚本;
- 元素定位:自动生成精准CSS选择器,完整捕获元素关键信息;
- 修改策略:
- 原生HTML/多文件:全量返回完整文件,简单高效;
- Vue工程:增量工具调用修改,避免全量返回资源浪费。
四、前端开发:可视化交互与通信实现
1. 同源配置(动态注入脚本前提)
父子页面同源是iframe注入JS脚本的必要条件,修改核心配置:
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8123', // 后端地址
changeOrigin: true,
secure: false
}
}
}
})
.env.development
VITE_DEPLOY_DOMAIN=http://localhost
VITE_API_BASE_URL=/api
2. 可视化编辑工具类(visualEditor.ts)
封装脚本注入、事件监听、元素捕获、消息收发核心逻辑,解耦对话页面复杂度:
// 元素信息接口
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?: (info: ElementInfo) => void
}
export class VisualEditor {
private iframe: HTMLIFrameElement | null = null
private isEditMode = false
private options: VisualEditorOptions
constructor(options: VisualEditorOptions = {}) {
this.options = options
}
// 初始化iframe
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(): boolean {
if (this.isEditMode) {
this.disableEditMode()
return false
} else {
this.enableEditMode()
return true
}
}
// iframe加载完成回调
onIframeLoad() {
if (this.isEditMode) {
setTimeout(() => this.injectEditScript(), 500)
}
}
// 处理iframe消息
handleIframeMessage(event: MessageEvent) {
const { type, data } = event.data
if (type === 'ELEMENT_SELECTED') {
this.options.onElementSelected?.(data.elementInfo)
}
}
// 向iframe发送消息
private sendMessageToIframe(msg: Record<string, any>) {
this.iframe?.contentWindow?.postMessage(msg, '*')
}
// 注入编辑脚本
private injectEditScript() {
if (!this.iframe) return
const script = document.createElement('script')
script.id = 'visual-edit-script'
script.textContent = this.generateEditScript()
this.iframe.contentDocument?.head.appendChild(script)
}
// 生成编辑脚本(悬浮/选中样式、事件监听、消息发送)
private generateEditScript(): string {
return `(function(){
let isEditMode = true;
let currentHover = null;
let currentSelected = null;
// 注入悬浮、选中样式
const style = document.createElement('style');
style.textContent = \`
.edit-hover { outline: 2px dashed #1890ff !important; outline-offset: 2px !important; }
.edit-selected { outline: 3px solid #52c41a !important; outline-offset: 2px !important; }
\`;
document.head.appendChild(style);
// 生成精准CSS选择器
function generateSelector(el) {
const path = [];
let cur = el;
while (cur && cur !== document.body) {
let sel = cur.tagName.toLowerCase();
if (cur.id) { sel += '#' + cur.id; path.unshift(sel); break; }
if (cur.className) sel += '.' + cur.className.split(' ').join('.');
const idx = Array.from(cur.parentNode.children).indexOf(cur) + 1;
sel += ':nth-child(' + idx + ')';
path.unshift(sel);
cur = cur.parentNode;
}
return path.join('>');
}
// 获取元素完整信息
function getElementInfo(el) {
const rect = el.getBoundingClientRect();
return {
tagName: el.tagName,
id: el.id,
className: el.className,
textContent: el.textContent?.trim().substring(0,100) || '',
selector: generateSelector(el),
pagePath: window.location.search + window.location.hash,
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
};
}
// 悬浮监听
document.addEventListener('mouseover', e => {
if (!isEditMode) return;
const t = e.target;
if (t === document.body) return;
if (currentHover) currentHover.classList.remove('edit-hover');
t.classList.add('edit-hover');
currentHover = t;
}, true);
// 点击选中监听
document.addEventListener('click', e => {
if (!isEditMode) return;
e.preventDefault();
const t = e.target;
if (t === document.body) return;
if (currentSelected) currentSelected.classList.remove('edit-selected');
t.classList.add('edit-selected');
currentSelected = t;
// 向父页面发送元素信息
const info = getElementInfo(t);
window.parent.postMessage({ type: 'ELEMENT_SELECTED', data: { elementInfo: info } }, '*');
}, true);
})();`
}
}
3. 对话页面集成(AppChatPage.vue核心逻辑)
添加编辑模式按钮、选中元素信息展示区、提示词自动拼接功能:
提示词拼接核心代码
// 发送消息时,自动拼接选中元素信息
let message = userInput.value.trim()
if (selectedElementInfo.value) {
const info = selectedElementInfo.value
message += `
选中元素信息:
- 标签:${info.tagName.toLowerCase()}
- 选择器:${info.selector}
- 页面路径:${info.pagePath}
- 当前内容:${info.textContent || '无'}`
}
交互效果
- 悬浮元素:蓝色虚线边框高亮;
- 点击选中:绿色实线边框锁定;
- 编辑提示:开启模式后右上角弹出提示,3秒自动消失;
- 信息展示:选中后顶部Alert显示元素详情,支持一键清除。
五、后端开发:AI精准修改逻辑
后端核心工具开发(完整可运行)
文件读取工具
@Tool("读取项目中的文件内容")
public String readFile(
@P("文件相对路径,例如 src/App.vue") String filePath,
@ToolMemoryId Long appId
) {
Path fullPath = getProjectPath(appId, filePath);
if (!Files.exists(fullPath)) {
return "文件不存在:" + filePath;
}
try {
return Files.readString(fullPath);
} catch (Exception e) {
return "读取失败:" + e.getMessage();
}
}
文件修改工具(最核心)
@Tool("用新内容替换文件中的旧内容,实现精准修改")
public String modifyFile(
@P("文件相对路径") String filePath,
@P("需要被替换的原文") String oldContent,
@P("替换后的新内容") String newContent,
@ToolMemoryId Long appId
) {
Path fullPath = getProjectPath(appId, filePath);
String fileContent = Files.readString(fullPath);
if (!fileContent.contains(oldContent)) {
return "未找到要替换的内容,请检查代码";
}
String updated = fileContent.replace(oldContent, newContent);
Files.writeString(fullPath, updated);
return "✅ 修改成功:" + filePath;
}
目录查看工具
@Tool("查看项目目录结构")
public String listDir(
@P("目录路径") String dirPath,
@ToolMemoryId Long appId
) {
Path fullPath = getProjectPath(appId, dirPath);
if (!Files.exists(fullPath)) {
return "目录不存在:" + dirPath;
}
try {
StringBuilder sb = new StringBuilder();
Files.walk(fullPath, 2)
.filter(Files::isRegularFile)
.forEach(path -> sb.append(path.getFileName()).append("\n"));
return sb.length() == 0 ? "目录为空" : sb.toString();
} catch (Exception e) {
return "获取目录失败:" + e.getMessage();
}
}
文件删除工具(带安全限制)
@Tool("删除无用文件")
public String deleteFile(
@P("文件相对路径") String filePath,
@ToolMemoryId Long appId
) {
Path fullPath = getProjectPath(appId, filePath);
String fileName = fullPath.getFileName().toString();
List<String> forbiddenFiles = Arrays.asList(
"package.json",
"index.html",
"vite.config.js",
"main.js",
"main.ts"
);
if (forbiddenFiles.contains(fileName)) {
return "❌ 禁止删除核心文件:" + filePath;
}
try {
Files.delete(fullPath);
return "✅ 删除成功:" + filePath;
} catch (Exception e) {
return "❌ 删除失败:" + e.getMessage();
}
}
项目路径拼接工具方法
private Path getProjectPath(Long appId, String relativePath) {
String projectDir = "vue_project_" + appId;
return Paths.get(AppConstant.CODE_OUTPUT_ROOT_DIR, projectDir, relativePath)
.normalize()
.toAbsolutePath();
}
2. Vue工程增量修改(工具调用)
Vue工程代码量大,全量返回效率低,基于LangChain4j工具调用能力,实现“读文件→查结构→改片段→写文件”的精准增量修改。
核心工具开发(Java)
文件读取工具
@Tool("读取指定文件内容")
public String readFile(
@P("文件相对路径") String relativeFilePath,
@ToolMemoryId Long appId
)
文件修改工具(核心)
@Tool("修改文件内容,用新内容替换指定旧内容")
public String modifyFile(
@P("文件相对路径") String relativeFilePath,
@P("要替换的旧内容") String oldContent,
@P("替换后的新内容") String newContent,
@ToolMemoryId Long appId
)
安全限制:新增文件删除、目录读取、文件写入工具,禁止删除package.json、index.html、main.js等核心文件,保障项目安全。
工具调用流程
- AI接收用户提示词(含选中元素信息+修改需求);
- 调用
FileDirReadTool:读取项目目录结构,理解项目组织; - 调用
FileReadTool:读取目标组件文件,查看现有代码; - 调用
FileModifyTool:精准替换目标元素对应代码片段; - 返回修改结果,前端刷新预览,展示修改效果。
工具输出优化
统一工具调用输出格式,区分不同工具执行结果,提升用户感知:
- 文件读取:
[工具调用] 读取文件:src/pages/Resume.vue; - 文件修改:
[工具调用] 修改文件:src/pages/Resume.vue \n 替换前:xxx \n 替换后:xxx; - 文件删除:
[工具调用] 删除文件:src/utils/temp.js。
六、功能测试
1. 原生HTML测试
- 操作:开启编辑→点击个人主页标题→输入“修改为:李鱼皮的个人主页”→发送;
- 结果:标题精准修改,页面样式、其他内容无任何变化,预览实时更新。
2. Vue工程测试
- 操作:开启编辑→选中简历页“教育背景”文本→输入“修改为:清华大学美术学院视觉传达设计专业,硕士学位”→发送;
- 结果:AI自动调用读文件、改文件工具,仅替换目标文本,Vue项目正常运行,无报错。
3. 图片修改测试
- 操作:开启编辑→选中作品展示区第一张图片→输入“替换为自然风光高清图片”→发送;
- 结果:图片成功替换,自动适配页面布局,样式正常、交互流畅。
七、开发总结
本期完成平台可视化修改全链路功能开发,彻底解决纯文字修改的痛点,覆盖原生HTML、多文件、Vue工程全场景适配:
- 技术落地:掌握
postMessage跨页面通信、Vite同源代理配置;精通LangChain4j工具调用,实现Vue工程增量精准修改; - 工程优化:前后端逻辑解耦,前端封装独立编辑工具类,后端统一工具管理;优化提示词约束与输出格式,大幅提升AI修改准确率;
- 能力闭环:完善平台“生成-预览-可视化修改-下载-部署”全流程,降低用户定制门槛,显著提升平台核心竞争力。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)