AI 应用生成平台:前端与可视化

六、前端与可视化

项目预览:https://www.joinoai.cloud

项目仓库:https://github.com/vasc-language/ai-code-mother

项目仓库:https://gitee.com/vasc-language/ai-code-mother

31. AI 零代码应用生成项目中,你在后端如何实现可视化修改功能?

基于项目代码分析,后端的可视化修改功能主要通过以下方式实现:

1. 工具系统架构

项目使用LangChain4j的工具调用机制,AI可以通过工具来实现文件的读取、修改和写入:

  • FileWriteTool: 文件写入工具
  • FileReadTool: 文件读取工具
  • FileModifyTool: 文件修改工具

2. 可视化编辑流程

// 用户在前端选中元素 → 生成选择器 → 发送修改指令 → AI调用工具修改代码
@Tool("修改HTML文件中指定元素的内容或样式")
public String modifyElement(
    @P("文件路径") String filePath,
    @P("元素选择器") String selector,
    @P("修改内容") String modification
) {
    // 1. 读取HTML文件
    String htmlContent = FileReadTool.readFile(filePath);

    // 2. 解析DOM,定位元素
    Document document = Jsoup.parse(htmlContent);
    Elements elements = document.select(selector);

    // 3. 应用修改
    elements.forEach(element -> {
        // 根据modification类型执行不同操作
        applyModification(element, modification);
    });

    // 4. 保存修改后的文件
    FileWriteTool.writeFile(filePath, document.html());
    return "元素修改成功";
}

3. 安全控制机制

  • 路径白名单:限制只能修改应用目录下的文件
  • 文件类型检查:只允许修改HTML、CSS、JS等前端文件
  • 元素验证:验证选择器的合法性,防止恶意修改

33. AI 零代码应用生成项目中,实现对话历史功能时,前端如何配合后端游标分页机制进行数据请求?

基于AppChatPage.vue的分析,对话历史的游标分页机制实现如下:

1. 前端分页状态管理

// 历史消息分页相关
const hasMoreHistory = ref(true)      // 是否有更多历史消息
const loadingHistory = ref(false)     // 正在加载历史状态
const oldestMessageId = ref<number>() // 最旧消息的ID(游标)

2. 加载更多历史消息逻辑

const loadMoreHistory = async () => {
  if (loadingHistory.value || !hasMoreHistory.value) return

  loadingHistory.value = true
  try {
    // 使用游标分页请求
    const res = await listChatHistoryByPage({
      appId: appId.value,
      pageSize: 20,
      cursor: oldestMessageId.value,  // 游标:最旧消息ID
      sortOrder: 'desc'               // 按时间倒序
    })

    if (res.data.code === 0) {
      const historyList = res.data.data?.records || []

      if (historyList.length > 0) {
        // 处理历史消息格式
        const formattedMessages = historyList.map(item => ({
          type: item.messageType === 'USER' ? 'user' : 'ai',
          content: item.message,
          timestamp: item.createTime
        }))

        // 插入到消息列表开头(因为是历史消息)
        messages.value.unshift(...formattedMessages.reverse())

        // 更新游标为最旧的消息ID
        oldestMessageId.value = historyList[historyList.length - 1].id

        // 检查是否还有更多数据
        hasMoreHistory.value = historyList.length === 20
      } else {
        hasMoreHistory.value = false
      }
    }
  } finally {
    loadingHistory.value = false
  }
}

3. 游标分页UI展示

<template>
  <div class="messages-container" ref="messagesContainer">
    <!-- 加载更多按钮 -->
    <div v-if="hasMoreHistory" class="load-more-container">
      <a-button
        type="link"
        @click="loadMoreHistory"
        :loading="loadingHistory"
        size="small"
      >
        加载更多历史消息
      </a-button>
    </div>

    <!-- 消息列表 -->
    <div v-for="(message, index) in messages" :key="index" class="message-item">
      <!-- 消息内容 -->
    </div>
  </div>
</template>

4. 后端游标分页接口设计

@PostMapping("/listByPage")
public BaseResponse<Page<ChatHistoryVO>> listChatHistoryByPage(
    @RequestBody ChatHistoryQueryRequest request
) {
    // 游标分页查询
    QueryWrapper<ChatHistory> queryWrapper = QueryWrapper.create()
        .eq(ChatHistory::getAppId, request.getAppId())
        .lt(request.getCursor() != null, ChatHistory::getId, request.getCursor()) // 游标条件
        .orderBy(ChatHistory::getCreateTime, false) // 按时间倒序
        .limit(request.getPageSize()); // 限制条数

    List<ChatHistory> records = chatHistoryService.list(queryWrapper);

    return ResultUtils.success(new Page<>(records, records.size()));
}

5. 游标分页优势

  • 性能优化:避免OFFSET深分页问题,查询性能稳定
  • 数据一致性:即使有新消息插入,历史消息加载不会重复
  • 无限滚动:支持平滑的历史消息加载体验
  • 内存友好:按需加载,不会一次性加载所有历史记录

34. AI 零代码应用生成项目中,实现可视化编辑功能时,前端如何捕获并传递 iframe 内的用户点击事件给父页面?

基于项目代码分析,虽然当前代码中没有直接使用iframe,但可视化编辑功能的实现原理如下:

1. 跨域通信机制(postMessage)

// iframe内页面:发送点击事件到父页面
const handleElementClick = (event) => {
  const element = event.target

  // 构建元素信息
  const elementInfo = {
    type: 'ELEMENT_CLICKED',
    data: {
      tagName: element.tagName,
      id: element.id,
      className: element.className,
      textContent: element.textContent?.substring(0, 100),
      innerHTML: element.innerHTML,
      selector: generateSelector(element),
      boundingRect: element.getBoundingClientRect(),
      styles: getComputedStyle(element)
    }
  }

  // 发送到父页面
  window.parent.postMessage(elementInfo, '*')

  // 阻止默认行为和冒泡
  event.preventDefault()
  event.stopPropagation()
}

// 在iframe内页面添加点击监听
document.addEventListener('click', handleElementClick, true)

2. 父页面接收iframe消息

// AppChatPage.vue 中的消息接收逻辑
const setupIframeMessageListener = () => {
  window.addEventListener('message', (event) => {
    // 验证来源(安全检查)
    if (!isValidOrigin(event.origin)) return

    const { type, data } = event.data

    if (type === 'ELEMENT_CLICKED' && isEditMode.value) {
      // 更新选中元素信息
      selectedElementInfo.value = {
        tagName: data.tagName,
        id: data.id,
        className: data.className,
        textContent: data.textContent,
        selector: data.selector,
        boundingRect: data.boundingRect,
        styles: data.styles
      }

      // 显示选中元素面板
      showSelectedElementPanel()

      // 可选:高亮显示选中元素
      highlightSelectedElement(data.selector)
    }
  })
}

3. 安全考虑

// 验证消息来源
const isValidOrigin = (origin: string) => {
  const allowedOrigins = [
    window.location.origin,
    'http://localhost:8123',
    // 其他允许的域名
  ]
  return allowedOrigins.includes(origin)
}

// Content Security Policy 设置
const cspMeta = document.createElement('meta')
cspMeta.httpEquiv = 'Content-Security-Policy'
cspMeta.content = "frame-src 'self' http://localhost:8123"
document.head.appendChild(cspMeta)

4. 实际项目实现方式

根据AppChatPage.vue的代码,项目采用了更直接的方式:在同域名下展示生成的页面,通过直接的DOM操作来实现可视化编辑,避免了iframe跨域通信的复杂性。

这种方式的优势:

  • 避免跨域问题
  • 更好的性能表现
  • 更简单的事件处理
  • 更直接的DOM操作能力

35. AI 零代码应用生成项目中,当用户开启可视化编辑并选中元素后,前端如何生成稳定且唯一的选择器?

基于AppChatPage.vue的代码分析,元素选择器生成采用了多层级策略:

1. 选择器生成优先级策略

const generateElementSelector = (element: HTMLElement): string => {
  // 第一优先级:ID选择器(最稳定)
  if (element.id && element.id.trim() !== '') {
    return `#${element.id}`
  }

  // 第二优先级:唯一class组合
  if (element.className && element.className.trim() !== '') {
    const classes = element.className.trim().split(/\s+/).filter(Boolean)
    const classSelector = element.tagName.toLowerCase() + '.' + classes.join('.')

    // 验证选择器唯一性
    if (document.querySelectorAll(classSelector).length === 1) {
      return classSelector
    }
  }

  // 第三优先级:结构化路径选择器
  return generatePathSelector(element)
}

2. 结构化路径选择器生成

const generatePathSelector = (element: HTMLElement): string => {
  const path: string[] = []
  let current = element

  while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
    let selector = current.tagName.toLowerCase()

    // 如果有ID,直接使用并终止路径构建
    if (current.id) {
      selector += `#${current.id}`
      path.unshift(selector)
      break
    }

    // 添加有意义的class
    if (current.className) {
      const classes = current.className.trim().split(/\s+/)
      const meaningfulClasses = classes.filter(cls =>
        // 过滤掉动态生成的class
        !cls.match(/^(active|selected|hover|focus|\d+)$/) &&
        cls.length > 2
      )
      if (meaningfulClasses.length > 0) {
        selector += '.' + meaningfulClasses.slice(0, 2).join('.')
      }
    }

    // 添加结构位置信息
    const parent = current.parentElement
    if (parent) {
      const siblings = Array.from(parent.children).filter(
        sibling => sibling.tagName === current.tagName
      )

      if (siblings.length > 1) {
        const index = siblings.indexOf(current) + 1
        selector += `:nth-of-type(${index})`
      }
    }

    path.unshift(selector)
    current = current.parentElement
  }

  return path.join(' > ')
}

3. 智能备用策略

const generateRobustSelector = (element: HTMLElement): string => {
  const strategies = [
    // 策略1:ID优先
    (el) => el.id ? `#${el.id}` : null,

    // 策略2:唯一属性组合
    (el) => {
      const attrs = ['data-id', 'data-key', 'data-testid', 'name']
      for (const attr of attrs) {
        const value = el.getAttribute(attr)
        if (value) {
          const selector = `${el.tagName.toLowerCase()}[${attr}="${value}"]`
          if (document.querySelectorAll(selector).length === 1) {
            return selector
          }
        }
      }
      return null
    },

    // 策略3:内容特征选择器
    (el) => {
      if (el.textContent && el.textContent.trim().length > 0 && el.textContent.length < 50) {
        const text = el.textContent.trim().replace(/['"]/g, '')
        const selector = `${el.tagName.toLowerCase()}:contains("${text}")`
        return selector
      }
      return null
    },

    // 策略4:结构路径
    generatePathSelector
  ]

  for (const strategy of strategies) {
    const selector = strategy(element)
    if (selector && validateSelectorStability(selector, element)) {
      return selector
    }
  }

  // 最终备用策略:带序号的完整路径
  return generateFullPathWithIndex(element)
}

4. 选择器优化特点

  • 稳定性优先:优先使用ID等不易变化的属性
  • 唯一性检查:确保选择器只能定位到一个元素
  • 语义化友好:使用有意义的class名而非自动生成的
  • 向后兼容:即使页面结构微调,选择器仍能正常工作
  • 层级适度:避免过深的嵌套路径,提高可读性

36. AI 零代码应用生成项目中,哪些内容抽象成了可复用的 Vue 组件?举例说明你开发一个组件时的主要思路。

基于项目代码分析,项目中抽象了以下可复用的Vue组件:

1. 核心业务组件

  • AppCard.vue: 应用卡片展示组件
  • AppDetailModal.vue: 应用详情弹窗组件
  • DeploySuccessModal.vue: 部署成功提示组件
  • MarkdownRenderer.vue: Markdown内容渲染组件
  • CodeHighlight.vue: 代码高亮显示组件

2. 布局框架组件

  • GlobalHeader.vue: 全局头部导航组件
  • GlobalFooter.vue: 全局底部组件
  • UserInfo.vue: 用户信息展示组件

3. 以 CodeHighlight.vue 为例说明组件开发思路

a) 职责单一原则

<!-- 专注于代码高亮显示功能 -->
<template>
  <div class="code-highlight-container">
    <div class="code-header">
      <!-- 文件信息展示 -->
    </div>
    <div class="code-content">
      <!-- 高亮代码内容 -->
    </div>
  </div>
</template>

b) Props接口设计

interface Props {
  code: string                    // 必需:代码内容
  language?: string              // 可选:编程语言
  fileName?: string              // 可选:文件名
  theme?: 'github' | 'vs2015'   // 可选:主题选择
}

c) 功能特性

// 1. 智能语言检测
const highlightedCode = computed(() => {
  if (props.language && hljs.getLanguage(props.language)) {
    // 使用指定语言高亮
    return hljs.highlight(props.code, { language: props.language }).value
  }
  // 自动检测语言
  return hljs.highlightAuto(props.code).value
})

// 2. 一键复制功能
const copyCode = async () => {
  await navigator.clipboard.writeText(props.code)
  message.success('代码已复制到剪贴板')
}

// 3. 动态主题切换
const loadTheme = async (theme: string) => {
  const link = document.createElement('link')
  link.href = `https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/${theme}.min.css`
  document.head.appendChild(link)
}

d) 样式设计考虑

.code-highlight-container {
  // 容器布局
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  overflow: hidden;

  .code-header {
    // 信息栏设计
    display: flex;
    justify-content: space-between;
    background: #f8f9fa;

    .language-badge {
      // 语言标识
      background: #1890ff;
      color: white;
      padding: 2px 6px;
      border-radius: 4px;
    }
  }

  // 响应式设计
  @media (max-width: 768px) {
    .code-content code {
      font-size: 12px;
    }
  }
}

4. 组件开发最佳实践总结

a) 接口设计原则

  • Props类型化,提供默认值
  • Events命名清晰,携带必要数据
  • Slots预留扩展点

b) 功能实现策略

  • 使用computed保证响应性
  • 错误边界处理
  • 性能优化(如防抖、节流)

c) 样式方案

  • CSS Module或Scoped CSS
  • 响应式设计
  • 主题化支持

d) 可维护性考虑

  • 单一职责,功能内聚
  • 文档完善,示例清晰
  • 测试覆盖,边界用例

37. 你是如何设计前端路由的?对于需要管理员权限的页面,在前端层面做了哪些访问控制?

基于router/index.ts的分析,前端路由设计如下:

1. 路由结构设计

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    // 公开页面
    { path: '/', name: '主页', component: HomePage },
    { path: '/user/login', name: '用户登录', component: UserLoginPage },
    { path: '/user/register', name: '用户注册', component: UserRegisterPage },

    // 用户功能页面
    { path: '/app/chat/:id', name: '应用对话', component: AppChatPage },
    { path: '/app/edit/:id', name: '编辑应用', component: AppEditPage },

    // 管理员页面
    { path: '/admin/userManage', name: '用户管理', component: UserManagePage },
    { path: '/admin/appManage', name: '应用管理', component: AppManagePage },
    { path: '/admin/chatManage', name: '对话管理', component: ChatManagePage },
  ]
})

2. 路由级别权限控制

// 路由守卫实现权限控制
router.beforeEach(async (to, from, next) => {
  const loginUserStore = useLoginUserStore()

  // 获取当前用户信息
  if (!loginUserStore.loginUser || !loginUserStore.loginUser.id) {
    await loginUserStore.fetchLoginUser()
  }

  const currentUser = loginUserStore.loginUser

  // 检查是否需要登录
  if (needsAuth(to.path) && !currentUser.id) {
    // 重定向到登录页,并保存目标页面
    next({
      path: '/user/login',
      query: { redirect: to.fullPath }
    })
    return
  }

  // 检查管理员权限
  if (isAdminRoute(to.path)) {
    if (!hasAdminPermission(currentUser)) {
      message.error('权限不足,无法访问该页面')
      next('/') // 重定向到首页
      return
    }
  }

  next()
})

3. 导航菜单权限控制

<!-- GlobalHeader.vue 中的权限控制 -->
<template>
  <a-menu mode="horizontal" :selectedKeys="selectedKeys">
    <!-- 公共菜单项 -->
    <a-menu-item key="home">
      <router-link to="/">首页</router-link>
    </a-menu-item>

    <!-- 登录用户菜单 -->
    <a-menu-item v-if="loginUser.id" key="apps">
      <router-link to="/apps">我的应用</router-link>
    </a-menu-item>

    <!-- 管理员菜单 -->
    <a-sub-menu v-if="isAdmin" key="admin" title="管理">
      <a-menu-item key="userManage">
        <router-link to="/admin/userManage">用户管理</router-link>
      </a-menu-item>
      <a-menu-item key="appManage">
        <router-link to="/admin/appManage">应用管理</router-link>
      </a-menu-item>
      <a-menu-item key="chatManage">
        <router-link to="/admin/chatManage">对话管理</router-link>
      </a-menu-item>
    </a-sub-menu>
  </a-menu>
</template>

4. 路由设计优势

  • 层次清晰:公开页面、用户页面、管理页面分层明确
  • 参数化路由:支持动态参数(如:id
  • 权限分离:前端权限控制与后端接口权限双重保障
  • 用户体验:登录重定向保持用户意图
  • 安全考虑:敏感操作需要后端验证,前端仅做UI层面控制

38. AI 返回的 Markdown 内容包含代码块时,前端是如何解析并实现代码高亮的?

基于MarkdownRenderer.vue和CodeHighlight.vue的分析,代码高亮实现如下:

1. Markdown解析配置

import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'

// 配置markdown-it实例
const md: MarkdownIt = new MarkdownIt({
  html: true,         // 允许HTML标签
  linkify: true,      // 自动识别链接
  typographer: true,  // 智能引号和其他符号
  highlight: function (str: string, lang: string): string {
    // 自定义代码高亮函数
    if (lang && hljs.getLanguage(lang)) {
      try {
        return (
          '<pre class="hljs"><code>' +
          hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
          '</code></pre>'
        )
      } catch {
        // 高亮失败时的降级处理
      }
    }

    // 默认处理:转义HTML
    return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
  }
})

2. 渲染流程

// 计算渲染后的Markdown
const renderedMarkdown = computed(() => {
  return md.render(props.content)
})
<template>
  <!-- 使用v-html渲染,包含高亮的代码块 -->
  <div class="markdown-content" v-html="renderedMarkdown"></div>
</template>

3. 代码高亮样式定制

.markdown-content {
  // 代码块基础样式
  :deep(pre) {
    background-color: #f8f8f8;
    border: 1px solid #e1e1e1;
    border-radius: 6px;
    padding: 1em;
    overflow-x: auto;
    margin: 1em 0;
  }

  :deep(pre code) {
    background-color: transparent;
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
    font-size: 0.9em;
    line-height: 1.4;
  }

  // 特定语言的代码块样式
  :deep(.hljs-keyword) { color: #d73a49; font-weight: 600; }
  :deep(.hljs-string) { color: #032f62; }
  :deep(.hljs-comment) { color: #6a737d; font-style: italic; }
  :deep(.hljs-number) { color: #005cc5; }
  :deep(.hljs-function) { color: #6f42c1; }
}

4. 独立代码高亮组件

// 智能语言检测和高亮
const highlightedCode = computed(() => {
  if (!props.code) return ''

  // 优先使用指定语言
  if (props.language && hljs.getLanguage(props.language)) {
    try {
      return hljs.highlight(props.code, {
        language: props.language,
        ignoreIllegals: true
      }).value
    } catch (error) {
      console.warn('代码高亮失败:', error)
    }
  }

  // 自动检测语言
  try {
    return hljs.highlightAuto(props.code).value
  } catch (error) {
    console.warn('自动检测语言失败:', error)
  }

  // 最终降级:HTML转义
  return hljs.escapeHtml(props.code)
})

5. 完整的代码块处理流程

AI输出Markdown文本
    ↓
MarkdownIt解析(识别```代码块)
    ↓
highlight函数处理(语言检测+语法高亮)
    ↓
生成带高亮的HTML
    ↓
v-html渲染到DOM
    ↓
CSS样式美化显示

6. 性能优化和错误处理

  • 降级策略:高亮失败时转义HTML显示
  • 语言检测:支持自动检测和手动指定
  • 缓存机制:计算属性缓存渲染结果
  • 主题热切换:动态CSS注入,无需刷新页面

这种实现方式确保了AI返回的代码内容能够以美观、易读的方式展示给用户,支持多种编程语言的语法高亮。

📞 联系我们

如果您有任何问题或建议,请随时联系我们:

📧 邮箱: zrt3ljnygz@163.com
💬 微信: Join2049
🐛 问题反馈: 提交Issue


扫码添加微信好友

扫码添加微信好友

⭐ Star History ⭐

如果这个项目对你有帮助,请给我们一个 Star!

posted @ 2025-10-06 10:21  Join2049  阅读(21)  评论(0)    收藏  举报