提交 d96644db authored 作者: 王鹏飞's avatar 王鹏飞

feat: 商品图文信息增加AI生成图片

上级 d6f7f647
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -19,6 +19,7 @@
"@dagrejs/dagre": "^1.1.3",
"@element-plus/icons-vue": "^2.3.1",
"@ezijing/ai-core": "^1.0.0",
"@ezijing/ai-vue": "^1.0.37",
"@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.1.2",
......
import { ref } from 'vue'
import { useImage } from '@ezijing/ai-vue'
export function useChatImage() {
const { generateImage, isLoading, error, cancel } = useImage({ provider: 'volcano' })
// 聊天相关状态
const messages = ref([])
// 聊天功能
const sendMessage = async (content, params = {}) => {
messages.value.push({ role: 'user', content, timestamp: new Date() })
// 所有聊天都生成图片
await handleImageGeneration({ prompt: content, ...params })
}
// 处理图片生成
const handleImageGeneration = async (params) => {
if (params.image && params.image.length === 0) delete params.image
try {
// 调用图片生成API
const result = await generateImage({
// model: 'doubao-seedream-4.0',
sequential_image_generation: 'auto',
watermark: true,
...params,
})
if (result && result.urls && result.urls.length > 0) {
// 添加AI回复
messages.value.push({
role: 'assistant',
content: `我已经为您生成了图片!共生成 ${result.urls.length} 张图片。`,
images: result.urls.map((url) => ({ url })),
timestamp: new Date(),
})
} else {
throw new Error('图片生成失败,请重试')
}
} catch (err) {
messages.value.push({
role: 'assistant',
content: `抱歉,图片生成失败了:${err.message}。请检查您的描述或稍后重试。`,
timestamp: new Date(),
})
}
}
return { messages, isLoading, error, sendMessage, generateImage: handleImageGeneration, cancel }
}
<script setup>
import { ref, nextTick } from 'vue'
import { Promotion, Close, Download, ZoomIn, Check } from '@element-plus/icons-vue'
import { saveAs } from 'file-saver'
import { ElMessage } from 'element-plus'
import { useChatImage } from '@/composables/useChatImage'
import { uploadFileByUrl } from '@/utils/upload'
const emit = defineEmits(['update', 'update:modelValue'])
const { sendMessage, messages, isLoading, cancel } = useChatImage()
const props = defineProps({
initialMessage: { type: String, default: '您好!我是AI助手,可以帮您处理各种图像生成任务。请告诉我您的需求!' },
quickPrompts: {
type: Array,
default: () => [
{ label: '赛博朋克风格', text: '生成一张赛博朋克风格的城市夜景' },
{ label: '水彩画风格', text: '生成一张水彩画风格的自然风景' },
{ label: '卡通风格', text: '生成一张可爱的卡通人物' },
{ label: '科幻概念', text: '生成一张科幻概念艺术' },
{ label: '超现实风格', text: '生成一张超现实主义风格的梦境' },
{ label: '油画风格', text: '生成一张古典油画风格的肖像' },
],
},
})
const chatMessages = ref(null)
const inputMessage = ref('')
const allMessages = computed(() => {
return [{ role: 'assistant', content: props.initialMessage }, ...messages.value]
})
// 图片预览功能
const previewImage = (imageUrl) => {
// 创建图片预览窗口
const previewWindow = window.open('', '_blank', 'width=1000,height=800,scrollbars=yes,resizable=yes')
previewWindow.document.write(`
<html>
<head>
<title>图片预览</title>
<style>
body { margin: 0; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
img { max-width: 100%; max-height: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
</style>
</head>
<body>
<img src="${imageUrl}" alt="Generated Image" />
</body>
</html>
`)
previewWindow.document.close()
}
// 图片下载功能
const downloadImage = async (imageUrl, imageIndex) => {
try {
// 获取图片文件名
const urlParts = imageUrl.split('/')
const fileName = urlParts[urlParts.length - 1] || `generated-image-${imageIndex + 1}.jpg`
// 获取图片数据
const response = await fetch(imageUrl)
const blob = await response.blob()
// 使用 file-saver 下载
saveAs(blob, fileName)
} catch (error) {
console.error('下载图片失败:', error)
ElMessage({ message: '下载图片失败', type: 'error' })
}
}
// 保存图片功能
const saveImage = async (imageUrl) => {
const url = await uploadFileByUrl(imageUrl)
emit('update', url)
emit('update:modelValue', false)
}
const scrollToBottom = () => {
nextTick(() => {
if (chatMessages.value) {
chatMessages.value.scrollTop = chatMessages.value.scrollHeight
}
})
}
// 获取提示词显示文本(标签)
const getPromptLabel = (prompt) => {
if (typeof prompt === 'string') return prompt
return prompt.label || prompt.text || ''
}
// 获取提示词完整文本(用于填充输入框)
const getPromptText = (prompt) => {
if (typeof prompt === 'string') return prompt
return prompt.text || prompt.label || ''
}
// 使用快捷提示词
const usePrompt = (text) => {
inputMessage.value = text
}
const handleSend = () => {
if (!inputMessage.value.trim() || props.isLoading) return
sendMessage(inputMessage.value.trim())
inputMessage.value = ''
scrollToBottom()
}
</script>
<template>
<el-dialog title="AI生成图片" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" width="860px">
<div class="chat-container">
<div class="chat-messages" ref="chatMessages">
<div v-for="(message, index) in allMessages" :key="index" class="chat-message" :class="message.role">
<div class="message-header">
<strong v-if="message.role === 'assistant'">AI助手:</strong>
<span v-else>您:</span>
<div class="message-content">
<div v-if="message.content" class="message-text">{{ message.content }}</div>
</div>
</div>
<div v-if="message.images && message.images.length > 0" class="message-images">
<div v-for="(image, imgIndex) in message.images" :key="imgIndex" class="image-item">
<div class="image-container">
<img :src="image.url" :alt="`Generated image ${imgIndex + 1}`" @click="previewImage(image.url)" />
<div class="image-actions">
<button class="action-btn download-btn" @click="downloadImage(image.url, imgIndex)" title="下载图片">
<el-icon><Download /></el-icon>
</button>
<button class="action-btn preview-btn" @click="previewImage(image.url)" title="预览图片">
<el-icon><ZoomIn /></el-icon>
</button>
<button class="action-btn save-btn" @click="saveImage(image.url)" title="保存图片">
<el-icon><Check /></el-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div v-if="isLoading" class="chat-message ai">
<div class="message-header">
<strong>AI助手:</strong>
<div class="message-content">
<div class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<!-- 快捷提示词 -->
<div v-if="props.quickPrompts.length > 0" class="quick-prompts">
<div
v-for="(prompt, index) in props.quickPrompts"
:key="index"
class="prompt-tag"
@click="usePrompt(getPromptText(prompt))">
{{ getPromptLabel(prompt) }}
</div>
</div>
<div class="chat-input-container">
<el-input
v-model="inputMessage"
type="text"
size="large"
placeholder="输入您的想法或问题..."
@keyup.enter="handleSend"
:disabled="isLoading"></el-input>
<el-button
type="primary"
size="large"
:icon="Promotion"
@click="handleSend"
:disabled="isLoading || !inputMessage.trim()"></el-button>
<el-button type="primary" size="large" :icon="Close" @click="cancel" v-if="isLoading"></el-button>
</div>
</div>
</el-dialog>
</template>
<style scoped>
.chat-container {
height: 600px;
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 16px;
}
.chat-message {
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 12px;
width: fit-content;
max-width: 80%;
font-size: 14px;
line-height: 1.5;
}
.chat-message.assistant {
background: linear-gradient(135deg, #eff6ff, #dbeafe);
color: #1e40af;
border: 1px solid #bfdbfe;
margin-right: auto;
}
.chat-message.user {
background: linear-gradient(135deg, #fce4ec, #f8bbd9);
color: #e91e63;
border: 1px solid #f48fb1;
margin-left: auto;
}
.chat-input-container {
display: flex;
gap: 12px;
}
.chat-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s ease;
}
.chat-input:focus {
border-color: #e91e63;
}
.chat-input:disabled {
background-color: #f9fafb;
cursor: not-allowed;
}
.send-btn {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #e91e63, #c2185b);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s ease;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(233, 30, 99, 0.3);
}
.send-btn:disabled {
background: #d1d5db;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loading-dots {
display: inline-flex;
gap: 4px;
align-items: center;
}
.loading-dots span {
width: 6px;
height: 6px;
background-color: #1e40af;
border-radius: 50%;
animation: loading 1.4s infinite ease-in-out both;
}
.loading-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* 消息头部样式 */
.message-header {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
}
.message-header strong,
.message-header span {
flex-shrink: 0;
}
/* 消息内容样式 */
.message-content {
flex: 1;
min-width: 0;
}
.message-text {
margin-bottom: 0;
word-wrap: break-word;
word-break: break-word;
}
.message-images {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
margin-left: 0;
}
.image-item {
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
background: white;
}
.image-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.image-container {
position: relative;
display: block;
}
.image-container img {
width: 200px;
height: auto;
object-fit: contain;
display: block;
cursor: pointer;
transition: transform 0.2s ease;
}
.image-container img:hover {
transform: scale(1.05);
}
.image-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 6px;
opacity: 0;
transition: opacity 0.2s ease;
}
.image-item:hover .image-actions {
opacity: 1;
}
.action-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
color: #374151;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s ease;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.action-btn:hover {
background: rgba(255, 255, 255, 1);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.download-btn:hover {
color: #059669;
}
.preview-btn:hover {
color: #2563eb;
}
.save-btn:hover {
color: #10b981;
}
/* 快捷提示词样式 */
.quick-prompts {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
padding: 0 2px;
}
.prompt-tag {
padding: 8px 14px;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border: 1px solid #e0e0e0;
border-radius: 16px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
line-height: 1;
}
.prompt-tag:hover {
border-color: #e91e63;
color: #e91e63;
background: linear-gradient(135deg, #fef5f8 0%, #ffe0ed 100%);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(233, 30, 99, 0.15);
}
.prompt-tag:active {
transform: translateY(0);
}
</style>
<script setup>
import { Plus, UploadFilled, Picture } from '@element-plus/icons-vue'
import { Plus, UploadFilled, Picture, MagicStick } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { upload } from '@/utils/upload'
import dayjs from 'dayjs'
const ImageDesign = defineAsyncComponent(() => import('./ImageDesign.vue'))
const ImageChat = defineAsyncComponent(() => import('./ImageChat.vue'))
const props = defineProps({
modelValue: { type: [Array, String], default: () => [] },
......@@ -63,12 +64,25 @@ const handleDesignSave = (url) => {
updatedValue[index.value] = result
emit('update:modelValue', updatedValue)
}
const chatVisible = ref(false)
const handleOpenChat = (i = 0) => {
index.value = i
chatVisible.value = true
}
const handleChatSave = (url) => {
const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
const result = { name: '未命名', size: '未知', type: 'image/png', url, upload_time: nowTime }
const updatedValue = [...props.modelValue]
updatedValue[index.value] = result
emit('update:modelValue', updatedValue)
}
</script>
<template>
<div class="upload-wrapper">
<template v-if="Array.isArray(props.modelValue)">
<div v-for="(item, i) in props.limit" :key="i">
<el-popover placement="top" width="160px" trigger="hover">
<el-popover placement="top" width="220px" trigger="hover">
<ul class="upload-popover">
<li @click="handleOpen(i)">
<i class="el-icon"><UploadFilled /></i>本地上传
......@@ -76,6 +90,9 @@ const handleDesignSave = (url) => {
<li @click="handleOpenDesign(i)" v-if="!isVideo">
<i class="el-icon"><Picture /></i>图库选择
</li>
<li @click="handleOpenChat(i)" v-if="!isVideo">
<i class="el-icon"><MagicStick /></i>AI辅助
</li>
</ul>
<template #reference>
<div class="upload-item">
......@@ -103,9 +120,12 @@ const handleDesignSave = (url) => {
<li @click="handleOpen">
<i class="el-icon"><UploadFilled /></i>本地上传
</li>
<li v-if="!isVideo">
<li @click="handleOpenDesign" v-if="!isVideo">
<i class="el-icon"><Picture /></i>图库选择
</li>
<li @click="handleOpenChat" v-if="!isVideo">
<i class="el-icon"><MagicStick /></i>AI辅助
</li>
</ul>
<template #reference>
<div class="upload-item">
......@@ -127,6 +147,7 @@ const handleDesignSave = (url) => {
</el-popover>
</template>
<ImageDesign v-model="designVisible" :kindId="29" @update="handleDesignSave" v-if="designVisible"></ImageDesign>
<ImageChat v-model="chatVisible" @update="handleChatSave" v-if="chatVisible"></ImageChat>
</div>
</template>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论