摘要

本期为AI零代码应用生成平台开发可视化修改核心功能,解决纯文字描述修改定位不准、易出错、沟通成本高的痛点。通过iframe父子页面通信、元素精准捕获、AI提示词拼接、原生全量修改、Vue增量工具调用等技术,实现“点击元素+输入一句话=AI精准修改”,补齐平台从生成、预览、修改、下载到部署的全流程闭环,大幅降低用户使用门槛。

封面图建议

推荐使用:平台可视化编辑操作截图(含编辑模式按钮、元素选中高亮、提示词输入框、预览区效果),突出“所见即所得、点哪改哪”的直观体验。

标签

#山东大学 #项目实训 #AI零代码 #LangChain4j #可视化编辑 #低代码平台

正文

一、本期核心任务

本期重点攻坚平台可视化修改能力,彻底告别“纯文字描述位置”的低效修改方式,核心目标:用户点击网页任意元素,输入修改需求,AI精准定位并完成修改,无需用户描述复杂元素位置,降低操作门槛,提升修改准确率。

二、需求分析

在前期迭代中,平台已实现对话历史记忆、上下文迭代优化功能,但用户修改网站时仍存在明显痛点:

  1. 定位模糊:仅靠文字描述(如“修改第二个卡片标题”),AI易理解偏差;
  2. 修改易错:描述不精准时,AI可能修改所有同类元素、改错顺序,甚至破坏页面结构;
  3. 成本较高:元素复杂时,用户难以清晰描述目标位置,沟通效率低。

典型场景:生成多卡片作品展示页,仅需修改某一张卡片标题。文字描述无法精准定位,而可视化修改可直接点击目标卡片,一步锁定修改范围。

三、竞品调研与方案设计

1. 竞品调研
美团NoCode
  • 支持手动编辑、AI提示词编辑双模式;
  • 手动编辑:点击元素直接添加临时style,绿色框标记范围;
  • AI编辑:拼接元素信息与提示词,传递全量文件;
  • 缺点:选择器不精准、部分元素不可编辑、全量文件传输浪费资源。
百度秒哒
  • 侧重AI提示词编辑,手动编辑仅支持基础样式;
  • 交互实时,支持截图辅助定位,参数传递精简;
  • 缺点:同样存在部分元素无法选中、修改范围受限问题。
2. 最终方案(成本与效果最优)

放弃高成本手动编辑器,专注「可视化选中+AI提示词修改」核心模式,流程简洁高效:

  1. 用户开启「编辑模式」;
  2. 点击iframe预览页目标元素;
  3. 前端自动捕获元素信息(标签、ID、类名、CSS选择器、文本、位置);
  4. 前端将元素信息自动拼接到用户输入的修改需求中;
  5. 后端接收提示词,调用AI精准修改对应元素;
  6. 前端刷新预览,实时展示修改结果。

核心优势:开发成本低、逻辑清晰、易维护;
合理权衡:修改精度依赖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等核心文件,保障项目安全。

工具调用流程
  1. AI接收用户提示词(含选中元素信息+修改需求);
  2. 调用FileDirReadTool:读取项目目录结构,理解项目组织;
  3. 调用FileReadTool:读取目标组件文件,查看现有代码;
  4. 调用FileModifyTool:精准替换目标元素对应代码片段;
  5. 返回修改结果,前端刷新预览,展示修改效果。
工具输出优化

统一工具调用输出格式,区分不同工具执行结果,提升用户感知:

  • 文件读取:[工具调用] 读取文件: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工程全场景适配:

  1. 技术落地:掌握postMessage跨页面通信、Vite同源代理配置;精通LangChain4j工具调用,实现Vue工程增量精准修改;
  2. 工程优化:前后端逻辑解耦,前端封装独立编辑工具类,后端统一工具管理;优化提示词约束与输出格式,大幅提升AI修改准确率;
  3. 能力闭环:完善平台“生成-预览-可视化修改-下载-部署”全流程,降低用户定制门槛,显著提升平台核心竞争力。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐