Explorar o código

图生图,图生视频

丸子 hai 7 meses
pai
achega
1c8d0d1c90

+ 19 - 0
src/api/questions.js

@@ -73,3 +73,22 @@ export async function PaintingGetMys(ids) {
   })
 }
 
+// 生成视频
+export function CreateVideo (data){
+  return axios({
+    url: "bjdxWeb/ai/create-video",
+    method: 'post',
+    data
+  })
+}
+
+
+// 获取视频结果
+export async function VideoGetMys(ids) {
+  return await axios({
+    url: "bjdxWeb/ai/video-get-mys",
+    method: 'get',
+    data: {ids: ids.join(',')}
+  })
+}
+

+ 17 - 0
src/api/teachers.js

@@ -8,3 +8,20 @@ export function teacherList (data) {
     data
   })
 }
+
+
+// 模型类型枚举
+export const ModelTypeEnum = {
+  IMAGE_TO_IMAGE: 7, // 图生图
+  IMAGE_TO_VIDEO: 4, // 图生视频
+  TEXT_TO_IMAGE: 2   // 文生图
+}
+
+// 获取模型ID
+export function getModelIdByType (data) {
+  return axios({
+    url: 'bjdxWeb/ai/getModelIdByType',
+    method: 'get',
+    data
+  })
+}

BIN=BIN
src/assets/icon/image2image.png


BIN=BIN
src/assets/icon/image2image02.png


BIN=BIN
src/assets/icon/video.png


BIN=BIN
src/assets/icon/video02.png


+ 269 - 0
src/components/ImageUpload/index.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="image-upload-container">
+    <input
+      type="file"
+      ref="fileInput"
+      accept="image/*"
+      style="display: none"
+      @change="handleFileSelect"
+    />
+    <button 
+      class="upload-btn"
+      @click="triggerFileSelect"
+      :disabled="isUploading"
+      title="上传参考图"
+    >
+      <el-icon><Picture /></el-icon>
+    </button>
+
+    <!-- 预览图显示区域 - 使用el-image组件 -->
+    <div v-if="previewImage && previewImage !== '/src/assets/images/default-preview.png'" class="preview-container">
+      <div class="image-wrapper">
+        <!-- 删除按钮 - 确保显示 -->
+        <button class="delete-btn" @click="removeImage">×</button>
+        <!-- 使用el-image组件实现预览功能 -->
+        <el-image
+          :src="previewImage"
+          :preview-src-list="[previewImage]"
+          fit="cover"
+          show-progress
+          class="preview-image"
+        >
+          <template
+            #toolbar="{ actions,  reset, activeIndex }"
+          >
+            <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
+            <el-icon
+              @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
+              <ZoomIn />
+            </el-icon>
+            <el-icon
+              @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
+              <RefreshRight />
+            </el-icon>
+            <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
+            <el-icon @click="reset"><Refresh /></el-icon>
+            <el-icon @click="download(activeIndex)"><Download /></el-icon>
+          </template>
+        </el-image>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download } from '@element-plus/icons-vue';
+import { Message } from '@/utils/message/Message.js';
+// 导入axios用于文件上传
+import axios from 'axios';
+
+// 定义props(保留,但在内部写死默认值)
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  }
+});
+
+// 定义emits
+const emit = defineEmits(['update:modelValue']);
+
+// 文件输入引用
+const fileInput = ref(null);
+// 上传状态
+const isUploading = ref(false);
+// 预览图片URL
+const previewImage = ref('');
+
+// 触发文件选择
+const triggerFileSelect = () => {
+  fileInput.value?.click();
+};
+
+// 处理文件选择
+const handleFileSelect = async (event) => {
+  const file = event.target.files[0];
+  if (!file) return;
+
+  // 验证文件类型
+  if (!file.type.match('image.*')) {
+    Message().error('请选择有效的图片文件!', true);
+    return;
+  }
+
+  // 验证文件大小
+  const maxSize = 5 * 1024 * 1024;
+  if (file.size > maxSize) {
+    Message().error('图片大小不能超过5MB!', true);
+    return;
+  }
+
+  try {
+    isUploading.value = true;
+    
+    // 创建FormData对象
+    const formData = new FormData();
+    formData.append('file', file);
+    
+    
+    // 构建上传地址
+    const uploadUrl = import.meta.env.VITE_BASE_URL + '/infra/file/upload';
+    
+    // 发送文件上传请求
+    const response = await axios.post(uploadUrl, formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data',
+        'Authorization': `Bearer ${localStorage.getItem('token')}`
+      },
+      timeout: 60000
+    });
+    
+    
+    let imageUrl = '';
+    if (response.data?.data) {
+      // 去除反引号和可能的空格
+      imageUrl = response.data.data.replace(/[`\s]/g, '');
+    }
+    
+    // 更新预览图
+    previewImage.value = imageUrl;
+    // 触发更新事件
+    emit('update:modelValue', imageUrl);
+    Message().success('图片上传成功!', true);
+    isUploading.value = false;
+  } catch (error) {
+    console.error('图片上传失败:', error);
+    Message().error('图片上传失败,请重试!', true);
+    isUploading.value = false;
+  } finally {
+    // 清空input值,允许重复选择同一文件
+    if (event.target) {
+      event.target.value = '';
+    }
+  }
+};
+
+// 清除预览图(供父组件调用)
+const clearPreview = () => {
+  previewImage.value = '/src/assets/images/default-preview.png';
+  emit('update:modelValue', '');
+};
+
+// 暴露方法给父组件
+defineExpose({
+  clearPreview
+});
+
+// 移除图片
+const removeImage = () => {
+  previewImage.value = '';
+  emit('update:modelValue', '');
+  Message().success('图片已移除!', true);
+};
+
+// 下载图片函数
+const download = (index) => {
+  const link = document.createElement('a');
+  link.href = [previewImage.value][index];
+  link.download = `image_${Date.now()}.png`;
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+};
+</script>
+
+<style scoped lang="scss">
+@use "sass:math";
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.image-upload-container {
+  position: relative;
+  display: inline-block;
+}
+
+.upload-btn {
+  padding: rpx(5) rpx(10);
+  background: #fff;
+  border: 1px solid #ffce1b;
+  box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+  border-radius: rpx(5);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: rpx(3);
+  font-size: rpx(6);
+
+  .el-icon {
+    font-size: rpx(8);
+    color: #666;
+  }
+
+  &:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+  }
+}
+
+// 预览图样式
+.preview-container {
+  position: absolute;
+  bottom: 100%;
+  margin-bottom: rpx(5);
+  padding: rpx(3);
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: rpx(5);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+}
+
+.image-wrapper {
+  position: relative;
+  display: inline-block;
+}
+
+.preview-image {
+  width: rpx(90);
+   max-height: rpx(150);
+  object-fit: contain;
+  border-radius: rpx(3);
+}
+
+// 删除按钮样式 - 确保正确显示
+.delete-btn {
+  position: absolute;
+  top: rpx(3);
+  right: rpx(3);
+  background: black;
+  color: white;
+  border: none;
+  border-radius: 50%;
+  width: rpx(8);
+  height: rpx(8);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0.8;
+  transition: opacity 0.3s;
+  font-size: rpx(5);
+  font-weight: bold;
+  line-height: 1;
+  padding: 0;
+  margin: 0;
+  min-width: rpx(8);
+  z-index: 10;
+}
+
+.image-wrapper:hover .delete-btn {
+  opacity: 0.8;
+}
+
+.delete-btn:hover {
+  opacity: 1;
+}
+</style>

+ 48 - 12
src/components/LeftPanel.vue

@@ -42,20 +42,28 @@ import { useRouter, useRoute } from 'vue-router'
 import question from '@/assets/icon/question.png'
 import painting from '@/assets/icon/painting.png'
 import Human from '@/assets/icon/human.png'
+import image2image from '@/assets/icon/image2image.png'
+import video from '@/assets/icon/video.png'
 // 黑色
 import question02 from '@/assets/icon/question02.png'
 import painting02 from '@/assets/icon/painting02.png'
 import Human02 from '@/assets/icon/Human02.png'
+import image2image02 from '@/assets/icon/image2image02.png'
+import video02 from '@/assets/icon/video02.png'
+
+
 
 import { teacherList } from '@/api/teachers.js'
 
+ 
 const router = useRouter()
 const route = useRoute()
 
 // 添加抽屉显示状态
 const drawerVisible = ref(true)
-// 添加当前选中索引状态
+// 当前选中索引状态
 const currentActiveIndex = ref('2')
+
 // 渲染侧边栏
 const groupList = ref([
   {
@@ -64,30 +72,52 @@ const groupList = ref([
     title: '智能问答'
   },
   {
-    icon: painting, // 默认图片
-    hoverIcon: painting02, // 交互图片
+    icon: painting, 
+    hoverIcon: painting02,
     title: '智能绘画'
   },
   {
-    icon: Human, // 默认图片
-    hoverIcon: Human02, // 交互图片
+    icon: Human,
+    hoverIcon: Human02,
     title: '数字人老师'
+  },
+  {
+    icon: image2image,
+    hoverIcon: image2image02,
+    title: '图生图'
+  },
+  {
+    icon: video,
+    hoverIcon: video02,
+    title: '图生视频'
   }
 ])
 
 
 // 提取更新选中状态的逻辑为单独函数
+// 优化后的更新选中状态的方法
 const updateActiveIndex = () => {
   const path = route.path;
   const from = route.query.from;
+  // 使用映射表存储路径和索引的对应关系
+  const pathIndexMap = {
+    'ai-questions': '0', // 智能问答
+    'ai-painting': '1',  // 智能绘画
+    'ai-laboratory': '2', // 数字人老师
+    'ai-image': '3' ,     // 图生图
+    'ai-video': '4' ,     // 图生视频
+  };
+  // 从数字人老师页面进入智能问答页面
   if (path.includes('ai-questions') && from === 'ai-laboratory') {
-    currentActiveIndex.value = '2'; // 数字人老师
-  } else if (path.includes('ai-questions')) {
-    currentActiveIndex.value = '0'; // 智能问答
-  } else if (path.includes('ai-painting')) {
-    currentActiveIndex.value = '1'; // 智能绘画
-  } else if (path.includes('ai-laboratory')) {
-    currentActiveIndex.value = '2'; // 数字人老师
+    currentActiveIndex.value = '2'; // 保持选中数字人老师
+    return;
+  }
+  // 查找路径对应的索引
+  for (const [key, index] of Object.entries(pathIndexMap)) {
+    if (path.includes(key)) {
+      currentActiveIndex.value = index;
+      return;
+    }
   }
 };
 
@@ -143,6 +173,12 @@ const navigateToAI = async (group) => {
   if (group.title === '数字人老师') {
     router.push('/ai-laboratory')
   }
+  if (group.title === '图生图') {
+    router.push('/ai-image')
+  }
+  if (group.title === '图生视频') {
+    router.push('/ai-video')
+  }
 }
 
 // 处理菜单展开和关闭

+ 10 - 2
src/views/AIGeneralCourse.vue

@@ -168,6 +168,7 @@ const drawerVisible = ref(true)
 
 // 实操课
 const ClassOutlineScData = ref([])
+
 // 状态变量,跟踪当前显示的是通识课还是实操课
 const showPracticalCourse = ref(false)
 
@@ -205,6 +206,8 @@ const handleNewButtonClick = async() => {
   showPracticalCourse.value = !showPracticalCourse.value
   // 保存状态到localStorage
   localStorage.setItem('showPracticalCourse', showPracticalCourse.value.toString())
+  // 清空搜索框内容
+  SearchInput.value = ''
 }
 
 // 处理下拉菜单选择
@@ -214,6 +217,8 @@ const handleGradeSelect = command => {
   localStorage.setItem('selectedGrade', command)
   // 年级切换时重新加载数据的逻辑
   const selectedItem = classData.value.find(item => item.ctType === command)
+  // 清空搜索框内容
+  SearchInput.value = ''
   // 存储年级id
   if (selectedItem) {
     localStorage.setItem('selectedGradeId', selectedItem.id)
@@ -280,12 +285,12 @@ const getCourseTitle = index => {
 const courseTitles = computed(() => {
   const data = showPracticalCourse.value ? ClassOutlineScData.value : classOutlineData.value
   return data.map(item => {
-    return `${item.ctTypeSort} ${item.ctType}`;
+    return `${item.ctTypeSort} ${item.ctType}`;  
   });
 })
 
 // 首页点击渲染后的页面title
-const pageTitle = ref('')
+const pageTitle = ref('AI智能课')
 onMounted(() => {
   fetchCtTypes()
   const title = router.currentRoute.value.query.title
@@ -315,12 +320,15 @@ const querySearch = (queryString, cb) => {
     : data
   cb(results)
 }
+
 // 搜索选择处理方法
 const handleSearchSelect = item => {
   goToAIExperience(item)
     // 清空输入框
   SearchInput.value = '';
 }
+
+
 // 修改过滤逻辑,直接使用classOutlineData
 const filteredTitles = computed(() => {
   const data = showPracticalCourse.value ? ClassOutlineScData.value : classOutlineData.value

+ 816 - 0
src/views/AIImageToImage.vue

@@ -0,0 +1,816 @@
+<template>
+  <!-- 图生图 -->
+  <div class="home-container">
+     <!-- 展开收起侧边栏 -->
+    <div
+      class="icon-expand"
+      :style="{
+        backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
+        left: drawerVisible ? '18%' : '0'
+      }"
+      @click="toggleDrawer"
+    >
+      <span
+        class="vertical-lines"
+        :style="{
+          color: drawerVisible ? '#8a78d0' : 'white'
+        }"
+        >||</span
+      >
+    </div>
+
+    <!-- 左侧折叠面板 -->
+    <LeftPanel ref="leftPanelRef" v-if="drawerVisible"/>
+
+    <div class="left-group2">
+      <div class="title-box">
+        <div class="box-icon" @click="goBack">
+          <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+          图生图
+        </div>
+      </div>
+      <div class="img-box">
+        <p>
+          <img
+              style=" width: fit-content; height: 180px; margin: 10px;"
+              src="@/assets/images/color.png"
+              class="avatar user"
+          />
+        </p>
+        <p>期待你的画作喔~</p>
+      </div>
+    </div>
+
+    <!-- 右侧AI问答 -->
+    <div class="number-people">
+      <div class="content-box">
+        <!-- AI对话框 -->
+        <div class="chat-dialog">
+          <!-- 对话消息列表 -->
+          <div class="message-list">
+            <div v-if="imageAllList.length > 0">
+                <div  v-for="(item, index) in imageAllList" :key="index">
+              <!-- 用户消息 -->
+              <div class="user-message" v-if="item.type === 'user'">
+                {{ item.content }}
+                <div class="user-image-list" v-if="item.imageUrl">
+                  <el-image
+                    style="width: fit-content; height: 180px; margin: 10px;"
+                    :src="item.imageUrl"
+                    :preview-src-list="[item.imageUrl]"
+                    fit="cover"
+                    show-progress
+                  >
+                    <template
+                      #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
+                    >
+                      <el-icon @click="prev"><Back /></el-icon>
+                      <el-icon @click="next"><Right /></el-icon>
+                      <el-icon @click="setActiveItem(item.imageList.length - 1)">
+                        <DArrowRight />
+                      </el-icon>
+                      <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
+                      <el-icon @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })"><ZoomIn /></el-icon>
+                      <el-icon @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })"><RefreshRight /></el-icon>
+                      <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
+                      <el-icon @click="reset"><Refresh /></el-icon>
+                      <el-icon @click="download(activeIndex)"><Download /></el-icon>
+                    </template>
+                  </el-image>
+                </div>
+              </div>
+              <!-- AI生成图片对话框 -->
+              <div class="ai-message" v-if="item.type !== 'user'">
+                {{ item.content }}
+                <div class="image-list" v-if="item.imageList">
+                  <el-image
+                      v-for="(image, index) in item.imageList"
+                      :key="index"
+                      style=" width: fit-content; height: 220px; margin: 10px;"
+                      :src="image"
+                      :preview-src-list="item.imageList"
+                      fit="cover"
+                      show-progress
+                  >
+                    <template
+                        #toolbar="{ actions,  reset, activeIndex}"
+                    >
+                      <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
+                      <el-icon
+                          @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
+                        <ZoomIn />
+                      </el-icon>
+                      <el-icon
+                          @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
+                        <RefreshRight />
+                      </el-icon>
+                      <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
+                      <el-icon @click="reset"><Refresh /></el-icon>
+                      <el-icon @click="download(activeIndex)"><Download /></el-icon>
+                    </template>
+                  </el-image>
+                </div>
+              </div>
+               </div>
+            </div>
+          </div>
+
+          <!-- 输入框和发送按钮 -->
+          <div class="input-section">
+            <input
+              type="text"
+              v-model="inputMessage"
+              placeholder="描述任何画面..."
+              @keyup.enter="sendMessage"
+              style="flex: 1; margin-right: 8px;"
+            />
+            <!-- 参考图 -->
+             <ImageUpload v-model="uploadedImage" ref="imageUploadRef"/>
+            <!-- 语音输入按钮 -->
+            <button
+                @click="toggleSpeechInput"
+                class="speech-btn"
+                :class="{ 'recording': isRecording }"
+            >
+              <el-icon v-if="!isRecording"><Microphone /></el-icon>
+              <el-icon v-else><Mute /></el-icon>
+              <!-- 显示倒计时(仅录音时显示) -->
+              <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
+            </button>
+
+            <!-- 终止按钮 -->
+            <div
+              v-if="conversationInProgress"
+              @click="stopStream"
+              class="stop-btn"
+              title="终止问答"
+            >
+              <img :src="stopicon" alt="停止" />
+            </div>
+            <button v-if="!conversationInProgress"
+              @click="sendMessage">发送</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted,onUnmounted} from 'vue'
+import {AiImageStatusEnum, CreatePainting, PaintingGetMys} from '@/api/questions.js'
+import { useRouter, useRoute } from 'vue-router'
+import NumberPeople00 from '@/assets/images/xiaozhi.png'
+import {
+  Document,
+  Menu as IconMenu,
+  Location,
+  Setting,
+  ArrowLeftBold,
+  Fold,
+  Expand,
+  ChatLineRound,
+  Picture,
+  MagicStick,
+  Tickets,
+  User
+} from '@element-plus/icons-vue'
+
+import { saveRecord } from '@/api/personalized/index.js'
+
+// 导入全局状态
+import { globalState } from '@/utils/globalState.js'
+// 语音图标
+import { Microphone, Mute } from "@element-plus/icons-vue";
+// 终止按钮
+import stopicon from "@/assets/icon/stopicon.png";
+// 消息组件
+import {Message} from "@/utils/message/Message.js";
+
+// 图生图
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+// 导入getModelIdByType接口
+import { getModelIdByType } from '@/api/teachers.js'
+import { ModelTypeEnum } from '@/api/teachers.js'
+
+// 存储上传的图片
+const uploadedImage = ref('');
+
+const imageUploadRef = ref(null);
+
+// 语音输入响应式变量
+const isRecording = ref(false); // 录音状态
+const recognition = ref(null); // 语音识别实例
+const countdown = ref(0); // 倒计时剩余秒数
+const countdownTimer = ref(null); // 倒计时定
+// 对话状态变量
+const conversationInProgress = ref(false); // 对话是否正在进行中
+const conversationInAbortController = ref(); // 对话进行中 abort 控制器
+
+
+// 返回上一页
+const goBack = () => {
+  router.push('/ai-laboratory')
+}
+const router = useRouter()
+const route = useRoute()
+
+// 导入图片
+import question from '@/assets/icon/question.png'
+import painting from '@/assets/icon/painting.png'
+import human from '@/assets/icon/human.png'
+
+import LeftPanel from '@/components/LeftPanel.vue'
+const leftPanelRef = ref(null)
+
+
+// tts 语音
+import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
+const { playAudioChunk } = useAudioPlayer();
+
+// 添加抽屉显示状态
+const drawerVisible = ref(true)
+// 添加切换抽屉显示状态的函数
+const toggleDrawer = () => {
+  drawerVisible.value = !drawerVisible.value
+}
+
+
+  // 年级ID相关
+const gradeId = ref('')
+// 添加消息计数器变量
+const messageCount = ref(0)
+// modelId响应式变量
+const modelId = ref(0)
+// 保存记录
+onMounted(async () => {
+    // 从全局状态初始化年级ID
+  gradeId.value = globalState.initGradeId()
+  try{
+    const res = await saveRecord({
+        brpNjId: gradeId.value,
+        brpType: "aiCount",
+        brpProgress: 1
+      });
+      // 获取modelId
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: "DouBao" })
+    modelId.value = modelRes.data
+  }catch(error){
+    console.error('保存记录失败:', error);
+  }
+});
+
+// 消息列表和输入内容的响应式变量
+const messages = ref([])
+const inputMessage = ref('')
+
+// 初始化语音识别
+const initSpeechRecognition = () => {
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+  if (!SpeechRecognition) {
+    alert("当前浏览器不支持语音输入功能");
+    return null;
+  }
+
+  const instance = new SpeechRecognition();
+  instance.lang = 'zh-CN';
+  instance.interimResults = false;
+
+  instance.onresult = (event) => {
+    if (event.results?.[0]?.[0]) {
+      inputMessage.value += event.results[0][0].transcript;
+    }
+  };
+
+  // 识别器结束时清除定时器
+  instance.onend = () => {
+    clearInterval(countdownTimer.value);
+    isRecording.value = false;
+    countdown.value = 0;
+  };
+
+  instance.onerror = (event) => {
+    console.error('语音识别错误:', event.error);
+    clearInterval(countdownTimer.value); // 出错时清除定时器
+    isRecording.value = false;
+    Message().error('语音输入失败,请重试!', true)
+    countdown.value = 0;
+  };
+
+  return instance;
+};
+
+// 切换录音状态
+const toggleSpeechInput = () => {
+  // 清除可能存在的旧定时器
+  clearInterval(countdownTimer.value);
+  countdownTimer.value = null;
+
+  if (isRecording.value) {
+    // 手动停止时重置状态
+    countdown.value = 0;
+    recognition.value?.stop();
+    isRecording.value = false;
+  } else {
+    // 初始化倒计时前再次清除定时器(防止快速点击)
+    clearInterval(countdownTimer.value);
+    countdown.value = 10; // 重置为10秒
+
+    recognition.value = initSpeechRecognition();
+    if (!recognition.value) return;
+
+    navigator.mediaDevices.getUserMedia({ audio: true })
+      .then(() => {
+        recognition.value.start();
+        isRecording.value = true;
+
+        // 启动新的倒计时定时器
+        countdownTimer.value = setInterval(() => {
+          countdown.value--;
+          if (countdown.value <= 0) {
+            clearInterval(countdownTimer.value); // 倒计时结束清除
+            recognition.value.stop();
+            isRecording.value = false;
+            countdown.value = 0;
+          }
+        }, 1000);
+      })
+      .catch((err) => {
+        console.error("麦克风权限获取失败:", err);
+        alert("请允许麦克风权限以使用语音输入");
+        // 出错时重置状态
+        isRecording.value = false;
+        countdown.value = 0;
+      });
+  }
+};
+
+// 停止操作函数
+const stopStream = async () => {
+  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
+  if (conversationInAbortController.value) {
+    conversationInAbortController.value.abort();
+  }
+  // 设置为 false
+  conversationInProgress.value = false;
+};
+
+// 发送消息函数,图片数据
+const sendMessage = async() => {
+  if (inputMessage.value.trim() || uploadedImage.value) {
+    // 创建 AbortController 实例,以便中止请求
+    conversationInAbortController.value = new AbortController();
+    // 标记对话进行中
+    conversationInProgress.value = true;
+    // 先保存内容 再置空输入框
+    let content = inputMessage.value;
+    inputMessage.value = '';
+    
+
+    // 创建用户消息对象,包含可能的图片
+    const userMessage = {
+      type: 'user',
+      content: content,
+    };
+    
+    // 如果有上传的图片,添加到用户消息中
+    if (uploadedImage.value) {
+      userMessage.imageUrl = uploadedImage.value;
+      // 清空上传的图片
+      // uploadedImage.value = '';
+    }
+    
+    imageAllList.value.push(userMessage);
+    imageAllList.value.push({
+      type: 'ai',
+      content: "正在为您生成图片,请稍等...",
+    })
+
+    // 递增消息计数器
+    messageCount.value++
+    // 发送saveRecord请求 保存消息次数
+    try{
+      await saveRecord({
+         brpNjId: gradeId.value,
+         brpType: "aiCount",
+         brpProgress: messageCount.value
+       });
+       console.log('保存记录成功,消息次数:', messageCount.value);
+   }catch(error){
+     console.error('保存记录失败:', error);
+     conversationInProgress.value = false;
+   }
+
+    try {
+      CreatePainting({
+        "modelId": modelId.value,
+        "prompt":content,
+        "width":1024,
+        "height":1024,
+        "promptImage":uploadedImage.value
+      }).then(res=>{
+        console.log("生成图片",res)
+        //目前写死调用已生成的图片,全部通了后再改
+        inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
+        // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
+      }).finally(() => {
+        // 图片生成请求完成后更新状态
+        conversationInProgress.value = false;
+      });
+    } catch (error) {
+      console.error('生成图片失败:', error);
+      conversationInProgress.value = false;
+    }
+  }
+  // 调用子组件的方法清除预览图
+  imageUploadRef.value?.clearPreview();
+};
+
+
+// 生成图片
+import { ElIcon } from 'element-plus'
+import {
+  Back,
+  DArrowRight,
+  Download,
+  Refresh,
+  RefreshLeft,
+  RefreshRight,
+  Right,
+  ZoomIn,
+  ZoomOut,
+} from '@element-plus/icons-vue'
+
+
+const imageAllList = ref([]) // 对话的消息列表
+const imageList = ref([]) // image 列表
+// 图片轮询相关的参数(正在生成中的)
+const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
+const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
+
+
+/** 轮询生成中的 image 列表 */
+const refreshWatchImages = async () => {
+  const imageIds = Object.keys(inProgressImageMap.value).map(Number)
+  if (imageIds.length === 0) {
+    return
+  }
+  const list = await PaintingGetMys(imageIds)
+  const newWatchImages = {}
+  list.data.forEach((image) => {
+    
+    if (image.status === AiImageStatusEnum.IN_PROGRESS) {
+      newWatchImages[image.id] = image
+    } else {
+      imageAllList.value.pop();
+      console.log('AI生成的图片地址:', image.picUrl);
+      imageAllList.value.push({
+        type: 'ai',
+        content: "已为您生成图片:",
+        imageList: [image.picUrl],
+      })
+    }
+  })
+  inProgressImageMap.value = newWatchImages
+  if (newWatchImages.size === 0) {
+    inProgressTimerFun()
+  }
+}
+
+
+/** 组件挂在的时候 */
+onMounted(async () => {
+  refreshWatchImagesFun()
+})
+
+/** 组件取消挂在的时候 */
+onUnmounted(async () => {
+  inProgressTimerFun()
+})
+
+// 自动刷新 image 列表
+const refreshWatchImagesFun = () => {
+  inProgressTimer.value = setInterval(async () => {
+    await refreshWatchImages()
+  }, 1000 * 3)
+}
+
+// 停止刷新image列表
+const inProgressTimerFun = () => {
+  if (inProgressTimer.value) {
+    clearInterval(inProgressTimer.value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+// 用户图片列表样式
+.user-image-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: rpx(5);
+}
+/* 添加过渡样式 */
+.drawer-slide-enter-active,
+.drawer-slide-leave-active {
+  transition: all 0.3s ease;
+}
+.drawer-slide-enter-from,
+.drawer-slide-leave-to {
+  transform: translateX(-100%);
+  opacity: 0;
+}
+:deep(.el-image-viewer__wrapper) {
+  z-index: 10000 !important;
+}
+.icon-expand {
+  width: rpx(8);
+  height: rpx(35);
+  border-top-right-radius: rpx(5);
+  border-bottom-right-radius: rpx(5);
+  z-index: 9999;
+  position: absolute;
+  top: 50%;
+  left: 18%;
+  transform: translateY(-50%);
+  background-color: #44449c;
+  cursor: pointer; // 添加鼠标指针样式
+  clip-path: polygon(0 0, 100% 15%, 100% 85%, 0 100%);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  transition: all 0.3s ease;
+}
+.icon-expand .vertical-lines {
+  color: #8a78d0;
+  font-size: rpx(10);
+}
+.menu-icon {
+  width:rpx(11);
+  height: rpx(11);
+  margin-right: rpx(2);
+}
+// 侧边栏
+.left-group1 {
+  width: rpx(135);
+  height: 100%;
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+
+}
+.home-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: row;
+  gap: rpx(0);
+   background: linear-gradient(
+    to bottom,
+    #e2ddfc,
+    #f1effd
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+}
+
+// 侧边栏
+.left-group {
+  width: rpx(135);
+  height: 100%;
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+}
+.mb-2 {
+  color: black;
+  margin-top: rpx(1);
+}
+.tac ::v-deep(.el-menu) {
+  background-color: transparent;
+  border: none;
+  width: 100%;
+  margin-top: rpx(55);
+  margin-left: rpx(10);
+}
+.el-menu-item {
+  width: rpx(115);
+  height: rpx(25);
+  margin-bottom: rpx(5);
+  border-radius: rpx(6);
+  color: white;
+  font-size: rpx(8);
+}
+.el-menu-item .el-icon svg {
+  font-size: rpx(15);
+  color: white;
+}
+
+.el-menu ::v-deep(.el-menu-item:hover),
+.el-menu ::v-deep(.el-menu-item:focus),
+.el-menu ::v-deep(.el-menu-item:active) {
+  background: linear-gradient(
+    to bottom,
+    #ffefb0,
+    #ffcc00
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  box-shadow: 0 8px 8px rgb(0, 0, 0, 0.3);
+  color: black;
+  font-size: rpx(8);
+}
+.el-menu-vertical-demo .el-menu-item.is-active {
+  /* 可根据需求修改选中样式 */
+  background: linear-gradient(
+    to bottom,
+    #ffefb0,
+    #ffcc00
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  box-shadow: 0 8px 8px rgb(0, 0, 0, 0.3);
+  color: black;
+  font-size: rpx(8);
+}
+
+// 侧边栏
+.left-group2 {
+  width: rpx(150);
+  height: 100%;
+  background-color: #ece9fd;
+}
+.left-group2 img {
+  width: rpx(110);
+  height: auto;
+  margin-top: rpx(30);
+}
+.title-box {
+  height: rpx(50);
+}
+.box-icon {
+  width: 100%;
+  height: 100%;
+  flex: 1;
+  display: flex; // 添加 flex 布局
+  align-items: center; // 垂直居中
+  color: black; // 设置图标颜色为白色
+  padding-left: rpx(15);
+  font-size: rpx(10); // 设置图标大小,可按需调整
+  cursor: pointer; // 添加鼠标指针样式
+}
+.box-icon .left-icon {
+  margin-left: rpx(10);
+  margin-right: rpx(5); // 设置图标和文字之间的间距 ;
+}
+
+.number-people {
+  flex: 1;
+  height: 100%;
+  display: flex;
+  background-color: #ece9fd;
+}
+
+.content-box {
+  flex: 1;
+  margin-top: rpx(10);
+  margin-bottom: rpx(10);
+  margin-right: rpx(10);
+  border-radius: rpx(15);
+  background: rgba($color: #ffffff, $alpha: 0.5);
+  overflow-y: auto;
+}
+
+//左侧展览区图标
+.img-box {
+  margin-top: rpx(50);
+  color: #a39dce;
+}
+// 对话框
+.chat-dialog {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+.message-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: rpx(15);
+}
+/* 自定义滚动条样式 */
+.message-list::-webkit-scrollbar {
+  width: rpx(2); /* 滚动条宽度 */
+}
+.message-list::-webkit-scrollbar-track {
+  background: #f1effd; /* 滚动条轨道背景色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb {
+  background: #e2ddfc; /* 滚动条滑块颜色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb:hover {
+  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+}
+
+.message-list .user-message {
+  background-color: #ffffff;
+  margin-left: auto; // 消息靠右显示
+  margin-right: 0; // 重置右边距
+  max-width: rpx(400);
+  font-size: rpx(8);
+  width: fit-content; // 宽度随文字内容变化
+  border-radius: rpx(5);
+  padding: rpx(5);
+  text-align: left; // 文字左对齐
+}
+
+.message-list .ai-message {
+  background-color: #ffdd55;
+  margin-left: 0; // 消息靠左显示
+  margin-right: auto; // 重置右边距
+  margin-bottom: rpx(10);
+  width: fit-content;
+  max-width: rpx(400);
+  padding: rpx(5);
+  font-size: rpx(8);
+  border-radius: rpx(5);
+  text-align: left; // 文字左对齐
+}
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+
+.content-demo {
+  background-color: #f4f2fa;
+  border-radius: 15px;
+  padding: 30px 10px;
+}
+
+.input-section {
+  display: flex;
+  padding: rpx(10);
+  gap: rpx(5);
+  .speech-btn {
+    padding: rpx(5) rpx(10);
+    background: #fff;
+    border: 1px solid #ffce1b;
+    border-radius: rpx(5);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    &.recording {
+      background: #ffeeba;
+      border-color: #ffc107;
+
+      .el-icon {
+        color: #dc3545;
+      }
+    }
+    .el-icon {
+      font-size: rpx(8);
+      color: #666;
+    }
+  }
+  // 终止按钮样式
+  .stop-btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    img {
+      width: rpx(20);
+      height: rpx(20);
+    }
+  }
+}
+.input-section input {
+  flex: 1;
+  padding: rpx(5);
+  font-size: rpx(7);
+  border: 1px solid #ccc;
+  border-radius: rpx(5);
+}
+.input-section button {
+  padding: rpx(5) rpx(15);
+  background: linear-gradient(
+    to bottom,
+    #fee78a,
+    #ffce1b
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  color: black;
+  border: none;
+  font-size: rpx(7);
+  border-radius: rpx(5);
+  cursor: pointer;
+    box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+
+}
+
+.image-upload-section {
+  padding: rpx(10);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 798 - 0
src/views/AIImageToVideo.vue

@@ -0,0 +1,798 @@
+<template>
+  <!-- 图生视频 -->
+  <div class="home-container">
+     <!-- 展开收起侧边栏 -->
+    <div
+      class="icon-expand"
+      :style="{
+        backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
+        left: drawerVisible ? '18%' : '0'
+      }"
+      @click="toggleDrawer"
+    >
+      <span
+        class="vertical-lines"
+        :style="{
+          color: drawerVisible ? '#8a78d0' : 'white'
+        }"
+        >||</span
+      >
+    </div>
+
+    <!-- 左侧折叠面板 -->
+    <LeftPanel ref="leftPanelRef" v-if="drawerVisible"/>
+
+    <div class="left-group2">
+      <div class="title-box">
+        <div class="box-icon" @click="goBack">
+          <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+          图生视频
+        </div>
+      </div>
+      <div class="img-box">
+        <p>
+          <img
+              style=" width: fit-content; height: 180px; margin: 10px;"
+              src="@/assets/images/color.png"
+              class="avatar user"
+          />
+        </p>
+        <p>期待你的画作喔~</p>
+      </div>
+    </div>
+
+    <!-- 右侧AI问答 -->
+    <div class="number-people">
+      <div class="content-box">
+        <!-- AI对话框 -->
+        <div class="chat-dialog">
+          <!-- 对话消息列表 -->
+          <div class="message-list">
+            <div v-if="imageAllList.length > 0">
+                <div  v-for="(item, index) in imageAllList" :key="index">
+              <!-- 用户消息 -->
+              <div class="user-message" v-if="item.type === 'user'">
+                {{ item.content }}
+                <div class="user-image-list" v-if="item.imageUrl">
+                  <el-image
+                    style="width: fit-content; height: 180px; margin: 10px;"
+                    :src="item.imageUrl"
+                    :preview-src-list="[item.imageUrl]"
+                    fit="cover"
+                    show-progress
+                  >
+                    <template
+                      #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
+                    >
+                      <el-icon @click="prev"><Back /></el-icon>
+                      <el-icon @click="next"><Right /></el-icon>
+                      <el-icon @click="setActiveItem(item.imageList.length - 1)">
+                        <DArrowRight />
+                      </el-icon>
+                      <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
+                      <el-icon @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })"><ZoomIn /></el-icon>
+                      <el-icon @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })"><RefreshRight /></el-icon>
+                      <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
+                      <el-icon @click="reset"><Refresh /></el-icon>
+                      <el-icon @click="download(activeIndex)"><Download /></el-icon>
+                    </template>
+                  </el-image>
+                </div>
+              </div>
+                  <!-- AI生成图片对话框 -->
+                 <div class="ai-message" v-if="item.type !== 'user'">
+                {{ item.content }}
+                <div class="image-list" v-if="item.imageList && item.imageList.length > 0">
+                    <video
+                      v-for="(video, index) in item.imageList"
+                      :key="index"
+                      style="width: 30%; max-width: 600px; height: auto; margin: 10px;"
+                      :src="video"
+                      controls
+                      playsinline
+                    >
+                      您的浏览器不支持视频播放
+                  </video>
+                </div>
+              </div>
+                
+            </div>
+            </div>
+          </div>
+
+          <!-- 输入框和发送按钮 -->
+          <div class="input-section">
+            <input
+              type="text"
+              v-model="inputMessage"
+              placeholder="描述任何画面..."
+              @keyup.enter="sendMessage"
+              style="flex: 1; margin-right: 8px;"
+            />
+             <ImageUpload v-model="uploadedImage" ref="imageUploadRef"/>
+            <!-- 语音输入按钮 -->
+            <button
+                @click="toggleSpeechInput"
+                class="speech-btn"
+                :class="{ 'recording': isRecording }"
+            >
+              <el-icon v-if="!isRecording"><Microphone /></el-icon>
+              <el-icon v-else><Mute /></el-icon>
+              <!-- 显示倒计时(仅录音时显示) -->
+              <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
+            </button>
+
+            <!-- 终止按钮 -->
+            <div
+              v-if="conversationInProgress"
+              @click="stopStream"
+              class="stop-btn"
+              title="终止问答"
+            >
+              <img :src="stopicon" alt="停止" />
+            </div>
+            <button v-if="!conversationInProgress"
+              @click="sendMessage">发送</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted,onUnmounted} from 'vue'
+import {AiImageStatusEnum, CreatePainting, PaintingGetMys,CreateVideo, VideoGetMys} from '@/api/questions.js'
+import { useRouter, useRoute } from 'vue-router'
+import NumberPeople00 from '@/assets/images/xiaozhi.png'
+import {
+  Document,
+  Menu as IconMenu,
+  Location,
+  Setting,
+  ArrowLeftBold,
+  Fold,
+  Expand,
+  ChatLineRound,
+  Picture,
+  MagicStick,
+  Tickets,
+  User
+} from '@element-plus/icons-vue'
+
+import { saveRecord } from '@/api/personalized/index.js'
+
+// 导入全局状态
+import { globalState } from '@/utils/globalState.js'
+// 语音图标
+import { Microphone, Mute } from "@element-plus/icons-vue";
+// 终止按钮
+import stopicon from "@/assets/icon/stopicon.png";
+// 消息组件
+import {Message} from "@/utils/message/Message.js";
+
+// 上传参考图
+import ImageUpload from '@/components/ImageUpload/index.vue';
+// 导入getModelIdByType接口
+import { getModelIdByType } from '@/api/teachers.js'
+import { ModelTypeEnum } from '@/api/teachers.js'
+
+// 存储上传的图片
+const uploadedImage = ref('');
+const imageUploadRef = ref(null);
+
+// 语音输入响应式变量
+const isRecording = ref(false); // 录音状态
+const recognition = ref(null); // 语音识别实例
+const countdown = ref(0); // 倒计时剩余秒数
+const countdownTimer = ref(null); // 倒计时定
+// 对话状态变量
+const conversationInProgress = ref(false); // 对话是否正在进行中
+const conversationInAbortController = ref(); // 对话进行中 abort 控制器
+
+
+// 返回上一页
+const goBack = () => {
+  router.push('/ai-laboratory')
+}
+const router = useRouter()
+const route = useRoute()
+
+// 导入图片
+import question from '@/assets/icon/question.png'
+import painting from '@/assets/icon/painting.png'
+import human from '@/assets/icon/human.png'
+
+import LeftPanel from '@/components/LeftPanel.vue'
+const leftPanelRef = ref(null)
+
+
+// tts 语音
+import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
+const { playAudioChunk } = useAudioPlayer();
+
+// 添加抽屉显示状态
+const drawerVisible = ref(true)
+// 添加切换抽屉显示状态的函数
+const toggleDrawer = () => {
+  drawerVisible.value = !drawerVisible.value
+}
+
+
+  // 年级ID相关
+const gradeId = ref('')
+// 添加消息计数器变量
+const messageCount = ref(0)
+// modelId响应式变量
+const modelId = ref(0)
+// 保存记录
+onMounted(async () => {
+    // 从全局状态初始化年级ID
+  gradeId.value = globalState.initGradeId()
+  try{
+    const res = await saveRecord({
+        brpNjId: gradeId.value,
+        brpType: "aiCount",
+        brpProgress: 1
+      });
+      // 获取modelId
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_VIDEO, platform: "DouBao" })
+    modelId.value = modelRes.data
+  }catch(error){
+    console.error('保存记录失败:', error);
+  }
+});
+
+// 消息列表和输入内容的响应式变量
+const messages = ref([])
+const inputMessage = ref('')
+
+// 初始化语音识别
+const initSpeechRecognition = () => {
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+  if (!SpeechRecognition) {
+    alert("当前浏览器不支持语音输入功能");
+    return null;
+  }
+
+  const instance = new SpeechRecognition();
+  instance.lang = 'zh-CN';
+  instance.interimResults = false;
+
+  instance.onresult = (event) => {
+    if (event.results?.[0]?.[0]) {
+      inputMessage.value += event.results[0][0].transcript;
+    }
+  };
+
+  // 识别器结束时清除定时器
+  instance.onend = () => {
+    clearInterval(countdownTimer.value);
+    isRecording.value = false;
+    countdown.value = 0;
+  };
+
+  instance.onerror = (event) => {
+    console.error('语音识别错误:', event.error);
+    clearInterval(countdownTimer.value); // 出错时清除定时器
+    isRecording.value = false;
+    Message().error('语音输入失败,请重试!', true)
+    countdown.value = 0;
+  };
+
+  return instance;
+};
+
+// 切换录音状态
+const toggleSpeechInput = () => {
+  // 清除可能存在的旧定时器
+  clearInterval(countdownTimer.value);
+  countdownTimer.value = null;
+
+  if (isRecording.value) {
+    // 手动停止时重置状态
+    countdown.value = 0;
+    recognition.value?.stop();
+    isRecording.value = false;
+  } else {
+    // 初始化倒计时前再次清除定时器(防止快速点击)
+    clearInterval(countdownTimer.value);
+    countdown.value = 10; // 重置为10秒
+
+    recognition.value = initSpeechRecognition();
+    if (!recognition.value) return;
+
+    navigator.mediaDevices.getUserMedia({ audio: true })
+      .then(() => {
+        recognition.value.start();
+        isRecording.value = true;
+
+        // 启动新的倒计时定时器
+        countdownTimer.value = setInterval(() => {
+          countdown.value--;
+          if (countdown.value <= 0) {
+            clearInterval(countdownTimer.value); // 倒计时结束清除
+            recognition.value.stop();
+            isRecording.value = false;
+            countdown.value = 0;
+          }
+        }, 1000);
+      })
+      .catch((err) => {
+        console.error("麦克风权限获取失败:", err);
+        alert("请允许麦克风权限以使用语音输入");
+        // 出错时重置状态
+        isRecording.value = false;
+        countdown.value = 0;
+      });
+  }
+};
+
+// 停止操作函数
+const stopStream = async () => {
+  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
+  if (conversationInAbortController.value) {
+    conversationInAbortController.value.abort();
+  }
+  // 设置为 false
+  conversationInProgress.value = false;
+};
+
+// 发送消息函数
+const sendMessage = async() => {
+  if (inputMessage.value.trim()  || uploadedImage.value) {
+    // 创建 AbortController 实例,以便中止请求
+    conversationInAbortController.value = new AbortController();
+    // 标记对话进行中
+    conversationInProgress.value = true;
+    // messages.value.push(inputMessage.value.trim())
+    // 先保存内容 再置空输入框
+    let content = inputMessage.value;
+    inputMessage.value = ''
+    // 创建用户消息对象,包含可能的图片
+    const userMessage = {
+      type: 'user',
+      content: content,
+    };
+    
+    // 如果有上传的图片,添加到用户消息中
+    if (uploadedImage.value) {
+      userMessage.imageUrl = uploadedImage.value;
+      // 清空上传的图片
+      // uploadedImage.value = '';
+    }
+    // 添加用户消息到消息列表
+    imageAllList.value.push(userMessage);
+    imageAllList.value.push({
+      type: 'ai',
+      content: "正在为您生成视频,请稍等...",
+    })
+
+    // 递增消息计数器
+    messageCount.value++
+    // 发送saveRecord请求 保存消息次数
+     try{
+       await saveRecord({
+          brpNjId: gradeId.value,
+          brpType: "aiCount",
+          brpProgress: messageCount.value
+        });
+        console.log('保存记录成功,消息次数:', messageCount.value);
+    }catch(error){
+      console.error('保存记录失败:', error);
+      conversationInProgress.value = false;
+    }
+
+    try {
+      CreateVideo({
+        "modelId": modelId.value,
+        "prompt":content,
+        "duration":4,
+        "resolution":"1080P",
+        "promptImage":uploadedImage.value
+      }).then(res=>{
+        console.log("生成视频",res)
+        //目前写死调用已生成的图片,全部通了后再改
+        inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
+        // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
+      }).finally(() => {
+        // 图片生成请求完成后更新状态
+        conversationInProgress.value = false;
+      });
+    } catch (error) {
+      console.error('生成视频失败:', error);
+      conversationInProgress.value = false;
+    }
+  }
+  // 调用子组件的方法清除预览图
+  imageUploadRef.value?.clearPreview();
+};
+
+
+// 生成图片
+import { ElIcon } from 'element-plus'
+import {
+  Back,
+  DArrowRight,
+  Download,
+  Refresh,
+  RefreshLeft,
+  RefreshRight,
+  Right,
+  ZoomIn,
+  ZoomOut,
+} from '@element-plus/icons-vue'
+
+
+const imageAllList = ref([]) // 对话的消息列表
+const imageList = ref([]) // image 列表
+// 图片轮询相关的参数(正在生成中的)
+const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
+const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
+
+
+/** 轮询生成中的 image 列表 */
+const refreshWatchImages = async () => {
+  const imageIds = Object.keys(inProgressImageMap.value).map(Number)
+  if (imageIds.length === 0) {
+    return
+  }
+  const list = await VideoGetMys(imageIds)
+  
+      console.log('AI生成的视频地址2222222222:', list,imageIds);
+  const newWatchImages = {}
+  list?.data.forEach((image) => {
+    if (image.status === AiImageStatusEnum.IN_PROGRESS) {
+      newWatchImages[image.id] = image
+    } else {
+      imageAllList.value.pop();
+      console.log('AI生成的视频地址:', image.videoUrl);
+      imageAllList.value.push({
+        type: 'ai',
+        content: "已为您生成视频:",
+        imageList: [image.videoUrl],
+      })
+    }
+  })
+  inProgressImageMap.value = newWatchImages
+  if (newWatchImages.size === 0) {
+    inProgressTimerFun()
+  }
+}
+
+
+/** 组件挂在的时候 */
+onMounted(async () => {
+  refreshWatchImagesFun()
+})
+
+/** 组件取消挂在的时候 */
+onUnmounted(async () => {
+  inProgressTimerFun()
+})
+
+// 自动刷新 image 列表
+const refreshWatchImagesFun = () => {
+  inProgressTimer.value = setInterval(async () => {
+    await refreshWatchImages()
+  }, 1000 * 3)
+}
+
+// 停止刷新image列表
+const inProgressTimerFun = () => {
+  if (inProgressTimer.value) {
+    clearInterval(inProgressTimer.value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+// 用户图片列表样式
+.user-image-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: rpx(5);
+}
+/* 添加过渡样式 */
+.drawer-slide-enter-active,
+.drawer-slide-leave-active {
+  transition: all 0.3s ease;
+}
+.drawer-slide-enter-from,
+.drawer-slide-leave-to {
+  transform: translateX(-100%);
+  opacity: 0;
+}
+:deep(.el-image-viewer__wrapper) {
+  z-index: 10000 !important;
+}
+.icon-expand {
+  width: rpx(8);
+  height: rpx(35);
+  border-top-right-radius: rpx(5);
+  border-bottom-right-radius: rpx(5);
+  z-index: 9999;
+  position: absolute;
+  top: 50%;
+  left: 18%;
+  transform: translateY(-50%);
+  background-color: #44449c;
+  cursor: pointer; // 添加鼠标指针样式
+  clip-path: polygon(0 0, 100% 15%, 100% 85%, 0 100%);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  transition: all 0.3s ease;
+}
+.icon-expand .vertical-lines {
+  color: #8a78d0;
+  font-size: rpx(10);
+}
+.menu-icon {
+  width:rpx(11);
+  height: rpx(11);
+  margin-right: rpx(2);
+}
+// 侧边栏
+.left-group1 {
+  width: rpx(135);
+  height: 100%;
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+
+}
+.home-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: row;
+  gap: rpx(0);
+   background: linear-gradient(
+    to bottom,
+    #e2ddfc,
+    #f1effd
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+}
+
+// 侧边栏
+.left-group {
+  width: rpx(135);
+  height: 100%;
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+}
+.mb-2 {
+  color: black;
+  margin-top: rpx(1);
+}
+.tac ::v-deep(.el-menu) {
+  background-color: transparent;
+  border: none;
+  width: 100%;
+  margin-top: rpx(55);
+  margin-left: rpx(10);
+}
+.el-menu-item {
+  width: rpx(115);
+  height: rpx(25);
+  margin-bottom: rpx(5);
+  border-radius: rpx(6);
+  color: white;
+  font-size: rpx(8);
+}
+.el-menu-item .el-icon svg {
+  font-size: rpx(15);
+  color: white;
+}
+
+.el-menu ::v-deep(.el-menu-item:hover),
+.el-menu ::v-deep(.el-menu-item:focus),
+.el-menu ::v-deep(.el-menu-item:active) {
+  background: linear-gradient(
+    to bottom,
+    #ffefb0,
+    #ffcc00
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  box-shadow: 0 8px 8px rgb(0, 0, 0, 0.3);
+  color: black;
+  font-size: rpx(8);
+}
+.el-menu-vertical-demo .el-menu-item.is-active {
+  /* 可根据需求修改选中样式 */
+  background: linear-gradient(
+    to bottom,
+    #ffefb0,
+    #ffcc00
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  box-shadow: 0 8px 8px rgb(0, 0, 0, 0.3);
+  color: black;
+  font-size: rpx(8);
+}
+
+// 侧边栏
+.left-group2 {
+  width: rpx(150);
+  height: 100%;
+  background-color: #ece9fd;
+}
+.left-group2 img {
+  width: rpx(110);
+  height: auto;
+  margin-top: rpx(30);
+}
+.title-box {
+  height: rpx(50);
+}
+.box-icon {
+  width: 100%;
+  height: 100%;
+  flex: 1;
+  display: flex; // 添加 flex 布局
+  align-items: center; // 垂直居中
+  color: black; // 设置图标颜色为白色
+  padding-left: rpx(15);
+  font-size: rpx(10); // 设置图标大小,可按需调整
+  cursor: pointer; // 添加鼠标指针样式
+}
+.box-icon .left-icon {
+  margin-left: rpx(10);
+  margin-right: rpx(5); // 设置图标和文字之间的间距 ;
+}
+
+.number-people {
+  flex: 1;
+  height: 100%;
+  display: flex;
+  background-color: #ece9fd;
+}
+
+.content-box {
+  flex: 1;
+  margin-top: rpx(10);
+  margin-bottom: rpx(10);
+  margin-right: rpx(10);
+  border-radius: rpx(15);
+  background: rgba($color: #ffffff, $alpha: 0.5);
+  overflow-y: auto;
+}
+
+//左侧展览区图标
+.img-box {
+  margin-top: rpx(50);
+  color: #a39dce;
+}
+// 对话框
+.chat-dialog {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+.message-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: rpx(15);
+}
+/* 自定义滚动条样式 */
+.message-list::-webkit-scrollbar {
+  width: rpx(2); /* 滚动条宽度 */
+}
+.message-list::-webkit-scrollbar-track {
+  background: #f1effd; /* 滚动条轨道背景色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb {
+  background: #e2ddfc; /* 滚动条滑块颜色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb:hover {
+  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+}
+
+.message-list .user-message {
+  background-color: #ffffff;
+  margin-left: auto; // 消息靠右显示
+  margin-right: 0; // 重置右边距
+  max-width: rpx(400);
+  font-size: rpx(8);
+  width: fit-content; // 宽度随文字内容变化
+  border-radius: rpx(5);
+  padding: rpx(5);
+  text-align: left; // 文字左对齐
+}
+
+.message-list .ai-message {
+  background-color: #ffdd55;
+  margin-left: 0; // 消息靠左显示
+  margin-right: auto; // 重置右边距
+  margin-bottom: rpx(10);
+  width: fit-content;
+  max-width: rpx(400);
+  padding: rpx(5);
+  font-size: rpx(8);
+  border-radius: rpx(5);
+  text-align: left; // 文字左对齐
+}
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+
+.content-demo {
+  background-color: #f4f2fa;
+  border-radius: 15px;
+  padding: 30px 10px;
+}
+
+.input-section {
+  display: flex;
+  padding: rpx(10);
+  gap: rpx(5);
+  .speech-btn {
+    padding: rpx(5) rpx(10);
+    background: #fff;
+    border: 1px solid #ffce1b;
+    border-radius: rpx(5);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    &.recording {
+      background: #ffeeba;
+      border-color: #ffc107;
+
+      .el-icon {
+        color: #dc3545;
+      }
+    }
+    .el-icon {
+      font-size: rpx(8);
+      color: #666;
+    }
+  }
+  // 终止按钮样式
+  .stop-btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    img {
+      width: rpx(20);
+      height: rpx(20);
+    }
+  }
+}
+.input-section input {
+  flex: 1;
+  padding: rpx(5);
+  font-size: rpx(7);
+  border: 1px solid #ccc;
+  border-radius: rpx(5);
+}
+.input-section button {
+  padding: rpx(5) rpx(15);
+  background: linear-gradient(
+    to bottom,
+    #fee78a,
+    #ffce1b
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  color: black;
+  border: none;
+  font-size: rpx(7);
+  border-radius: rpx(5);
+  cursor: pointer;
+    box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+
+}
+
+.image-upload-section {
+  padding: rpx(10);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 18 - 0
src/views/AILaboratory.vue

@@ -276,9 +276,27 @@ const groupList = ref([
 }
 // 内容样式
 .content-box {
+  // width: 100%;
+  box-sizing: border-box;
+  cursor: pointer; // 鼠标指针样式
   flex: 1;
   display: flex;
   flex-wrap: wrap;
+  overflow-y: auto;
+}
+.content-box::-webkit-scrollbar {
+  width: rpx(2);
+}
+.content-box::-webkit-scrollbar-track {
+  background: transparent; // 设置滚动条轨道背景
+  border-radius: rpx(3); // 设置滚动条轨道圆角
+}
+.content-box::-webkit-scrollbar-thumb {
+  background: linear-gradient(to bottom, hsl(230, 100%, 21%), #8a78d0);
+  border-radius: rpx(3); // 设置滚动条滑块圆角
+}
+.content-box::-webkit-scrollbar-thumb:hover {
+  background: linear-gradient(to bottom, hsl(230, 100%, 21%), #8a78d0);
 }
 .small-box {
   flex: 0 0 calc(30% - rpx(10)); // 每个小盒子占三分之一宽度,减去间距

+ 36 - 2
src/views/AIPainting.vue

@@ -48,7 +48,8 @@
         <div class="chat-dialog">
           <!-- 对话消息列表 -->
           <div class="message-list">
-            <div v-if="imageAllList.length > 0" v-for="(item, index) in imageAllList" :key="index">
+            <div v-if="imageAllList.length > 0">
+              <div v-for="(item, index) in imageAllList" :key="index">
               <!-- 用户消息 -->
               <div class="user-message" v-if="item.type === 'user'">
                 {{ item.content }}
@@ -91,6 +92,8 @@
                 </div>
               </div>
             </div>
+            </div>
+            
 
             <div v-else class="content-demo">
               <h3>请参考示例:</h3>
@@ -209,6 +212,10 @@ import stopicon from "@/assets/icon/stopicon.png";
 // 消息组件
 import {Message} from "@/utils/message/Message.js";
 
+// 导入getModelIdByType接口
+import { getModelIdByType } from '@/api/teachers.js'
+import { ModelTypeEnum } from '@/api/teachers.js'
+
 // 语音输入响应式变量
 const isRecording = ref(false); // 录音状态
 const recognition = ref(null); // 语音识别实例
@@ -252,6 +259,9 @@ const demoImageList = [demo1, demo2, demo3, demo4]
 const gradeId = ref('')
 // 添加消息计数器变量
 const messageCount = ref(0)
+// modelId响应式变量
+const modelId = ref(0)
+
 // 保存记录
 onMounted(async () => {
     // 从全局状态初始化年级ID
@@ -262,6 +272,10 @@ onMounted(async () => {
         brpType: "aiCount",
         brpProgress: 1
       });
+      // 获取modelId
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.TEXT_TO_IMAGE, platform: "DouBao" })
+    modelId.value = modelRes.data
+    
   }catch(error){
     console.error('保存记录失败:', error);
   }
@@ -399,7 +413,7 @@ const sendMessage = async() => {
 
     try {
       CreatePainting({
-        "modelId": 56,
+        "modelId": modelId.value,
         "prompt":content,
         "width":1024,
         "height":1024
@@ -508,6 +522,9 @@ const inProgressTimerFun = () => {
   transform: translateX(-100%);
   opacity: 0;
 }
+:deep(.el-image-viewer__wrapper) {
+  z-index: 10000 !important;
+}
 .icon-expand {
   width: rpx(8);
   height: rpx(35);
@@ -656,6 +673,8 @@ const inProgressTimerFun = () => {
   margin-right: rpx(10);
   border-radius: rpx(15);
   background: rgba($color: #ffffff, $alpha: 0.5);
+  overflow-y: auto;
+
 }
 
 //左侧展览区图标
@@ -674,6 +693,21 @@ const inProgressTimerFun = () => {
   overflow-y: auto;
   padding: rpx(15);
 }
+/* 自定义滚动条样式 */
+.message-list::-webkit-scrollbar {
+  width: rpx(2); /* 滚动条宽度 */
+}
+.message-list::-webkit-scrollbar-track {
+  background: #f1effd; /* 滚动条轨道背景色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb {
+  background: #e2ddfc; /* 滚动条滑块颜色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb:hover {
+  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+}
 
 .message-list .user-message {
   background-color: #ffffff;

+ 5 - 5
src/views/QuickLogin.vue

@@ -81,17 +81,17 @@ const autoLogin = async () => {
         localStorage.setItem('maxCourseSections', 'all')
       }
 
-      ElMessage.success('自动登录成功')
-      // 跳转到首页
+      ElMessage.success('信息校验成功')
+      // 跳转到课程界面
       router.push('/ai-general-course')
     } else {
-      ElMessage.error(res?.message || '自动登录失败')
+      ElMessage.error(res?.message || '信息校验失败')
       // 如果登录失败,跳转到正常登录页面
       router.push('/login')
     }
   } catch (error) {
-    ElMessage.error('自动登录过程中发生错误')
-    console.error('自动登录错误:', error)
+    ElMessage.error('信息校验过程中发生错误')
+    console.error('信息校验错误:', error)
     // 错误时跳转到登录页面
     router.push('/login')
   } finally {