Sfoglia il codice sorgente

feat: add AI web editor functionality for creating web H5 elements

1. 新增多语言配置用于网页编辑器相关文案
2. 添加AI网页编辑器弹窗组件
3. 集成第三方网页编辑服务,支持创建并插入网页元素到课件
4. 新增webId字段用于标识网页元素
5. 优化工具栏按钮加载状态交互
lsc 1 settimana fa
parent
commit
f2e3e19787

+ 167 - 0
src/components/CollapsibleToolbar/componets/aiWeb.vue

@@ -0,0 +1,167 @@
+<template>
+  <div v-if="visible" class="aiweb-modal-overlay" @click.self="closeModal">
+    <div class="aiweb-modal">
+      <div class="aiweb-modal-header">
+        <span class="aiweb-modal-title">{{ lang.ssWebEditor }}</span>
+        <button class="aiweb-modal-close" @click="closeModal">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M18 6L6 18"></path>
+            <path d="M6 6l12 12"></path>
+          </svg>
+        </button>
+      </div>
+      <div class="aiweb-modal-body">
+        <iframe :src="iframeUrl" frameborder="0" class="aiweb-iframe" ref="iframeRef"></iframe>
+      </div>
+      <div class="aiweb-modal-footer">
+        <button class="aiweb-btn aiweb-btn-close" @click="closeModal">{{ lang.ssClose }}</button>
+        <button class="aiweb-btn aiweb-btn-confirm" @click="handleConfirm" :disabled="isLoading">
+          {{ isLoading ? lang.ssCreating : lang.ssConfirmAdd }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { lang } from '@/main'
+
+const props = defineProps<{
+  visible: boolean
+  webId: string
+  isLoading: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'close'): void
+  (e: 'add', webCode: string): void
+}>()
+
+const iframeUrl = computed(() => {
+  if (!props.webId) return ''
+  return `https://beta.app.cocorobo.cn/#/web?id=${props.webId}&create=false&isPPT=true`
+})
+
+const closeModal = () => {
+  emit('close')
+}
+
+const iframeRef = ref<HTMLIFrameElement>()
+
+const handleConfirm = () => {
+  const webCode = iframeRef.value?.contentWindow?.ReturnWebCode() as string || ''
+  console.log(webCode)
+  if (!webCode) return
+  console.log('确定按钮被点击')
+  emit('add', webCode)
+}
+</script>
+
+<style lang="scss" scoped>
+.aiweb-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.aiweb-modal {
+  width: 90%;
+  max-width: 1200px;
+  height: 90vh;
+  background-color: #fff;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.aiweb-modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-modal-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.aiweb-modal-close {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 8px;
+  color: #666;
+  transition: color 0.2s;
+
+  &:hover {
+    color: #333;
+  }
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
+}
+
+.aiweb-modal-body {
+  height: calc(100% - 140px);
+}
+
+.aiweb-modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 20px;
+  border-top: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-btn {
+  padding: 8px 20px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid transparent;
+}
+
+.aiweb-btn-close {
+  background-color: #fff;
+  border-color: #d9d9d9;
+  color: #666;
+
+  &:hover {
+    background-color: #f5f5f5;
+    border-color: #bfbfbf;
+  }
+}
+
+.aiweb-btn-confirm {
+  background-color: #1890ff;
+  border-color: #1890ff;
+  color: #fff;
+
+  &:hover {
+    background-color: #40a9ff;
+    border-color: #40a9ff;
+  }
+}
+
+.aiweb-iframe {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 86 - 12
src/components/CollapsibleToolbar/index2.vue

@@ -488,15 +488,21 @@
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpageLink }}</span>
         </div>
-        <div class="submenu-item" @click="handleToolClick('createWebpage')">
-          <div class="submenu-icon">
+         <!-- @click="handleToolClick('createWebpage')" -->
+        <div class="submenu-item" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
+          <div class="submenu-icon" v-if="!create_app_loading">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
               <rect x="3" y="3" width="18" height="14" rx="2"></rect>
               <path d="M12 8v6"></path>
               <path d="M9 11h6"></path>
             </svg>
           </div>
-          <span class="submenu-label">{{ lang.ssNewWebpage }}</span>
+          <div class="submenu-icon loading-icon" v-else>
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
+              <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ create_app_loading ? lang.ssCreating : lang.ssNewWebpage }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('uploadCode')">
           <div class="submenu-icon">
@@ -702,7 +708,7 @@
 
 
 
-
+    <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" @add="addAiWeb" :is-loading="isLoading" />
   </div>
 </template>
 
@@ -715,14 +721,14 @@ import { useSlidesStore } from '@/store'
 import { useSpeakingStore } from '@/store/speaking'
 import FileInput from '@/components/FileInput.vue'
 import AiChat from './componets/aiChat.vue'
+import AiWeb from './componets/aiWeb.vue'
 import SpeakingPanel from '@/views/Editor/EnglishSpeaking/SpeakingPanel.vue'
 import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
 import toolVote from '@/assets/img/tool_vote.png'
 import toolPhoto from '@/assets/img/tool_photo.png'
-
-
+import axios from '@/services/config'
 
 interface ContentItem {
   tool?: number
@@ -741,6 +747,16 @@ const props = withDefaults(defineProps<{
   userid: null,
 })
 
+
+const userJson = ref<any>({})
+watch(() => props.userid, async (newVal) => {
+  if (!newVal) return
+  const res = await axios.get('https://pbl.cocorobo.cn/api/pbl/selectUser', {
+    params: { userid: newVal }
+  })
+  userJson.value = res[0][0] || {}
+})
+
 const emit = defineEmits<{
   (e: 'toggle', collapsed: boolean): void
 }>()
@@ -1339,6 +1355,59 @@ const handleUploadCode = async () => {
   codeInput.value = ''
   isLoading.value = false
 }
+
+// AI Web 相关状态
+const create_app_loading = ref(false)
+const webId = ref('')
+const showAiWebModal = ref(false)
+
+const handle_add_aiWeb = _.throttle(async () => {
+  create_app_loading.value = true
+  try {
+    const elements = currentSlide.value?.elements || []
+    console.log(elements)
+    const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
+    
+    if (existingWebElement && existingWebElement.webId) {
+      webId.value = existingWebElement.webId
+      create_app_loading.value = false
+      showAiWebModal.value = true
+      return
+    }
+
+    const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
+      userid: props.userid || '',
+      username: userJson.value.name || '',
+      muti_name: lang.ssAddWebH5,
+      description: lang.ssAddWebH5,
+      headUrl: 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F51741770599274.svg',
+      organizeid: userJson.value.organizeid || '',
+      content: ''
+    })
+    console.log(res)
+    webId.value = res.id || ''
+
+    create_app_loading.value = false
+    showAiWebModal.value = true
+  }
+  catch (error) {
+    console.log(error)
+    create_app_loading.value = false
+  }
+}, 3000)
+
+
+const addAiWeb = async (code: string) => {
+  if (!code) return
+  const file = new File([code], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  createSlide()
+  createFrameElement(url, 73, webId.value)
+  isLoading.value = false
+  showAiWebModal.value = false
+  webId.value = ''
+}
 </script>
 
 <style lang="scss" scoped>
@@ -2398,14 +2467,19 @@ const handleUploadCode = async () => {
       cursor: not-allowed;
       opacity: 0.8;
     }
+  }
 
-    &.error {
-      background-color: #ff4d4f;
-      color: white;
+  .loading-state {
+    opacity: 0.7;
+    cursor: not-allowed;
+    pointer-events: none;
 
-      &:hover:not(:disabled) {
-        background-color: #ff7875;
-      }
+    .submenu-icon {
+      animation: pulse 1s infinite;
+    }
+
+    .spin {
+      animation: spin 1s linear infinite;
     }
   }
 }

+ 2 - 1
src/hooks/useCreateElement.ts

@@ -318,7 +318,7 @@ export default () => {
    * 创建网页元素
    * @param url 网页链接地址
    */
-  const createFrameElement = (url: string, type: number) => {
+  const createFrameElement = (url: string, type: number, id?: string) => {
     // 检查当前幻灯片是否已经包含网页元素
     const { currentSlide } = storeToRefs(useSlidesStore())
     const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
@@ -334,6 +334,7 @@ export default () => {
     createElement({
       type: 'frame',
       id: nanoid(10),
+      webId: id || '',
       url,
       width: width,
       height: height,

+ 1 - 0
src/types/slides.ts

@@ -651,6 +651,7 @@ export interface PPTFrameElement extends PPTBaseElement {
   isHTML?: boolean,
   toolType?: number,
   isDone?: boolean,
+  webId?: string,
 }
 
 export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement | PPTAudioElement | PPTFrameElement

+ 2 - 1
src/views/Editor/CanvasTool/index2.vue

@@ -268,6 +268,7 @@ import Slider from '@/components/Slider.vue'
 import ColorButton from '@/components/ColorButton.vue'
 import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.vue'
 
+
 const mainStore = useMainStore()
 const slidesStore = useSlidesStore()
 const speakingStore = useSpeakingStore()
@@ -544,6 +545,7 @@ const pattern = ref('')
 import emitter, { EmitterEvents } from '@/utils/emitter'
 const { addHistorySnapshot } = useHistorySnapshot()
 const currentGradientIndex = ref(0)
+
 watch(handleElementId, () => {
   currentGradientIndex.value = 0
 })
@@ -617,7 +619,6 @@ const updateGradientColors = (color: string) => {
   updateGradient({ colors })
 }
 
-
 </script>
 
 <style lang="scss" scoped>

+ 1 - 1
src/views/Editor/index3.vue

@@ -164,7 +164,7 @@
     <div class="layout-content">
       <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
       <div class="layout-content-center">
-        <CanvasTool class="center-top" />
+        <CanvasTool class="center-top"  :userid="props.userid"/>
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
           :courseid="props.courseid" @course-loaded="handleCourseLoaded"  ref="canvas"/>
         <!-- <Remark

+ 5 - 1
src/views/lang/cn.json

@@ -905,5 +905,9 @@
   "ssAiChatQuickAction5": "为课件推荐适合页面的互动工具",
   "ssDragAndDropHint": "点击或拖拽文件到此处上传",
   "ssSupportHTML": "支持 HTML、HTM 格式",
-  "ssPasteHTML": "请在此处粘贴完整的HTML代码"
+  "ssPasteHTML": "请在此处粘贴完整的HTML代码",
+  "ssAddWebH5": "新建网页h5",
+  "ssCreating": "创建中...",
+  "ssWebEditor": "AI 网页编辑器",
+  "ssConfirmAdd": "确认添加"
 }

+ 5 - 1
src/views/lang/en.json

@@ -905,5 +905,9 @@
   "ssAiChatQuickAction5": "Recommend interactive tools for the current page",
   "ssDragAndDropHint": "Click or drag files here to upload",
   "ssSupportHTML": "Supports HTML, HTM format",
-  "ssPasteHTML": "Please paste the complete HTML code here"
+  "ssPasteHTML": "Please paste the complete HTML code here",
+  "ssAddWebH5": "Add Web H5",
+  "ssCreating": "Creating...",
+  "ssWebEditor": "AI Web Editor",
+  "ssConfirmAdd": "Confirm Add"
 }

+ 5 - 1
src/views/lang/hk.json

@@ -905,5 +905,9 @@
   "ssAiChatQuickAction5": "為課件推薦適合的互動工具。",
   "ssDragAndDropHint": "點擊或拖拽文件至此,或點擊選擇",
   "ssSupportHTML": "支持 HTML、HTM 格式",
-  "ssPasteHTML": "請在此处粘貼完整的HTML代碼"
+  "ssPasteHTML": "請在此处粘貼完整的HTML代碼",
+  "ssAddWebH5": "新增網頁H5",
+  "ssCreating": "創建中...",
+  "ssWebEditor": "AI 網頁編輯器",
+  "ssConfirmAdd": "確認添加"
 }