|
@@ -7,7 +7,7 @@
|
|
|
<!-- 对话消息列表 -->
|
|
<!-- 对话消息列表 -->
|
|
|
<div class="message-list">
|
|
<div class="message-list">
|
|
|
<div v-if="imageAllList.length > 0">
|
|
<div v-if="imageAllList.length > 0">
|
|
|
- <div v-for="(item, index) in imageAllList" :key="index">
|
|
|
|
|
|
|
+ <div v-for="(item, index) in imageAllList" :key="item.id || index">
|
|
|
<!-- 用户消息 -->
|
|
<!-- 用户消息 -->
|
|
|
<div class="user-message" v-if="item.type === 'user'">
|
|
<div class="user-message" v-if="item.type === 'user'">
|
|
|
{{ item.content }}
|
|
{{ item.content }}
|
|
@@ -24,7 +24,7 @@
|
|
|
>
|
|
>
|
|
|
<el-icon @click="prev"><Back /></el-icon>
|
|
<el-icon @click="prev"><Back /></el-icon>
|
|
|
<el-icon @click="next"><Right /></el-icon>
|
|
<el-icon @click="next"><Right /></el-icon>
|
|
|
- <el-icon @click="setActiveItem(item.imageList.length - 1)">
|
|
|
|
|
|
|
+ <el-icon @click="setActiveItem(0)">
|
|
|
<DArrowRight />
|
|
<DArrowRight />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
<el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
|
|
<el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
|
|
@@ -89,8 +89,8 @@
|
|
|
style="flex: 1; margin-right: 8px;"
|
|
style="flex: 1; margin-right: 8px;"
|
|
|
/>
|
|
/>
|
|
|
<!-- 风格选择下拉框 -->
|
|
<!-- 风格选择下拉框 -->
|
|
|
- <el-select
|
|
|
|
|
- v-model="selectedStyle"
|
|
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="selectedStyle"
|
|
|
placeholder="选择风格"
|
|
placeholder="选择风格"
|
|
|
filterable
|
|
filterable
|
|
|
allow-create
|
|
allow-create
|
|
@@ -99,10 +99,10 @@
|
|
|
size="small"
|
|
size="small"
|
|
|
class="custom-style-select"
|
|
class="custom-style-select"
|
|
|
>
|
|
>
|
|
|
- <el-option
|
|
|
|
|
- v-for="style in styleList"
|
|
|
|
|
- :key="style"
|
|
|
|
|
- :label="style"
|
|
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="style in styleList"
|
|
|
|
|
+ :key="style"
|
|
|
|
|
+ :label="style"
|
|
|
:value="style"
|
|
:value="style"
|
|
|
/>
|
|
/>
|
|
|
</el-select>
|
|
</el-select>
|
|
@@ -138,26 +138,17 @@
|
|
|
import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
|
|
import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
|
|
|
import {AiImageStatusEnum, CreatePainting, PaintingGetMys, getMyAiPointBalance} from '@/api/questions.js'
|
|
import {AiImageStatusEnum, CreatePainting, PaintingGetMys, getMyAiPointBalance} from '@/api/questions.js'
|
|
|
import VoiceInput from '../voice/VoiceInput_Api.vue'
|
|
import VoiceInput from '../voice/VoiceInput_Api.vue'
|
|
|
-
|
|
|
|
|
-// 导入全局状态
|
|
|
|
|
-import { globalState } from '@/utils/globalState.js'
|
|
|
|
|
-// 终止按钮
|
|
|
|
|
import stopicon from "@/assets/icon/stopicon.png";
|
|
import stopicon from "@/assets/icon/stopicon.png";
|
|
|
-// 消息组件
|
|
|
|
|
import {Message} from "@/utils/message/Message.js";
|
|
import {Message} from "@/utils/message/Message.js";
|
|
|
-
|
|
|
|
|
-// 上传参考图
|
|
|
|
|
import ImageUpload from '@/components/ImageUpload/index.vue';
|
|
import ImageUpload from '@/components/ImageUpload/index.vue';
|
|
|
-
|
|
|
|
|
-// 导入接口
|
|
|
|
|
-import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
|
|
|
|
|
-import { ModelTypeEnum } from '@/api/teachers.js'
|
|
|
|
|
|
|
+import {getModelIdByType, ModelPlatformEnum, ModelTypeEnum} from '@/api/teachers.js'
|
|
|
|
|
+import {
|
|
|
|
|
+ Back, DArrowRight, Download, Refresh, RefreshLeft, RefreshRight, Right, ZoomIn, ZoomOut,
|
|
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
|
|
|
|
|
// 定义props
|
|
// 定义props
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- //根据需求传输
|
|
|
|
|
isCourse: { type: Boolean, default: false},
|
|
isCourse: { type: Boolean, default: false},
|
|
|
- cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToImage" },
|
|
|
|
|
cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToImage_history" },
|
|
cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToImage_history" },
|
|
|
preDialogueList: { type: Array, default: () => []},
|
|
preDialogueList: { type: Array, default: () => []},
|
|
|
replySupplement: { type: String, default: ''},
|
|
replySupplement: { type: String, default: ''},
|
|
@@ -169,111 +160,68 @@ const emits = defineEmits(['saveProgress'])
|
|
|
//历史记录缓存集合
|
|
//历史记录缓存集合
|
|
|
const cacheDataHistoryList = ref([]);
|
|
const cacheDataHistoryList = ref([]);
|
|
|
|
|
|
|
|
-// 存储上传的图片
|
|
|
|
|
const uploadedImage = ref('');
|
|
const uploadedImage = ref('');
|
|
|
-
|
|
|
|
|
const imageUploadRef = ref(null);
|
|
const imageUploadRef = ref(null);
|
|
|
-
|
|
|
|
|
-// 当前选中的风格
|
|
|
|
|
const selectedStyle = ref('');
|
|
const selectedStyle = ref('');
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-// 风格列表
|
|
|
|
|
const styleList = ['电影写真', '中国风', '卡通', '动漫', '素描', '像素风格'];
|
|
const styleList = ['电影写真', '中国风', '卡通', '动漫', '素描', '像素风格'];
|
|
|
-// 对话状态变量
|
|
|
|
|
-const conversationInProgress = ref(false); // 对话是否正在进行中
|
|
|
|
|
-const conversationInAbortController = ref(); // 对话进行中 abort 控制器
|
|
|
|
|
-
|
|
|
|
|
-// tts 语音
|
|
|
|
|
-import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
|
|
|
|
|
-
|
|
|
|
|
-// 添加抽屉显示状态
|
|
|
|
|
-const drawerVisible = ref(true)
|
|
|
|
|
-
|
|
|
|
|
- // 年级ID相关
|
|
|
|
|
-const gradeId = ref('')
|
|
|
|
|
-// modelId响应式变量
|
|
|
|
|
|
|
+const conversationInProgress = ref(false);
|
|
|
const modelId = ref(0)
|
|
const modelId = ref(0)
|
|
|
-// 保存记录
|
|
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
- // 从全局状态初始化年级ID
|
|
|
|
|
- gradeId.value = globalState.initGradeId()
|
|
|
|
|
- try{
|
|
|
|
|
- // 获取modelId
|
|
|
|
|
|
|
+ try {
|
|
|
const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
|
|
const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
|
|
|
modelId.value = modelRes.data
|
|
modelId.value = modelRes.data
|
|
|
- }catch(error){
|
|
|
|
|
- console.error('保存记录失败:', error);
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取模型ID失败:', error);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 取缓存历史记录
|
|
|
|
|
let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
|
|
let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
|
|
|
if (cacheDataHistoryStr) {
|
|
if (cacheDataHistoryStr) {
|
|
|
cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
|
|
cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
|
|
|
imageAllList.value.push(...cacheDataHistoryList.value);
|
|
imageAllList.value.push(...cacheDataHistoryList.value);
|
|
|
- }else{
|
|
|
|
|
- // 加入预置对话
|
|
|
|
|
|
|
+ } else {
|
|
|
imageAllList.value.push(...props.preDialogueList);
|
|
imageAllList.value.push(...props.preDialogueList);
|
|
|
cacheDataHistoryList.value.push(...props.preDialogueList);
|
|
cacheDataHistoryList.value.push(...props.preDialogueList);
|
|
|
- if (cacheDataHistoryList.value.length > 0) localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
|
|
|
|
+ if (cacheDataHistoryList.value.length > 0) {
|
|
|
|
|
+ localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 自动刷新 image 列表
|
|
|
|
|
refreshWatchImagesFun()
|
|
refreshWatchImagesFun()
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 消息列表和输入内容的响应式变量
|
|
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ inProgressTimerFun()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const inputMessage = ref('')
|
|
const inputMessage = ref('')
|
|
|
-// 输入框引用
|
|
|
|
|
const inputRef = ref(null)
|
|
const inputRef = ref(null)
|
|
|
|
|
|
|
|
|
|
+const isVoiceRecording = ref(false);
|
|
|
|
|
+const voiceRecognizedText = ref("");
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-// 导入计算属性
|
|
|
|
|
-
|
|
|
|
|
-// 语音输入状态跟踪
|
|
|
|
|
-const isVoiceRecording = ref(false); // 当前是否正在录音
|
|
|
|
|
-const voiceRecognizedText = ref(""); // 实时语音识别结果
|
|
|
|
|
-
|
|
|
|
|
-// 用于控制输入框显示的内容
|
|
|
|
|
const displayedPrompt = computed({
|
|
const displayedPrompt = computed({
|
|
|
- get() {
|
|
|
|
|
- // 录音时,只显示inputMessage.value(已经在handleVoiceRecognized中实时更新)
|
|
|
|
|
- if (isVoiceRecording.value) {
|
|
|
|
|
- return inputMessage.value;
|
|
|
|
|
- }
|
|
|
|
|
- // 不录音时,只显示inputMessage.value
|
|
|
|
|
- return inputMessage.value;
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ get() { return inputMessage.value },
|
|
|
set(newValue) {
|
|
set(newValue) {
|
|
|
- // 只在用户手动输入时更新inputMessage.value
|
|
|
|
|
- if (!isVoiceRecording.value) {
|
|
|
|
|
- inputMessage.value = newValue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!isVoiceRecording.value) inputMessage.value = newValue
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 语音输入识别结果处理
|
|
// 语音输入识别结果处理
|
|
|
const handleVoiceRecognized = (data) => {
|
|
const handleVoiceRecognized = (data) => {
|
|
|
if (isVoiceRecording.value) {
|
|
if (isVoiceRecording.value) {
|
|
|
- // 在同一次录音过程中,实时更新文本框内容
|
|
|
|
|
voiceRecognizedText.value = data.originalText;
|
|
voiceRecognizedText.value = data.originalText;
|
|
|
inputMessage.value = data.processedText;
|
|
inputMessage.value = data.processedText;
|
|
|
} else {
|
|
} else {
|
|
|
- // 在录音结束时,将最终的语音内容插入到光标位置
|
|
|
|
|
const input = document.querySelector('input[type="text"]');
|
|
const input = document.querySelector('input[type="text"]');
|
|
|
if (input) {
|
|
if (input) {
|
|
|
inputMessage.value = data.processedText;
|
|
inputMessage.value = data.processedText;
|
|
|
- // 重新设置光标位置到插入文本的末尾
|
|
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
input.selectionStart = input.selectionEnd = data.cursorPos;
|
|
input.selectionStart = input.selectionEnd = data.cursorPos;
|
|
|
}, 0);
|
|
}, 0);
|
|
|
} else {
|
|
} else {
|
|
|
- // 如果没有找到输入框,直接替换整个内容
|
|
|
|
|
inputMessage.value = data.originalText;
|
|
inputMessage.value = data.originalText;
|
|
|
}
|
|
}
|
|
|
- // 清空临时变量
|
|
|
|
|
voiceRecognizedText.value = "";
|
|
voiceRecognizedText.value = "";
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -283,24 +231,20 @@ const handleRecordingStatusChanged = (status) => {
|
|
|
const wasRecording = isVoiceRecording.value;
|
|
const wasRecording = isVoiceRecording.value;
|
|
|
isVoiceRecording.value = status;
|
|
isVoiceRecording.value = status;
|
|
|
|
|
|
|
|
- // 如果是从录音状态切换到非录音状态,清空临时变量
|
|
|
|
|
if (wasRecording && !status) {
|
|
if (wasRecording && !status) {
|
|
|
- // 清空临时变量
|
|
|
|
|
voiceRecognizedText.value = "";
|
|
voiceRecognizedText.value = "";
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 停止操作函数
|
|
// 停止操作函数
|
|
|
-const stopStream = async () => {
|
|
|
|
|
- // tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
|
|
|
|
- if (conversationInAbortController.value) {
|
|
|
|
|
- conversationInAbortController.value.abort();
|
|
|
|
|
- }
|
|
|
|
|
- // 设置为 false
|
|
|
|
|
|
|
+const stopStream = () => {
|
|
|
conversationInProgress.value = false;
|
|
conversationInProgress.value = false;
|
|
|
|
|
+ inProgressTimerFun()
|
|
|
|
|
+ inProgressImageMap.value = {}
|
|
|
|
|
+ const idx = imageAllList.value.findIndex(m => m.loading === true)
|
|
|
|
|
+ if (idx !== -1) imageAllList.value.splice(idx, 1)
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 发送消息函数,图片数据
|
|
|
|
|
const ensureAiPoint = async (cost) => {
|
|
const ensureAiPoint = async (cost) => {
|
|
|
try {
|
|
try {
|
|
|
const res = await getMyAiPointBalance()
|
|
const res = await getMyAiPointBalance()
|
|
@@ -313,208 +257,191 @@ const ensureAiPoint = async (cost) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const sendMessage = async() => {
|
|
const sendMessage = async() => {
|
|
|
- if (uploadedImage.value) {
|
|
|
|
|
- if (!await ensureAiPoint(100)) return
|
|
|
|
|
- // 如果正在录音,先停止录音
|
|
|
|
|
- if (isVoiceRecording.value) {
|
|
|
|
|
- const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
|
|
|
|
|
- if (voiceInputComponent) {
|
|
|
|
|
- voiceInputComponent.click();
|
|
|
|
|
- }
|
|
|
|
|
- // 等待录音结束后再发送
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- sendMessage();
|
|
|
|
|
- }, 100);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- // 创建 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: "正在为您生成图片,请稍等",
|
|
|
|
|
- loading: true
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ if (!uploadedImage.value) {
|
|
|
|
|
+ Message().error('请先上传参考图!', true);
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const content = inputMessage.value.trim();
|
|
|
|
|
+ if (!content) {
|
|
|
|
|
+ Message().error('请描述要生成的图片内容');
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (conversationInProgress.value) return
|
|
|
|
|
+
|
|
|
|
|
+ if (!await ensureAiPoint(100)) return
|
|
|
|
|
|
|
|
- // 通过emit事件通知父组件保存进度
|
|
|
|
|
- emits('saveProgress', "aiCount", 1)
|
|
|
|
|
- if (props.isCourse){
|
|
|
|
|
- emits('saveProgress', "course", 100)
|
|
|
|
|
|
|
+ // 如果正在录音,先停止录音
|
|
|
|
|
+ if (isVoiceRecording.value) {
|
|
|
|
|
+ const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
|
|
|
|
|
+ if (voiceInputComponent) {
|
|
|
|
|
+ voiceInputComponent.click();
|
|
|
}
|
|
}
|
|
|
|
|
+ setTimeout(() => sendMessage(), 100);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ conversationInProgress.value = true;
|
|
|
|
|
+ inputMessage.value = '';
|
|
|
|
|
|
|
|
- // 同步历史记录缓存
|
|
|
|
|
- cacheDataHistoryList.value.push(userMessage);
|
|
|
|
|
- localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
|
|
|
|
+ const userMessage = {
|
|
|
|
|
+ id: genMsgId(),
|
|
|
|
|
+ type: 'user',
|
|
|
|
|
+ content,
|
|
|
|
|
+ imageUrl: uploadedImage.value,
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 如果没有上传图片,显示提示信息
|
|
|
|
|
- Message().error('请先上传参考图!', true);
|
|
|
|
|
|
|
+ imageAllList.value.push(userMessage);
|
|
|
|
|
+
|
|
|
|
|
+ const loadingMsg = { id: genMsgId(), type: 'ai', content: '正在为您生成图片,请稍等', loading: true }
|
|
|
|
|
+ imageAllList.value.push(loadingMsg);
|
|
|
|
|
+
|
|
|
|
|
+ emits('saveProgress', 'aiCount', 1)
|
|
|
|
|
+ if (props.isCourse) emits('saveProgress', 'course', 100)
|
|
|
|
|
+
|
|
|
|
|
+ cacheDataHistoryList.value.push(userMessage);
|
|
|
|
|
+ localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await CreatePainting({
|
|
|
|
|
+ modelId: modelId.value,
|
|
|
|
|
+ prompt: content,
|
|
|
|
|
+ width: 1024,
|
|
|
|
|
+ height: 1024,
|
|
|
|
|
+ promptImage: uploadedImage.value
|
|
|
|
|
+ })
|
|
|
|
|
+ inProgressImageMap.value[res.data] = { id: res.data, status: AiImageStatusEnum.IN_PROGRESS }
|
|
|
|
|
+ pollFailCount.value = 0
|
|
|
|
|
+ if (!inProgressTimer.value) refreshWatchImagesFun()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
|
|
|
|
|
+ if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
|
|
|
|
|
+ Message().error('创建图片生成任务失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ conversationInProgress.value = false;
|
|
|
}
|
|
}
|
|
|
- // 调用子组件的方法清除预览图
|
|
|
|
|
|
|
+
|
|
|
imageUploadRef.value?.clearPreview();
|
|
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 inProgressImageMap = ref({})
|
|
|
|
|
+const inProgressTimer = ref()
|
|
|
|
|
+const pollFailCount = ref(0)
|
|
|
|
|
+const MAX_POLL_FAIL = 5
|
|
|
|
|
|
|
|
|
|
+/** 生成唯一ID */
|
|
|
|
|
+const genMsgId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
|
|
|
|
|
|
-const imageAllList = ref([]) // 对话的消息列表
|
|
|
|
|
-// 图片轮询相关的参数(正在生成中的)
|
|
|
|
|
-const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
|
|
|
|
-const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
|
|
|
|
|
-
|
|
|
|
|
|
|
+/** 解析 ArkHttpException 错误信息 */
|
|
|
|
|
+const parseArkErrorMessage = (errorMessage) => {
|
|
|
|
|
+ if (!errorMessage) return '未知错误'
|
|
|
|
|
+ const match = errorMessage.match(/message='([^']*)'/)
|
|
|
|
|
+ if (match) return match[1]
|
|
|
|
|
+ return errorMessage
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-/** 轮询生成中的 image 列表 */
|
|
|
|
|
|
|
+/** 轮询生成中的任务 */
|
|
|
const refreshWatchImages = async () => {
|
|
const refreshWatchImages = async () => {
|
|
|
const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
|
const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
|
|
- if (imageIds.length === 0) {
|
|
|
|
|
|
|
+ if (imageIds.length === 0) return
|
|
|
|
|
+
|
|
|
|
|
+ let list
|
|
|
|
|
+ try {
|
|
|
|
|
+ list = await PaintingGetMys(imageIds)
|
|
|
|
|
+ pollFailCount.value = 0
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ pollFailCount.value++
|
|
|
|
|
+ if (pollFailCount.value >= MAX_POLL_FAIL) {
|
|
|
|
|
+ inProgressTimerFun()
|
|
|
|
|
+ inProgressImageMap.value = {}
|
|
|
|
|
+ Message().error('查询图片生成状态失败,已停止轮询,请稍后重试')
|
|
|
|
|
+ }
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- const list = await PaintingGetMys(imageIds)
|
|
|
|
|
|
|
+
|
|
|
const newWatchImages = {}
|
|
const newWatchImages = {}
|
|
|
- list.data.forEach((image) => {
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ list?.data.forEach((image) => {
|
|
|
if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
|
newWatchImages[image.id] = image
|
|
newWatchImages[image.id] = image
|
|
|
- } else {
|
|
|
|
|
- imageAllList.value.pop();
|
|
|
|
|
- console.log('AI生成的图片地址:', image.picUrl);
|
|
|
|
|
- let imageMsg = {
|
|
|
|
|
- type: 'ai',
|
|
|
|
|
- content: "已为您生成图片:",
|
|
|
|
|
- imageList: [image.picUrl],
|
|
|
|
|
- }
|
|
|
|
|
- imageAllList.value.push(imageMsg)
|
|
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 精确找到并移除对应的 loading 占位消息
|
|
|
|
|
+ const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
|
|
|
|
|
+ if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
|
|
|
|
|
+
|
|
|
|
|
+ if (image.status === AiImageStatusEnum.FAIL) {
|
|
|
|
|
+ const errorMsg = parseArkErrorMessage(image.errorMessage)
|
|
|
|
|
+ const failMsg = { id: genMsgId(), type: 'ai', content: `生成图片失败:${errorMsg}` }
|
|
|
|
|
+ imageAllList.value.push(failMsg)
|
|
|
|
|
+ cacheDataHistoryList.value.push(failMsg)
|
|
|
|
|
+ localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value))
|
|
|
|
|
+ Message().error(`图片生成失败:${errorMsg}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 添加补充信息
|
|
|
|
|
- if (!conversationInProgress.value && props.replySupplement) {
|
|
|
|
|
- imageAllList.value.push({
|
|
|
|
|
- type: "system",
|
|
|
|
|
- content: props.replySupplement,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const imageMsg = { id: genMsgId(), type: 'ai', content: '已为您生成图片:', imageList: [image.picUrl] }
|
|
|
|
|
+ imageAllList.value.push(imageMsg)
|
|
|
|
|
|
|
|
- // 同步缓存历史记录
|
|
|
|
|
- cacheDataHistoryList.value.push(imageMsg);
|
|
|
|
|
- localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
|
|
|
|
+ if (!conversationInProgress.value && props.replySupplement) {
|
|
|
|
|
+ imageAllList.value.push({ id: genMsgId(), type: 'system', content: props.replySupplement })
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ cacheDataHistoryList.value.push(imageMsg);
|
|
|
|
|
+ localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
inProgressImageMap.value = newWatchImages
|
|
inProgressImageMap.value = newWatchImages
|
|
|
- if (newWatchImages.size === 0) {
|
|
|
|
|
|
|
+ if (Object.keys(newWatchImages).length === 0) {
|
|
|
inProgressTimerFun()
|
|
inProgressTimerFun()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 组件取消挂在的时候 */
|
|
|
|
|
-onUnmounted(async () => {
|
|
|
|
|
- inProgressTimerFun()
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-// 自动刷新 image 列表
|
|
|
|
|
|
|
+/** 启动轮询(已运行则跳过) */
|
|
|
const refreshWatchImagesFun = () => {
|
|
const refreshWatchImagesFun = () => {
|
|
|
|
|
+ if (inProgressTimer.value) return
|
|
|
inProgressTimer.value = setInterval(async () => {
|
|
inProgressTimer.value = setInterval(async () => {
|
|
|
await refreshWatchImages()
|
|
await refreshWatchImages()
|
|
|
- }, 1000 * 3)
|
|
|
|
|
|
|
+ }, 3000)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 停止刷新image列表
|
|
|
|
|
|
|
+/** 停止轮询 */
|
|
|
const inProgressTimerFun = () => {
|
|
const inProgressTimerFun = () => {
|
|
|
if (inProgressTimer.value) {
|
|
if (inProgressTimer.value) {
|
|
|
clearInterval(inProgressTimer.value)
|
|
clearInterval(inProgressTimer.value)
|
|
|
|
|
+ inProgressTimer.value = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理风格选择变化事件
|
|
// 处理风格选择变化事件
|
|
|
const handleStyleSelectChange = (style) => {
|
|
const handleStyleSelectChange = (style) => {
|
|
|
- // 检查输入框中是否已经包含风格描述,如果有则替换,没有则添加
|
|
|
|
|
const styleRegex = /生成图片风格为[^,]+/g;
|
|
const styleRegex = /生成图片风格为[^,]+/g;
|
|
|
if (styleRegex.test(inputMessage.value)) {
|
|
if (styleRegex.test(inputMessage.value)) {
|
|
|
- // 替换现有的风格描述
|
|
|
|
|
inputMessage.value = inputMessage.value.replace(styleRegex, `生成图片风格为${style}`);
|
|
inputMessage.value = inputMessage.value.replace(styleRegex, `生成图片风格为${style}`);
|
|
|
} else {
|
|
} else {
|
|
|
- // 添加新的风格描述
|
|
|
|
|
inputMessage.value += `生成图片风格为${style}`;
|
|
inputMessage.value += `生成图片风格为${style}`;
|
|
|
}
|
|
}
|
|
|
- // 选中风格后,将光标自动对焦到输入
|
|
|
|
|
inputRef.value?.focus();
|
|
inputRef.value?.focus();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 图片下载功能
|
|
// 图片下载功能
|
|
|
const download = (event, activeIndex) => {
|
|
const download = (event, activeIndex) => {
|
|
|
- // 获取当前点击的图片
|
|
|
|
|
const target = event.target || {};
|
|
const target = event.target || {};
|
|
|
- // 从父元素找到图片预览器,并获取当前图片URL
|
|
|
|
|
const imgPreview = target.closest('.el-image-viewer__wrapper');
|
|
const imgPreview = target.closest('.el-image-viewer__wrapper');
|
|
|
if (!imgPreview) return;
|
|
if (!imgPreview) return;
|
|
|
- // 获取所有预览图片
|
|
|
|
|
const imgElements = imgPreview.querySelectorAll('.el-image-viewer__img');
|
|
const imgElements = imgPreview.querySelectorAll('.el-image-viewer__img');
|
|
|
if (!imgElements || imgElements.length === 0) return;
|
|
if (!imgElements || imgElements.length === 0) return;
|
|
|
- // 确定要下载的图片URL,根据传入的 activeIndex 获取对应图片的URL
|
|
|
|
|
let imgUrl;
|
|
let imgUrl;
|
|
|
if (imgElements[activeIndex]) {
|
|
if (imgElements[activeIndex]) {
|
|
|
imgUrl = imgElements[activeIndex].src;
|
|
imgUrl = imgElements[activeIndex].src;
|
|
|
} else {
|
|
} else {
|
|
|
imgUrl = imgElements[0].src;
|
|
imgUrl = imgElements[0].src;
|
|
|
}
|
|
}
|
|
|
- // 创建下载链接并触发下载
|
|
|
|
|
const link = document.createElement('a');
|
|
const link = document.createElement('a');
|
|
|
link.download = `image_${new Date().getTime()}.png`;
|
|
link.download = `image_${new Date().getTime()}.png`;
|
|
|
link.href = imgUrl;
|
|
link.href = imgUrl;
|
|
|
- // 触发下载
|
|
|
|
|
document.body.appendChild(link);
|
|
document.body.appendChild(link);
|
|
|
link.click();
|
|
link.click();
|
|
|
document.body.removeChild(link);
|
|
document.body.removeChild(link);
|
|
@@ -524,11 +451,9 @@ const download = (event, activeIndex) => {
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
@use 'sass:math';
|
|
@use 'sass:math';
|
|
|
-// 定义rpx转换函数
|
|
|
|
|
@function rpx($px) {
|
|
@function rpx($px) {
|
|
|
@return math.div($px, 750) * 100vw;
|
|
@return math.div($px, 750) * 100vw;
|
|
|
}
|
|
}
|
|
|
-// 用户图片列表样式
|
|
|
|
|
.user-image-list {
|
|
.user-image-list {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
@@ -543,15 +468,12 @@ const download = (event, activeIndex) => {
|
|
|
|
|
|
|
|
.content-box {
|
|
.content-box {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- // margin-top: rpx(10);
|
|
|
|
|
- // margin-bottom: rpx(10);
|
|
|
|
|
margin: rpx(7);
|
|
margin: rpx(7);
|
|
|
border-radius: rpx(15);
|
|
border-radius: rpx(15);
|
|
|
background: rgba($color: #ffffff, $alpha: 0.5);
|
|
background: rgba($color: #ffffff, $alpha: 0.5);
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 对话框
|
|
|
|
|
.chat-dialog {
|
|
.chat-dialog {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
@@ -562,48 +484,46 @@ const download = (event, activeIndex) => {
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
padding: rpx(15);
|
|
padding: rpx(15);
|
|
|
}
|
|
}
|
|
|
-/* 自定义滚动条样式 */
|
|
|
|
|
.message-list::-webkit-scrollbar {
|
|
.message-list::-webkit-scrollbar {
|
|
|
- width: rpx(2); /* 滚动条宽度 */
|
|
|
|
|
|
|
+ width: rpx(2);
|
|
|
}
|
|
}
|
|
|
.message-list::-webkit-scrollbar-track {
|
|
.message-list::-webkit-scrollbar-track {
|
|
|
- background: #f1effd; /* 滚动条轨道背景色 */
|
|
|
|
|
|
|
+ background: #f1effd;
|
|
|
border-radius: rpx(4);
|
|
border-radius: rpx(4);
|
|
|
}
|
|
}
|
|
|
.message-list::-webkit-scrollbar-thumb {
|
|
.message-list::-webkit-scrollbar-thumb {
|
|
|
- background: #e2ddfc; /* 滚动条滑块颜色 */
|
|
|
|
|
|
|
+ background: #e2ddfc;
|
|
|
border-radius: rpx(4);
|
|
border-radius: rpx(4);
|
|
|
}
|
|
}
|
|
|
.message-list::-webkit-scrollbar-thumb:hover {
|
|
.message-list::-webkit-scrollbar-thumb:hover {
|
|
|
- background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
|
|
|
|
|
|
|
+ background: #e2ddfc;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.message-list .user-message {
|
|
.message-list .user-message {
|
|
|
background-color: #ffffff;
|
|
background-color: #ffffff;
|
|
|
- margin-left: auto; // 消息靠右显示
|
|
|
|
|
- margin-right: 0; // 重置右边距
|
|
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ margin-right: 0;
|
|
|
max-width: rpx(400);
|
|
max-width: rpx(400);
|
|
|
font-size: rpx(8);
|
|
font-size: rpx(8);
|
|
|
- width: fit-content; // 宽度随文字内容变化
|
|
|
|
|
|
|
+ width: fit-content;
|
|
|
border-radius: rpx(5);
|
|
border-radius: rpx(5);
|
|
|
padding: rpx(5);
|
|
padding: rpx(5);
|
|
|
- text-align: left; // 文字左对齐
|
|
|
|
|
|
|
+ text-align: left;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.message-list .ai-message {
|
|
.message-list .ai-message {
|
|
|
background-color: #ffdd55;
|
|
background-color: #ffdd55;
|
|
|
- margin-left: 0; // 消息靠左显示
|
|
|
|
|
- margin-right: auto; // 重置右边距
|
|
|
|
|
|
|
+ margin-left: 0;
|
|
|
|
|
+ margin-right: auto;
|
|
|
margin-bottom: rpx(10);
|
|
margin-bottom: rpx(10);
|
|
|
width: fit-content;
|
|
width: fit-content;
|
|
|
max-width: rpx(400);
|
|
max-width: rpx(400);
|
|
|
padding: rpx(5);
|
|
padding: rpx(5);
|
|
|
font-size: rpx(8);
|
|
font-size: rpx(8);
|
|
|
border-radius: rpx(5);
|
|
border-radius: rpx(5);
|
|
|
- text-align: left; // 文字左对齐
|
|
|
|
|
|
|
+ text-align: left;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 加载动画效果
|
|
|
|
|
.loading-dots {
|
|
.loading-dots {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
margin-left: rpx(5);
|
|
margin-left: rpx(5);
|
|
@@ -644,9 +564,7 @@ const download = (event, activeIndex) => {
|
|
|
padding: 30px 10px;
|
|
padding: 30px 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 风格下拉框
|
|
|
|
|
.style-dropdown{
|
|
.style-dropdown{
|
|
|
- // min-width: auto;
|
|
|
|
|
width: fit-content;
|
|
width: fit-content;
|
|
|
padding: rpx(6);
|
|
padding: rpx(6);
|
|
|
}
|
|
}
|
|
@@ -654,7 +572,7 @@ const download = (event, activeIndex) => {
|
|
|
font-size: rpx(8);
|
|
font-size: rpx(8);
|
|
|
color: black;
|
|
color: black;
|
|
|
border-radius: rpx(5);
|
|
border-radius: rpx(5);
|
|
|
- min-width: auto;
|
|
|
|
|
|
|
+ min-width: auto;
|
|
|
width: rpx(55);
|
|
width: rpx(55);
|
|
|
height: rpx(15);
|
|
height: rpx(15);
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -675,8 +593,7 @@ const download = (event, activeIndex) => {
|
|
|
#ffce1b
|
|
#ffce1b
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
|
.input-section {
|
|
.input-section {
|
|
@@ -687,7 +604,7 @@ const download = (event, activeIndex) => {
|
|
|
align-content: center;
|
|
align-content: center;
|
|
|
flex-wrap: nowrap;
|
|
flex-wrap: nowrap;
|
|
|
min-height: rpx(30);
|
|
min-height: rpx(30);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
> * {
|
|
> * {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -695,7 +612,7 @@ const download = (event, activeIndex) => {
|
|
|
height: rpx(20);
|
|
height: rpx(20);
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.speech-btn {
|
|
.speech-btn {
|
|
|
padding: rpx(5) rpx(10);
|
|
padding: rpx(5) rpx(10);
|
|
|
background: #fff;
|
|
background: #fff;
|
|
@@ -721,8 +638,7 @@ const download = (event, activeIndex) => {
|
|
|
transition: transform 0.3s ease;
|
|
transition: transform 0.3s ease;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 终止按钮样式
|
|
|
|
|
|
|
+
|
|
|
.stop-btn {
|
|
.stop-btn {
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
@@ -731,15 +647,13 @@ const download = (event, activeIndex) => {
|
|
|
height: rpx(20);
|
|
height: rpx(20);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 确保图片上传组件垂直居中
|
|
|
|
|
|
|
+
|
|
|
:deep(.el-upload) {
|
|
:deep(.el-upload) {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 确保语音输入组件垂直居中
|
|
|
|
|
|
|
+
|
|
|
:deep(.voice-input-container) {
|
|
:deep(.voice-input-container) {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -762,7 +676,7 @@ const download = (event, activeIndex) => {
|
|
|
to bottom,
|
|
to bottom,
|
|
|
#fee78a,
|
|
#fee78a,
|
|
|
#ffce1b
|
|
#ffce1b
|
|
|
- ); /* 设置悬停、聚焦、点击状态下的背景色 */
|
|
|
|
|
|
|
+ );
|
|
|
color: black;
|
|
color: black;
|
|
|
border: none;
|
|
border: none;
|
|
|
font-size: rpx(7);
|
|
font-size: rpx(7);
|
|
@@ -787,7 +701,6 @@ const download = (event, activeIndex) => {
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 风格下拉框
|
|
|
|
|
.custom-style-select {
|
|
.custom-style-select {
|
|
|
width: rpx(55);
|
|
width: rpx(55);
|
|
|
color: black;
|
|
color: black;
|