This commit is contained in:
13121765685 2025-12-10 18:13:59 +08:00
parent e70102de13
commit 0594d473e1
37 changed files with 1095 additions and 525 deletions

View File

@ -0,0 +1,377 @@
<template>
<div
class="image-wrapper"
@click="handlePreview"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div v-if="imageUrl === ''">
<div class="loading-container">
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-dot"></div>
</div>
<p class="loading-text">正在拆件中...</p>
<div class="loading-progress">
<div class="progress-bar"></div>
</div>
</div>
</div>
<div v-else>
<img :src="imageUrl" :alt="altText" />
<!-- 删除按钮 -->
<el-button
v-if="showActions"
class="delete-button"
type="danger"
size="small"
circle
@click.stop="handleDelete"
>
<el-icon><Close /></el-icon>
</el-button>
<!-- 生成模型按钮 -->
<div v-if="showActions" class="generate-model-button-container">
<el-button
class="generate-model-button"
type="success"
size="small"
@click.stop="handleGenerateModel"
>
生成模型
</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { computed,onMounted,ref} from 'vue'
import { Close } from '@element-plus/icons-vue'
import { GiminiServer,prompt } from '@deotaland/utils';
const giminiServer = new GiminiServer()
const imageUrl = ref('');
// Props
const props = defineProps({
cardData: {
type: Object,
default: () => ({})
}
})
// Emits
const emit = defineEmits([
'preview',
'delete',
'generate-model',
'show-actions',
'hide-actions',
'save'
])
// Computed
const showActions = computed(() => {
return props.hoveredIndex === props.index
})
// Event handlers
const handlePreview = () => {
emit('preview', imageUrl.value)
}
const handleDelete = () => {
emit('delete', props.index)
}
const handleGenerateModel = () => {
emit('generate-model', imageUrl.value)
}
const handleMouseEnter = () => {
emit('show-actions', props.index)
}
const handleMouseLeave = () => {
emit('hide-actions')
}
//
const queryImageTask = async (taskId,taskQueue) => {
giminiServer.getTaskGinimi(
taskId,
taskQueue
,(imgurls)=>{
imageUrl.value = imgurls[0].url
saveProject({taskId,taskQueue});
},()=>{
emit('delete')
})
}
const saveProject = (result)=>{//
emit('save',{
imageUrl: imageUrl.value,
taskId: result.taskId,
taskQueue: result.taskQueue
})
}
//
const createImageTask = async () => {
try{
const result = await giminiServer.handleGenerateImage(props.cardData.thumbnailUrl, prompt.Hairseparation,{
project_id:props.cardData.project_id,
})
if(result){
saveProject(result);
queryImageTask(result.taskId,result.taskQueue);
}
}catch(err){
emit('delete')
}
}
onMounted(()=>{
if(props.cardData.imageUrl){
imageUrl.value = props.cardData.imageUrl
}else if(props?.cardData?.taskId&&props?.cardData?.taskQueue){
queryImageTask(props.cardData.taskId,props.cardData.taskQueue)
}else{
createImageTask();
}
})
</script>
<style scoped>
/* 图片包装器样式 */
.image-wrapper {
position: relative;
display: block;
width: 100%;
cursor: pointer;
transition: transform 0.2s ease;
}
.image-wrapper:hover {
transform: scale(1.02);
}
.image-wrapper img {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.image-wrapper:hover img {
border-color: #6B46C1;
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.1);
}
/* 删除按钮样式 */
.delete-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
opacity: 0.9;
transition: all 0.2s ease;
background-color: rgba(239, 68, 68, 0.9);
border-color: rgba(239, 68, 68, 0.9);
}
.delete-button:hover {
opacity: 1;
transform: scale(1.1);
background-color: rgba(239, 68, 68, 1);
border-color: rgba(239, 68, 68, 1);
}
/* 生成模型按钮容器 */
.generate-model-button-container {
position: absolute;
bottom: 8px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 10;
}
.generate-model-button {
opacity: 0.9;
transition: all 0.2s ease;
background-color: rgba(34, 197, 94, 0.9);
border-color: rgba(34, 197, 94, 0.9);
}
.generate-model-button:hover {
opacity: 1;
background-color: rgba(34, 197, 94, 1);
border-color: rgba(34, 197, 94, 1);
}
/* 加载状态样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 150px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 8px;
border: 1px solid #e2e8f0;
position: relative;
overflow: hidden;
}
.loading-container::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(167, 139, 250, 0.1), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.loading-spinner {
position: relative;
width: 48px;
height: 48px;
margin-bottom: 16px;
}
.spinner-ring {
width: 48px;
height: 48px;
border: 3px solid #e2e8f0;
border-top: 3px solid #6B46C1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.spinner-dot {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
background-color: #A78BFA;
border-radius: 50%;
transform: translate(-50%, -50%);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% {
opacity: 0.4;
transform: translate(-50%, -50%) scale(0.8);
}
50% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
}
.loading-text {
color: #6B46C1;
font-size: 14px;
font-weight: 500;
margin: 0 0 12px 0;
text-align: center;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
position: relative;
z-index: 1;
}
.loading-progress {
width: 120px;
height: 2px;
background-color: #e2e8f0;
border-radius: 1px;
overflow: hidden;
position: relative;
z-index: 1;
}
.progress-bar {
width: 40%;
height: 100%;
background: linear-gradient(90deg, #6B46C1, #A78BFA);
border-radius: 1px;
animation: progress 2s ease-in-out infinite;
}
@keyframes progress {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(0%);
}
100% {
transform: translateX(200%);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.image-wrapper img {
height: 120px;
}
.loading-container {
height: 120px;
}
.loading-spinner {
width: 40px;
height: 40px;
margin-bottom: 12px;
}
.spinner-ring {
width: 40px;
height: 40px;
border-width: 2px;
}
.spinner-dot {
width: 6px;
height: 6px;
}
.loading-text {
font-size: 13px;
margin-bottom: 10px;
}
.loading-progress {
width: 100px;
}
.delete-button {
top: 4px;
right: 4px;
}
.generate-model-button-container {
bottom: 4px;
}
}
</style>

View File

@ -0,0 +1,126 @@
# ImageWrapper 组件
## 组件概述
`ImageWrapper` 是一个用于展示拆件图片的可复用Vue组件具有悬停交互、删除和生成模型功能。
## 文件位置
```
src/components/disassembly/ImageWrapper.vue
```
## 功能特性
- ✅ 图片展示与预览
- ✅ 鼠标悬停显示操作按钮
- ✅ 删除图片功能
- ✅ 生成模型功能
- ✅ 响应式设计,适配移动端和桌面端
- ✅ 平滑动画效果
- ✅ Vue 3 Composition API 支持
## 组件参数
| 参数名 | 类型 | 必需 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `imageUrl` | String | ✅ | - | 图片地址 |
| `index` | Number | ✅ | - | 图片索引 |
| `hoveredIndex` | Number | ❌ | -1 | 当前悬停的图片索引 |
| `altText` | String | ❌ | '拆件图片' | 图片替代文本 |
## 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| `preview` | `imageUrl` | 点击图片预览时触发 |
| `delete` | `index` | 点击删除按钮时触发 |
| `generate-model` | `imageUrl` | 点击生成模型按钮时触发 |
| `show-actions` | `index` | 鼠标进入时触发 |
| `hide-actions` | - | 鼠标离开时触发 |
## 使用示例
```vue
<template>
<div class="disassembled-images">
<div
v-for="(image, index) in disassembledImages"
:key="index"
class="image-item"
>
<ImageWrapper
:image-url="image"
:index="index"
:hovered-index="hoveredImageIndex"
:alt-text="`拆件图 ${index + 1}`"
@preview="previewImage"
@delete="deleteImage"
@generate-model="generateModelFromImage"
@show-actions="showImageActions"
@hide-actions="hideImageActions"
/>
<div class="image-label">预览 {{ index + 1 }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ImageWrapper } from '@/components/disassembly'
// 数据
const disassembledImages = ref([
'https://example.com/image1.jpg',
'https://example.com/image2.jpg'
])
const hoveredImageIndex = ref(-1)
// 方法
const previewImage = (url) => {
console.log('预览图片:', url)
}
const deleteImage = (index) => {
disassembledImages.value.splice(index, 1)
}
const generateModelFromImage = (imageUrl) => {
console.log('生成模型:', imageUrl)
}
const showImageActions = (index) => {
hoveredImageIndex.value = index
}
const hideImageActions = () => {
hoveredImageIndex.value = -1
}
</script>
```
## 样式特性
- **响应式设计**:适配移动端(<768px)、平板768px-1024px和桌面端>1024px
- **主题色彩**:使用项目主色调(深紫色 #6B46C1
- **动画效果**平滑的过渡动画200ms缓入缓出
- **悬停效果**:鼠标悬停时图片缩放和阴影效果
- **按钮样式**圆角设计8px半径微妙阴影
## 导入方式
```javascript
// 方式1直接导入
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
// 方式2通过索引文件导入
import { ImageWrapper } from '@/components/disassembly'
```
## 更新日志
### v1.0.0 (2025-12-10)
- 初始版本发布
- 实现图片展示、悬停交互、删除和生成模型功能
- 支持响应式设计
- 完整的Vue 3 Composition API支持

View File

@ -0,0 +1,2 @@
// 导出 ImageWrapper 组件
export { default as ImageWrapper } from './ImageWrapper.vue'

View File

@ -7,8 +7,10 @@ export class AdminDisassemblyDetail {
async disassemble(imgurl,callback,errorCallback,config) {
try{
const result = await gimiServer.handleGenerateImage(imgurl, prompt.Hairseparation,config)
console.log('resultresult',result);
callback(result)
// 检查任务状态
gimiServer.getTaskGinimi(result.taskId,result.taskQueue,(imgurls)=>{
callback(imgurls[0].uri);
},errorCallback);
}catch(error){
errorCallback(error);
}

View File

@ -66,7 +66,6 @@
type="primary"
size="small"
@click.stop="changePreviewImage"
:disabled="currentStep > 1"
>
{{ $t('admin.disassemblyOrders.detail.changePreview') }}
</el-button>
@ -102,39 +101,22 @@
>
<div class="timeline-content">
<h3>{{ $t('admin.disassemblyOrders.detail.step2') }}</h3>
<div v-if="currentStep >= 2" class="step-content">
<div v-if="currentStep >= 2&&disassembledImages.length>0" class="step-content">
<div class="disassembled-images">
<div
v-for="(image, index) in disassembledImages"
:key="index"
v-for="(item, index) in disassembledImages"
:key="item.id"
class="image-item"
@click="previewImage(image)"
@mouseenter="showImageActions(index)"
@mouseleave="hideImageActions"
>
<div class="image-wrapper">
<img :src="image" :alt="`拆件图 ${index + 1}`" />
<el-button
v-if="hoveredImageIndex === index"
class="delete-button"
type="danger"
size="small"
circle
@click.stop="deleteImage(index)"
>
<el-icon><Close /></el-icon>
</el-button>
<div v-if="hoveredImageIndex === index" class="generate-model-button-container">
<el-button
class="generate-model-button"
type="success"
size="small"
@click.stop="generateModelFromImage(image)"
>
生成模型
</el-button>
</div>
</div>
<ImageWrapper
:card-data="item"
@preview="previewImage"
@delete="deleteImage(index)"
@generate-model="generateModelFromImage"
@show-actions="showImageActions"
@hide-actions="hideImageActions"
@save="(result)=>saveImage(index,result)"
/>
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div>
</div>
</div>
@ -297,8 +279,9 @@ import {
} from '@element-plus/icons-vue'
import ModelViewer from '@/components/common/ModelViewer.vue'
import ModelCom from '@/components/modelCom/index.vue'
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
import { MeshyServer } from '@deotaland/utils';
import { MeshyServer,GiminiServer } from '@deotaland/utils';
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
@ -317,6 +300,11 @@ const saveModel = (index,result,resultTask)=>{
generatedModels.value[index].taskId = result;
generatedModels.value[index].resultTask = resultTask;
}
const saveImage = (index,result)=>{
disassembledImages.value[index].taskId = result.taskId;
disassembledImages.value[index].taskQueue = result.taskQueue;
disassembledImages.value[index].imageUrl = result.imageUrl;
}
//
const currentStep = ref(1)
@ -338,14 +326,6 @@ const hoveredImageIndex = ref(-1)
//
const showImageUpload = ref(false)
const uploadLoading = ref(false)
//
const modelLoadingStates = ref([])
const modelLoadingProgress = ref([])
const modelInitializationStates = ref([])
const modelInitializationProgress = ref([])
//
const thumbnailUrl = ref('');
const modelUrl = ref('')
@ -467,21 +447,21 @@ const startDisassembly = () => {
}
//
const handleDisassembly = () => {
disassemblyLoading.value = true
Plug.disassemble(thumbnailUrl.value,(result) => {
//
disassembledImages.value.push(result)
if(currentStep.value==1){
currentStep.value = 2
}
disassemblyLoading.value = false
},(error) => {
disassemblyLoading.value = false
ElMessage.error('拆件失败,请稍后重试')
},{
project_id:orderDetail.value.projectId,
role:'admin',
})
const newItem = {
id: new Date().getTime(),
thumbnailUrl: thumbnailUrl.value,
imageUrl: '',
taskID: '',
taskQueue: '',
project_id: orderDetail.value.projectId,
};
// disassembledImages.value = []
// return
// 使
disassembledImages.value = [...disassembledImages.value, newItem];
if(currentStep.value<=2){
currentStep.value = 2;
}
}
@ -526,6 +506,7 @@ const orderData = ref({});
//
onMounted(async () => {
MeshyServer.pollingEnabled = true;
GiminiServer.pollingEnabled = true;
orderId.value = route.params.id
const result = await adminOrders.getOrderDetail({
id:orderId.value
@ -562,6 +543,7 @@ onMounted(async () => {
//
onUnmounted(() => {
MeshyServer.pollingEnabled = false;
GiminiServer.pollingEnabled = false;
//
imagePreviewVisible.value = false
modelPreviewVisible.value = false
@ -569,25 +551,9 @@ onUnmounted(() => {
//
const deleteImage = (index) => {
ElMessageBox.confirm(t('admin.disassemblyOrders.detail.messages.confirmDelete'), t('admin.disassemblyOrders.detail.messages.confirmTitle'), {
confirmButtonText: t('admin.disassemblyOrders.detail.messages.confirm'),
cancelButtonText: t('admin.disassemblyOrders.detail.messages.cancel'),
type: 'warning'
}).then(() => {
disassembledImages.value.splice(index, 1)
ElMessage.success(t('admin.disassemblyOrders.detail.messages.deleteSuccess'))
}).catch(() => {
//
})
// disassembledImages.value.splice(index, 1)
}
//
const handleGenerateStep2 = () => {
generateStep2Loading.value = true
// API
disassembledImages.value.push(newImage)
generateStep2Loading.value = false
}
//
const showImageActions = (index) => {
@ -600,15 +566,19 @@ const hideImageActions = () => {
}
//
const generateModelFromImage = async (image) => {
generatedModels.value.push({
id:new Date().getTime(),
image:image,
taskId:''
});
//
if (currentStep.value < 3) {
currentStep.value = 3
}
const newModel = {
id: new Date().getTime(),
image: image,
taskId: ''
};
// 使
generatedModels.value = [...generatedModels.value, newModel];
//
if (currentStep.value < 3) {
currentStep.value = 3
}
}
</script>
@ -1082,46 +1052,6 @@ generatedModels.value.push({
align-items: center;
}
/* 图片包装器和删除按钮样式 */
.image-wrapper {
position: relative;
display: block;
width: 100%;
}
.delete-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
opacity: 0.9;
transition: all 0.2s ease;
}
.delete-button:hover {
opacity: 1;
transform: scale(1.1);
}
.generate-model-button-container {
position: absolute;
bottom: 8px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 10;
}
.generate-model-button {
opacity: 0.9;
transition: all 0.2s ease;
}
.generate-model-button:hover {
opacity: 1;
}
/* 上传容器样式 */
.upload-container {
display: flex;

View File

@ -34,7 +34,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
// target: 'https://api.deotaland.ai',
target: 'http://192.168.0.174:9000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -74,17 +74,22 @@ import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plu
import { ElButton, ElIcon, ElInput } from 'element-plus'
import ThemeToggle from '../ui/ThemeToggle.vue'
import LanguageToggle from '../ui/LanguageToggle.vue'
const emit = defineEmits(['openGuideModal'])
const props = defineProps({
freeImageCount: {
type: Number,
default: 0
},
freeModelCount: {
type: Number,
default: 0
},
projectName: {
type: String,
default: 'project'
}
})
//
const freeImageCount = ref(5)
//
const freeModelCount = ref(3)
//
const isEditing = ref(false)
const editedProjectName = ref('')

View File

@ -49,11 +49,11 @@
</div>
</div>
<!-- 右侧控件区域 -->
<div class="right-controls-container">
<div class="right-controls-container" v-if="formData.internalImageUrl&&!(props.cardData.imgyt)">
<!-- 右侧圆形按钮控件 -->
<div class="right-circular-controls">
<!-- v-if="props.generateFourView" -->
<button class="control-button share-btn" title="生成3D模型" @click="handleGenerateModel">
<button class="control-button share-btn" title="3DMODEL" @click="handleGenerateModel">
<el-icon class="btn-icon"><Cpu /></el-icon>
</button>
<!-- v-if="props.generateSmoothWhiteModelStatus" -->
@ -66,9 +66,13 @@
</button> -->
<!-- 文本输入功能按钮 -->
<button class="control-button share-btn" title="文本输入" @click="toggleTextInput">
<button class="control-button share-btn" title="textInput" @click="toggleTextInput">
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
</button>
<button class="control-button share-btn" title="scene graph" @click="handleTextcjt">
<el-icon class="btn-icon"><Grid /></el-icon>
</button>
<!-- 发型脱离功能按钮 -->
<!-- <button class="control-button share-btn" title="发型脱离" @click="handleHairDetach">
<el-icon class="btn-icon"><MagicStick /></el-icon>
@ -99,12 +103,17 @@
<script setup>
import demoImage from '@/assets/demo.png'
import {cjt} from './tsc.js'
import cjimg from '@/assets/sketches/cjt.png';
import { computed, ref, onMounted, watch, nextTick } from 'vue';
import { GiminiServer } from '@deotaland/utils';
import humanTypeImg from '@/assets/sketches/tcww.png'
import anTypeImg from '@/assets/sketches/dwww.png';
// import humanTypeImg from '@/assets/sketches/tcww.png'
import humanTypeImg from '@/assets/sketches/tcww2.png'
// import anTypeImg from '@/assets/sketches/dwww.png';
import anTypeImg from '@/assets/sketches/dwww2.png';
import cz2 from '@/assets/material/cz2.png';
// Element Plus
import { Cpu, ChatDotRound, CloseBold } from '@element-plus/icons-vue'
import { Cpu, ChatDotRound, CloseBold,Grid } from '@element-plus/icons-vue'
import { ElIcon,ElMessage } from 'element-plus'
const formData = ref({
internalImageUrl: '',//URL
@ -116,7 +125,7 @@ const showRightControls = ref(false);
//
const isHovered = ref(false);
//
const imageAspectRatio = ref(16 / 9); //
const imageAspectRatio = ref(9/16); //
//
const showTextInput = ref(false);
//
@ -148,7 +157,14 @@ const toggleTextInput = () => {
});
}
};
const handleTextcjt = ()=>{
emit('create-prompt-card', {
img: formData.value.internalImageUrl,
imgyt:props?.cardData?.inspirationImage||'',
diyPromptText:cjt,
cardData: props.cardData
});
}
//
const handleTextInputConfirm = () => {
//
@ -167,7 +183,6 @@ const handleTextInputCancel = () => {
showTextInput.value = false;
textInputValue.value = '';
};
//
const props = defineProps({
// URL
@ -175,7 +190,7 @@ const props = defineProps({
type: String,
default: ''
},
// 200px
// 200px400px
cardWidth: {
type: Number,
default: 200
@ -190,11 +205,6 @@ const props = defineProps({
type: Number,
default: 8
},
// 使9:16false使
useFixedRatio: {
type: Boolean,
default: false
},
//
cardData: {
type: Object,
@ -212,102 +222,136 @@ const emit = defineEmits(['generate-model-requested', 'create-new-card','create-
//
const handleGenerateImage = async () => {
const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
try {
let imageUrl;
// 使
const referenceImages = [];
let referenceImages = [];
//
if (props?.cardData?.inspirationImage) {
referenceImages.push(props.cardData.inspirationImage);
}
// IP
// if (props?.cardData?.ipTypeImg) {
// }
if(props.cardData.diyPromptText){
referenceImages.push(props.cardData.imageUrl);
console.log(props.cardData.diyPromptImg,'diyPromptImgdiyPromptImgdiyPromptImg');
referenceImages.push(props.cardData.diyPromptImg);
if(iscjt){
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
referenceImages.push(cjimg);
}
}else{
referenceImages.push(humanTypeImg);
referenceImages.push(anTypeImg);
}
// referenceImages.push(cz2);
// referenceImages.push(humanTypeImg);
// if(props?.cardData?.selectedExpression){
// referenceImages.push(props.cardData.selectedExpression.imageUrl);
// }
//
const prompt = props.cardData.diyPromptText|| `
如果有三张参考图并且第一张参考图是动物则使用第一张图参考疯狂动物城的人物设计和特征进行设计动作参考第三张参考图角色不要是白膜颜色要还原第一张参考图并且忽略一下写的所有规则
如果有三张参考图并且第一张参考图是人的话则忽略上一个规则按照以下规则
设计动作一定要参考第二张参考图让人物动作与第二张参考图中的人物动作完全一致
let prompt = props.cardData.diyPromptText|| `
首先保证生成的角色符合以下要求
要求1一个通体由单一纯色木材雕刻而成的角色全身包括服装皮肤头发均为木质材质衣服一定不要有其他颜色全部统一无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计纯色例如暖棕色
如果至少有两张参考图并且第一张参考图是动物则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考最后一张参考图
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#bfa888
重点保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#bfa888
一个通体由单一纯色木材雕刻而成的角色全身包括服装皮肤头发均为木质材质无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计纯色例如暖棕色
如果至少有两张参考图并且第一张参考图是人的话则忽略上一个规则动作参考倒数第二张参考图按照以下规则
设计动作参考人物参考图让人物动作与参考图中的人物动作完全一致
A full-body character portrait
Ensure the output image has a portrait aspect ratio of 9:16.
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
${props?.cardData?.selectedHairColor?
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
${props?.cardData?.selectedSkinColor?
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
${props?.cardData?.sketch?
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
${props?.cardData?.selectedMaterial?
`Replace the material/texture of the character with the reference image I provided.
Intelligently determine which parts should be replaced (e.g., clothing, accessories) and which should remain unchanged (e.g., skin, eyes, nose).
Ensure the new material seamlessly integrates with the character's design.`:``}
${props?.cardData?.selectedColor?
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
${props?.cardData?.selectedExpression?
`The facial expression is completely based on the picture I gave you`:``}
${props?.cardData?.inspirationImage?`
如果参考图是人形并且有发型有的动物参考图可能没有发型可以忽略发型需要与第一张参考图完全一致严格按照参考图中的头发形态方向长度体积进行完整还原.
发型必须适合3d打印
请严格按照参考图中的角色服装进行完整还原
服装结构确保上衣下装外套鞋履配饰等所有服装元素与参考图一致保持准确的比例形状与层次
眼睛位置与第一张参考图一致材质表现逼真保留参考图中的细节比如还原参考图中的头发
`:``}
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征)色彩搭配和谐且富有层次.
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
如果参考图是动物使用疯狂动物城的动物风格设计动物的特征要保留
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
Note: The image should not have white borders.
完整度不要简化修改或重新设计服装需忠于原设计
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性
排除项禁止添加额外装饰图案文字徽章或修改风格
去除原图中复杂的背景只保留人物角色的主体
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性手指头轮廓清晰重点保证角色全身包括衣服都为木头材质颜色并且要带一些木头纹理颜色为#bfa888
3D打印结构优化
模型用于3D打印必须保持结构厚实稳定无细小悬空部件或过薄结构
不生成透明或复杂内构
保持厚度和连贯性适合打印
服装请还原参考图本来的服装比例
材质处理
身材请还原参考图本来的身材比例
整体需光滑稳固边缘柔和防止打印时断裂
模型应呈现专业3D打印白模效果
Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturingwith smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
模型应呈现专业3D效果
调整角色的发型使其厚实蓬松且结构坚固轮廓清晰扎实适合3D打印
确保头发具备足够的厚度与结构完整性避免在打印过程中出现脆弱断裂同时保留原有的可爱美感
头发纹理细节需针对3D制造进行优化层次平滑且分明兼顾视觉吸引力与可打印性维持整体俏皮且高品质的盲盒角色风格
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
图片不要有任何水印,
保证生成的任务图片一定要有眼睛一定要有嘴巴
保证生成的图片一定要有眼睛一定要有嘴巴
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#bfa888
衣服如果不适合做木制一定要简化衣服不能用复杂的衣服设计保留衣服特征即可衣服一定要纯色木质材质
如果参考图是动物保证动物双腿是向前伸展并分开的膝盖弯曲脚掌朝上或朝前它的双手前爪放在两腿之间靠近脚踝的位置整个身体是直立的面带微笑W坐姿W型坐姿盘腿坐V字坐
保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#bfa888
`
;
imageUrl = await giminiServer.handleGenerateImage(referenceImages, prompt,{
if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){
prompt = '按原图生成'
referenceImages = [props.cardData.inspirationImage];
}
const taskResult = await giminiServer.handleGenerateImage(referenceImages, prompt,{
project_id: props.cardData.project_id,
aspect_ratio:iscjt?'16:9':'9:16',
});
saveProject(taskResult);
getImageTask(taskResult.taskId,taskResult.taskQueue);
//
return
formData.value.internalImageUrl = imageUrl;
formData.value.status = 'success';
saveProject();
} catch (error) {
// ElMessage.error('');
emit('delete');
}
};
//
const getImageTask = async (taskId,taskQueue)=>{
giminiServer.getTaskGinimi(taskId,taskQueue,(imgurls)=>{
let imgItem = imgurls.splice(0,1)[0]
formData.value.internalImageUrl=imgItem.url;
createRemainingImageData(imgurls);
},()=>{
ElMessage.error('Failed to generate image, please try again later.');
emit('delete');
});
}
//
const createRemainingImageData = (imageUrl)=>{
if(imageUrl.length>0){
imageUrl.forEach((item)=>{
emit('create-remaining-image-data',{
...props.cardData,
imageUrl:item.url,
})
})
}
}
const init = ()=>{
let status = props.cardData.status;
switch (status) {
case 'loading':
handleGenerateImage();
break;
default:
if(props.cardData.imageUrl){
formData.value.internalImageUrl = props.cardData.imageUrl;
break;
}else if(props.cardData.taskId&&props.cardData.taskQueue){
getImageTask(props.cardData.taskId,props.cardData.taskQueue);
}else{
handleGenerateImage();
}
// let status = props.cardData.status;
// switch (status) {
// case 'loading':
// handleGenerateImage();
// break;
// default:
// formData.value.internalImageUrl = props.cardData.imageUrl;
// break;
// }
}
//
const saveProject = ()=>{
const saveProject = (taskResult)=>{
emit('save-project', {
imageUrl:formData.value.internalImageUrl,
taskId:taskResult?.taskId||'',
taskQueue:taskResult?.taskQueue||'',
// status:formData.value.status,
status:'success',
});
@ -331,13 +375,12 @@ const handleGenerateModel = async () => {
console.error('生成3D模型失败:', error);
}
};
//
//
const cardStyle = computed(() => {
const width = props.cardWidth;
// useFixedRatio使
const height = props.useFixedRatio
? (width * 16) / 9 // 9:16
: width / imageAspectRatio.value; // 使
// 使400px使props
const width = imageAspectRatio.value > 1 ? 400 : props.cardWidth;
// 使
const height = width / imageAspectRatio.value;
return {
width: `${width}px`,
@ -352,6 +395,7 @@ const handleImageError = (event) => {
};
//
const handleImageLoad = (event) => {
console.log(event,'eventeventevent');
const img = event.target;
const width = img.naturalWidth;
const height = img.naturalHeight;

View File

@ -1,100 +1,102 @@
export const tsc1 = `
重点后两张参考图分别是人形和动物形的参考图片
4.如果有三张参考图根据第一张参考图判断是人还是动物选择后两张对应参考图
5.如果有二张参考图根据提示词判断是人形还是动物形选择后两张对应参考图
6.如果有三张参考图并且第一张是动物形状参考zootopia的人物设计风格特征进行设计并且忽略下面的所有规则
7.如果有三张参考图并且第一张是人形状Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感色彩搭配和谐且富有层次
`
export const demo = `
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
如果有两张参考图并且第一张参考图是动物则使用第一张图参考疯狂动物城的人物设计风格特征进行设计忽略第二张参考图并且忽略一下所有规则
如果不是这条规则就忽略
1.A full-body character portrait
2.Ensure the output image has a portrait aspect ratio of 9:16.
3.Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
完整度不要简化修改或重新设计服装需忠于原设计
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性
排除项禁止添加额外装饰图案文字徽章或修改风格
8.3D打印结构优化
模型用于3D打印必须保持结构厚实稳定无细小悬空部件或过薄结构
不生成透明或复杂内构
保持厚度和连贯性适合打印
材质处理
整体需光滑稳固边缘柔和防止打印时断裂
模型应呈现专业3D打印白模效果
Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturingwith smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
图片不要有任何水印,
保证生成的任务图片一定要有眼睛一定要有嘴巴
Note: The image should not have white borders.
${props?.cardData?.selectedHairColor?
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
${props?.cardData?.selectedSkinColor?
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
${props?.cardData?.sketch?
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
${props?.cardData?.selectedMaterial?
`Replace the material/texture of the character with the reference image I provided.
Intelligently determine which parts should be replaced (e.g., clothing, accessories) and which should remain unchanged (e.g., skin, eyes, nose).
Ensure the new material seamlessly integrates with the character's design.`:``}
${props?.cardData?.selectedColor?
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
${props?.cardData?.selectedExpression?
`The facial expression is completely based on the picture I gave you`:``}
${true?`
如果参考图是人形并且有发型有的动物参考图可能没有发型可以忽略发型需要与第一张参考图完全一致严格按照参考图中的头发形态方向长度体积进行完整还原.
发型必须适合3d打印
请严格按照参考图中的角色服装进行完整还原
服装结构确保上衣下装外套鞋履配饰等所有服装元素与参考图一致保持准确的比例形状与层次
眼睛位置与第一张参考图一致材质表现逼真保留参考图中的细节比如还原参考图中的头发
`:``}
// export const tsc1 = `
// 重点:后两张参考图分别是人形和动物形的参考图片。
// 4.如果有三张参考图:根据第一张参考图判断是人还是动物,选择后两张对应参考图。
// 5.如果有二张参考图:根据提示词判断是人形还是动物形,选择后两张对应参考图。
// 6.如果有三张参考图并且第一张是动物形状参考zootopia的人物设计风格特征进行设计并且忽略下面的所有规则。
// 7.如果有三张参考图并且第一张是人形状Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感,色彩搭配和谐且富有层次,
// `
// export const demo = `
// ${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
// 如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计,忽略第二张参考图并且忽略一下所有规则,
// ,如果不是,这条规则就忽略。
// 1.A full-body character portrait
// 2.Ensure the output image has a portrait aspect ratio of 9:16.
// 3.Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
// 完整度:不要简化、修改或重新设计服装,需忠于原设计,
// 适配3D打印请保持服装边缘、装饰等细节略微加厚、避免过细结构以提高打印稳定性。
// 排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
// 8.【3D打印结构优化】
// 模型用于3D打印必须保持结构厚实、稳定无细小悬空部件或过薄结构。
// 不生成透明或复杂内构。
// 保持厚度和连贯性,适合打印。
// 【材质处理】
// 整体需光滑、稳固、边缘柔和,防止打印时断裂。
// 模型应呈现专业3D打印白模效果。
// Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturing—with smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
// 调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
// 图片不要有任何水印,
// 保证生成的任务图片一定要有眼睛,一定要有嘴巴
// Note: The image should not have white borders.
// ${props?.cardData?.selectedHairColor?
// `Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
// ${props?.cardData?.selectedSkinColor?
// `Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
// ${props?.cardData?.sketch?
// `Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
// ${props?.cardData?.selectedMaterial?
// `Replace the material/texture of the character with the reference image I provided.
// Intelligently determine which parts should be replaced (e.g., clothing, accessories) and which should remain unchanged (e.g., skin, eyes, nose).
// Ensure the new material seamlessly integrates with the character's design.`:``}
// ${props?.cardData?.selectedColor?
// `Material Color:${props?.cardData?.selectedColor?.hex}`:``}
// ${props?.cardData?.selectedExpression?
// `The facial expression is completely based on the picture I gave you`:``}
// ${true?`
// 如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
// 发型必须适合3d打印
// 请严格按照参考图中的角色服装进行完整还原。
// 服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
// 眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
// `:``}
`
// `
// 没有动物参考图情况
const demo2 = `
如果有两张参考图并且第一张参考图是动物则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考第二张参考图并且忽略一下写的所有规则
如果有两张参考图并且第一张参考图是人的话则忽略上一个规则按照以下规则
设计动作一定要参考第二张参考图让人物动作与第二张参考图中的人物动作完全一致
A full-body character portrait
Ensure the output image has a portrait aspect ratio of 9:16.
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
${props?.cardData?.selectedHairColor?
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
${props?.cardData?.selectedSkinColor?
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
${props?.cardData?.sketch?
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
${props?.cardData?.selectedMaterial?
`Replace the material/texture of the character with the reference image I provided.
Intelligently determine which parts should be replaced (e.g., clothing, accessories) and which should remain unchanged (e.g., skin, eyes, nose).
Ensure the new material seamlessly integrates with the character's design.`:``}
${props?.cardData?.selectedColor?
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
${props?.cardData?.selectedExpression?
`The facial expression is completely based on the picture I gave you`:``}
${props?.cardData?.inspirationImage?`
如果参考图是人形并且有发型有的动物参考图可能没有发型可以忽略发型需要与第一张参考图完全一致严格按照参考图中的头发形态方向长度体积进行完整还原.
发型必须适合3d打印
请严格按照参考图中的角色服装进行完整还原
服装结构确保上衣下装外套鞋履配饰等所有服装元素与参考图一致保持准确的比例形状与层次
眼睛位置与第一张参考图一致材质表现逼真保留参考图中的细节比如还原参考图中的头发
`:``}
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征)色彩搭配和谐且富有层次.
Note: The image should not have white borders.
完整度不要简化修改或重新设计服装需忠于原设计
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性
排除项禁止添加额外装饰图案文字徽章或修改风格
3D打印结构优化
模型用于3D打印必须保持结构厚实稳定无细小悬空部件或过薄结构
不生成透明或复杂内构
保持厚度和连贯性适合打印
材质处理
整体需光滑稳固边缘柔和防止打印时断裂
模型应呈现专业3D打印白模效果
Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturingwith smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
图片不要有任何水印,
保证生成的任务图片一定要有眼睛一定要有嘴巴
`
// const demo2 = `
// 如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考第二张参考图,并且忽略一下写的所有规则,
// 如果有两张参考图并且第一张参考图是人的话,则忽略上一个规则,按照以下规则:
// 设计动作一定要参考第二张参考图,让人物动作与第二张参考图中的人物动作完全一致。
// A full-body character portrait
// Ensure the output image has a portrait aspect ratio of 9:16.
// Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
// ${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
// ${props?.cardData?.selectedHairColor?
// `Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
// ${props?.cardData?.selectedSkinColor?
// `Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
// ${props?.cardData?.sketch?
// `Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
// ${props?.cardData?.selectedMaterial?
// `Replace the material/texture of the character with the reference image I provided.
// Intelligently determine which parts should be replaced (e.g., clothing, accessories) and which should remain unchanged (e.g., skin, eyes, nose).
// Ensure the new material seamlessly integrates with the character's design.`:``}
// ${props?.cardData?.selectedColor?
// `Material Color:${props?.cardData?.selectedColor?.hex}`:``}
// ${props?.cardData?.selectedExpression?
// `The facial expression is completely based on the picture I gave you`:``}
// ${props?.cardData?.inspirationImage?`
// 如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
// 发型必须适合3d打印
// 请严格按照参考图中的角色服装进行完整还原。
// 服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
// 眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
// `:``}
// 角色特征Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征),色彩搭配和谐且富有层次.
// Note: The image should not have white borders.
// 完整度:不要简化、修改或重新设计服装,需忠于原设计。
// 适配3D打印请保持服装边缘、装饰等细节略微加厚、避免过细结构以提高打印稳定性。
// 排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
// 【3D打印结构优化】
// 模型用于3D打印必须保持结构厚实、稳定无细小悬空部件或过薄结构。
// 不生成透明或复杂内构。
// 保持厚度和连贯性,适合打印。
// 【材质处理】
// 整体需光滑、稳固、边缘柔和,防止打印时断裂。
// 模型应呈现专业3D打印白模效果。
// Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturing—with smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
// 调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
// 图片不要有任何水印,
// 保证生成的任务图片一定要有眼睛,一定要有嘴巴
// `
//场景图
export const cjt = `将玩偶放在桌子玻璃正中间上,原图放在图中桌面上的相框里面[CJT_DEOTA]`

View File

@ -4,7 +4,7 @@
<!-- 加载动画覆盖层 -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">{{ t('modelCard.loadingModelText') }}</div>
<div class="loading-text">{{ 'loadingModel' }}</div>
<div class="loading-progress">{{ Math.round(loadingProgress) }}%</div>
<div class="loading-bar">
<div class="loading-progress-bar" :style="{ width: loadingProgress + '%' }"></div>
@ -310,7 +310,7 @@ const loadModel = (url) => {
(xhr) => {
const progress = (xhr.loaded / xhr.total) * 100;
loadingProgress.value = progress;
console.log(progress + '% loaded');
// console.log(progress + '% loaded');
},
//
(error) => {

View File

@ -296,7 +296,7 @@ const formData = ref({
const textareaRef = ref(null);
const fileInput = ref(null); //
const generateCount = ref(4); // 1
const generateCount = ref(1); // 1
const isOptimizing = ref(false); //
const isDragOver = ref(false); //
const isUploading = ref(false); //

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import NProgress from 'nprogress'
const ModernHome = () => import('../views/ModernHome.vue')
const ModernHome = () => import('../views/ModernHome/ModernHome.vue')
const List = () => import('../views/List.vue')
const Login = () => import('../views/Login/Login.vue')
const Register = () => import('../views/Register.vue')
@ -131,8 +131,10 @@ const router = createRouter({
// 路由守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
// window.localStorage.setItem('token','123')
// return next()
if(window.location.hostname=='localhost'){
window.localStorage.setItem('token','123')
return next()
}
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
// 如果没有 token跳转到登录页

View File

@ -77,7 +77,6 @@
</div>
</div>
</section>
<!-- 统计卡片区 -->
<section class="stats-section">
<div class="stats-container">
@ -88,7 +87,7 @@
:style="{ '--delay': index * 0.1 + 's' }"
>
<div class="stat-icon" :style="{ '--icon-color': stat.color }">
<component :is="stat.icon" />
<component :is="iconComponents[stat.icon]" />
</div>
<div class="stat-content">
<div class="stat-value">
@ -98,7 +97,7 @@
<div class="stat-label">{{ stat.label }}</div>
</div>
<div class="stat-trend" :class="stat.trend">
<component :is="stat.trendIcon" />
<component :is="iconComponents[stat.trendIcon]" />
<span>{{ stat.trendValue }}</span>
</div>
</div>
@ -107,14 +106,15 @@
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
<script setup>
import { computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import CountUp from 'vue-countup-v3'
import { ModernHome } from './index.js'
import { dateUtils } from '@deotaland/utils';
const modernHome = new ModernHome();
//
import {
User,
@ -133,122 +133,108 @@ import {
Cpu
} from '@element-plus/icons-vue'
export default {
name: 'ModernHome',
components: {
User,
Star,
Clock,
Document,
MagicStick,
ArrowUp,
ArrowDown,
Minus,
Cpu,
VideoPlay,
ChatDotRound,
Tickets,
Setting,
Picture,
CountUp
},
setup() {
const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
const themeStore = useThemeStore()
const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
// - 使
const statsData = computed(() => [
{
id: 'creations',
value: 156,
unit: '',
label: t('home.stats.creations'),
icon: 'MagicStick',
color: '#6B46C1',
trend: 'up',
trendIcon: 'ArrowUp',
trendValue: '12%'
},
{
id: 'credits',
value: 2840,
unit: '',
label: t('home.stats.credits'),
icon: 'Star',
color: '#F59E0B',
trend: 'up',
trendIcon: 'ArrowUp',
trendValue: '5%'
},
{
id: 'hours',
value: 42,
unit: 'h',
label: t('home.stats.hours'),
icon: 'Clock',
color: '#10B981',
trend: 'down',
trendIcon: 'ArrowDown',
trendValue: '8%'
},
{
id: 'projects',
value: 23,
unit: '',
label: t('home.stats.projects'),
icon: 'Document',
color: '#3B82F6',
trend: 'stable',
trendIcon: 'Minus',
trendValue: '0%'
}
])
//
const currentUser = computed(() => authStore.user)
const userName = computed(() => currentUser.value?.nickname || '')
const userAvatar = computed(() => currentUser.value?.avatarUrl || '')
const isLoggedIn = computed(() => authStore.token)
//
const navigateToFeature = (feature) => {
router.push(feature.path)
}
const formatTime = (timestamp) => {
const now = new Date()
const diff = now - timestamp
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 60) {
return t('home.recentActivity.minutesAgo', { count: minutes })
} else if (hours < 24) {
return t('home.recentActivity.hoursAgo', { count: hours })
} else {
return t('home.recentActivity.daysAgo', { count: days })
}
}
onMounted(() => {
//
setTimeout(() => {
// API
}, 500)
})
return {
t,
statsData,
userName,
userAvatar,
isLoggedIn,
navigateToFeature
}
}
//
const iconComponents = {
MagicStick,
Star,
Clock,
Document,
ArrowUp,
ArrowDown,
Minus,
VideoPlay,
ChatDotRound,
Tickets,
Setting,
Picture,
Cpu
}
// - 使
const statsData = computed(() => [
// {
// id: 'creations',
// value: homeData.value.agent_count,
// unit: '',
// label: t('home.stats.creations'),
// icon: 'MagicStick',
// color: '#6B46C1',
// trend: 'up',
// trendIcon: 'ArrowUp',
// trendValue: '12%'
// },
// {
// id: 'credits',
// value: 2840,
// unit: '',
// label: t('home.stats.credits'),
// icon: 'Star',
// color: '#F59E0B',
// trend: 'up',
// trendIcon: 'ArrowUp',
// trendValue: '5%'
// },
{
id: 'hours',
value: dateUtils.secondsToTime(homeData.value.total_duration_seconds),
unit: 'm',
label: t('home.stats.hours'),
icon: 'Clock',
color: '#10B981',
trend: 'down',
trendIcon: 'ArrowDown',
trendValue: '8%'
},
{
id: 'projects',
value: homeData.value.project_count,
unit: '',
label: t('home.stats.projects'),
icon: 'Document',
color: '#3B82F6',
trend: 'stable',
trendIcon: 'Minus',
trendValue: '0%'
}
])
//
const currentUser = computed(() => authStore.user)
const userName = computed(() => currentUser.value?.nickname || '')
const userAvatar = computed(() => currentUser.value?.avatarUrl || '')
const isLoggedIn = computed(() => authStore.token)
//
const navigateToFeature = (feature) => {
router.push(feature.path)
}
const homeData = ref({
total_duration_seconds: 0,
project_count: 0,
agent_count: 0,
});
const init = () => {
modernHome.getUserStatistics().then(res => {
if (res.code === 0) {
homeData.value = res.data;
}
});
// orderManagement.orderStatistics().then(res => {
// if (res.code === 0) {
// }
// });
}
onMounted(() => {
let token = window.localStorage.getItem('token');
token&&init();
})
</script>
<style scoped>

View File

@ -0,0 +1,13 @@
import {clientApi,requestUtils} from '@deotaland/utils';
export class ModernHome {
constructor() {
}
//获取用户首页统计数据
getUserStatistics(){
return requestUtils.common(clientApi.default.USER_STATISTICS, {});
}
//获取模型限制
getModelLimits(){
return requestUtils.common(clientApi.default.MODEL_LIMITS, {});
}
}

View File

@ -2,7 +2,7 @@
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
<HeaderComponent :freeImageCount="Limits.generateCount" :freeModelCount="Limits.modelCount" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
</div>
<!-- 导入的侧边栏组件 -->
<div class="sidebar-container">
@ -65,6 +65,7 @@
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
@create-remaining-image-data="handleCreateRemainingImageData"
v-if="card.type === 'image'"
:cardData="card"
/>
@ -124,9 +125,15 @@ import CharacterImportModal from '../../components/CharacterImportModal/index.vu
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
import GuideModal from '../../components/GuideModal/index.vue';
import {useRoute,useRouter} from 'vue-router';
import {MeshyServer} from '@deotaland/utils';
import {MeshyServer,GiminiServer} from '@deotaland/utils';
import { ElMessage } from 'element-plus';
import {Project} from './index';
import {ModernHome} from '../ModernHome/index.js'
const modernHome = new ModernHome();
const Limits = ref({
generateCount: 0,
modelCount: 0,
})
const router = useRouter();
const PluginProject = new Project();
//
@ -140,6 +147,12 @@ const cleanupFunctions = ref({});
const projectId = ref(null);
//
const projectInfo = ref({});
//
const getGenerateCount = async ()=>{
const {data} = await modernHome.getModelLimits();
Limits.value.generateCount = data[0].model_count;
Limits.value.modelCount = data[1].model_count;
}
//
const cards = ref([
@ -156,6 +169,8 @@ const handleSaveProject = (index,item,type='image')=>{
switch(type){
case 'image':
cardItem.imageUrl = item.imageUrl;
cardItem.taskId = item.taskId;
cardItem.taskQueue = item.taskQueue;
break;
case 'model':
cardItem.modelUrl = item.modelUrl;
@ -164,6 +179,7 @@ const handleSaveProject = (index,item,type='image')=>{
break;
}
cardItem.status = item.status;
getGenerateCount();
console.log(cards.value,'保存项目');
}
const createProject = async ()=>{
@ -180,6 +196,7 @@ const createProject = async ()=>{
const getProjectInfo = async (id)=>{
const data = await PluginProject.getProject(id);
projectInfo.value = {...data};
PluginProject.addProjectDuration(data.duration_seconds);
// idid
cards.value = [...projectInfo.value.details.node_card].map(card => ({
...card,
@ -188,6 +205,7 @@ const getProjectInfo = async (id)=>{
}
//
const updateProjectInfo = async (newProjectInfo)=>{
newProjectInfo.duration_seconds = PluginProject.duration_seconds;
PluginProject.updateProject(projectId.value,newProjectInfo);
}
//
@ -356,7 +374,6 @@ const createSmartCard = (cardConfig,index=null) => {
//
zIndex: cards.value.length+1,
};
console.log(newCard);
return newCard;
};
@ -409,16 +426,20 @@ const handleCreateFourViewCard = (index,params) => {
}
//
const handleCreatePromptCard = (index,params) => {
const { img, diyPromptText } = params;
const { img, diyPromptText,imgyt,cardData } = params;
//
const newCard = createSmartCard({
imageUrl: img,
diyPromptImg:img,
diyPromptText:diyPromptText,
status:'loading',
imgyt:imgyt||'',
type:'image',
inspirationImage:cardData.inspirationImage||'',
},index);
console.log(newCard,'newCardnewCard');
//
cards.value.push(newCard);
console.log(cards.value);
}
//
const handleGenerateRequested = async (params) => {
@ -442,6 +463,18 @@ const handleGenerateRequested = async (params) => {
}
};
const handleCreateRemainingImageData = (cardItem)=>{
const newCard = createSmartCard({
imageUrl: cardItem.imageUrl||'', // IPCard
prompt:cardItem.prompt||'',
inspirationImage: cardItem.inspirationImage||'',
status:'success',
type:'image',
});
//
cards.value.push(newCard);
}
//
const handleGenerateModelRequested = (index,params) => {
// IPCardmodelCard
@ -461,7 +494,6 @@ const handleGenerateModelRequested = (index,params) => {
// cards
cards.value.push(newModelCard);
console.log('已创建新的modelCard实例:', newModelCard);
};
//
@ -484,8 +516,6 @@ const handleModelGenerated = (modelData) => {
// cards
cards.value.push(newCard);
//
console.log('新3D模型已添加到场景中:', newCard);
};
//
const isElementDragging = ref(false);
@ -773,10 +803,12 @@ const init = ()=>{
return
}
getProjectInfo(projectId.value);
getGenerateCount();
}
//
onMounted(() => {
MeshyServer.pollingEnabled = true;
GiminiServer.pollingEnabled = true;
//
// showGuideModal.value = true;
init();
@ -795,6 +827,8 @@ onMounted(() => {
//
onUnmounted(() => {//
MeshyServer.pollingEnabled = false;
GiminiServer.pollingEnabled = false;
clearInterval(Project.timer);
if (cleanupFunctions.value) {
Object.values(cleanupFunctions.value).forEach(cleanup => {
if (typeof cleanup === 'function') {

View File

@ -1,5 +1,8 @@
import { clientApi, requestUtils } from '@deotaland/utils';
export class Project{
static timer = null;
//项目时常
duration_seconds = 0;
constructor() {
// 用于防抖处理的定时器
this.updateTimer = null;
@ -715,4 +718,14 @@ export class Project{
}
})
}
//叠加项目时常
addProjectDuration = (time) => {
this.duration_seconds = time;
if(Project.timer){
clearInterval(Project.timer);
}
Project.timer = setInterval(() => {
this.duration_seconds +=1;
}, 1000);
}
}

View File

@ -1,15 +1,15 @@
<template>
<Bg>
<!-- <div :style="getResponsiveWidthStyle()">
<div :style="getResponsiveWidthStyle()">
<div style="position: sticky;top: 0;">
<spline :scene="'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode'"/>
</div>
</div> -->
<div style="position: relative;" class="min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
</div>
<div style="position: relative;pointer-events:none;" class="min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
<!-- Navbar -->
<header
:class="[
'fixed top-0 left-0 right-0 z-900 transition-all duration-300',
'fixed top-0 left-0 right-0 z-900 transition-all duration-300 pointer-events-auto',
isScrolled ? 'bg-black/90 backdrop-blur-md py-4' : 'bg-transparent py-6'
]"
>
@ -115,11 +115,11 @@
</div>
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Retro Bot" /></div>
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center2" class="w-full h-full object-cover" alt="Toy Bot" /></div>
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center8" class="w-full h-full object-cover" alt="Cyberpunk" /></div>
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center7" class="w-full h-full object-cover" alt="Cyberpunk" /></div>
<!-- --- ROW 2 (Middle) --- -->
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center5" class="w-full h-full object-cover" alt="Interactive" /></div>
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Small Bot" /></div>
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center6" class="w-full h-full object-cover" alt="Small Bot" /></div>
<!-- --- CENTER HERO IMAGE (Always Visible) --- -->
<!-- :style="{ scale: scale*1.2 }" -->
@ -300,7 +300,7 @@
</section>
<MotionCom>
<!-- Companionship Section -->
<section class="py-24 border-t border-gray-900">
<section class="py-24 border-t border-gray-900 ">
<div class="container mx-auto px-6 flex flex-col items-center text-center">
<h2 class="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
@ -317,7 +317,7 @@
<a
href="https://deotaland.com"
class="inline-flex items-center justify-center px-8 py-4 text-base font-bold text-white bg-purple-600 rounded-full hover:scale-105 transition-transform duration-200"
class="pointer-events-auto inline-flex items-center justify-center px-8 py-4 text-base font-bold text-white bg-purple-600 rounded-full hover:scale-105 transition-transform duration-200"
>
{{ t('companionship.examples') }}
</a>
@ -444,7 +444,7 @@
</section>
</main>
<!-- Footer -->
<footer class=" text-white py-16 border-t border-gray-900">
<footer class="pointer-events-auto text-white py-16 border-t border-gray-900">
<div class="container mx-auto px-6">
<div class="flex flex-col md:flex-row justify-between items-start gap-12">
@ -726,40 +726,10 @@ const navLinks = [
{ name: 'Land', href: '#' },
{ name: 'Pricing', href: '#' },
];
// Theme: Desktop Robots, Emotional Companions, DIY Electronics
const heroImage = "https://images.unsplash.com/photo-1546776310-eef45dd6d63c?auto=format&fit=crop&q=80&w=1200";
// Grid images
const gridImages = [
// Row 1
"https://images.unsplash.com/photo-1535378437327-b7149b379c2e?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1581092160562-40aa08e78837?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1534723328310-e82dad3af43f?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1593085512500-bfd11932f80c?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1555680202-c86f0e12f086?auto=format&fit=crop&q=80&w=600",
// Row 2
"https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&q=80&w=600",
// Row 3 (Center Hero is here in grid)
"https://images.unsplash.com/photo-1601132359864-c974e7989094?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1580835239846-5bb9ce03c8c3?auto=format&fit=crop&q=80&w=600",
// Row 4
"https://images.unsplash.com/photo-1484557985045-edf25e08da73?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1517430816045-df4b7de8db98?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1563770095-39d468f9583d?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1580835239846-5bb9ce03c8c3?auto=format&fit=crop&q=80&w=600"
];
// Creation Canvas Images
const refImage = dog;
const model3dImage = qdog;
const realRobotImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/fd7ab938-d101-4ddd-9b01-f40a3180004c.jpg';
const realRobotImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/367b3ceb-5a3d-46c4-880b-d7028d3c93d4.jpg';
// Robot Cards
const cards = [
@ -767,15 +737,6 @@ const cards = [
{ id: 2, title: 'Custom Robot', user: '@Lil Moods', img: 'https://draft-user.s3.us-east-2.amazonaws.com/images/72109af5-7ff7-47a1-ba7d-5e18ab855479.png' },
{ id: 3, title: 'Custom Robot', user: '@Deo Monkey', img:'https://draft-user.s3.us-east-2.amazonaws.com/images/fd21fc66-8cae-417f-9e9a-617f18af9406.png' },
];
// Features List
const featureList = [
{ title: "Stunning Custom Looks", desc: "Generate unique robot designs from your ideas or images." },
{ title: "From Idea to 3D Model", desc: "Turn concepts into printable 3D shells ready for your robot core." },
{ title: "One-Click Expression Packs", desc: "Create and apply DIY eye styles and animated expressions." },
{ title: "Always-On AI Companion", desc: "Long-term memory, emotional responses, and daily interactions." }
];
// Strong Engine Logos
const logos = [
"AI Engine 1", "3D Engine 2", "Voice Engine 3", "Memory Engine 4",

View File

@ -66,7 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
// target: 'https://api.deotaland.ai',
target: 'http://192.168.0.174:9000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -1,4 +1,5 @@
const login = {
GENERATE_IMAGE_ADMIN:{url:'/api-core/admin/gemini/generate-image',method:'POST'},// 生图模型
GET_TASK_GINIMI_ADMIN:{url:'/api-core/admin/gemini/task/TASKID',method:'GET'},// 根据TASKID获取Gemini任务的详细信息
}
export default login;

View File

@ -1,4 +1,5 @@
const login = {
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST'},// 生图模型
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST'},// 生图模型任务创建
GET_TASK_GINIMI:{url:'/api-core/front/gemini/task/TASKID',method:'GET'},// 根据record_id获取Gemini任务的详细信息 由于Gemini是同步返回所以不需要task_id直接查询record即可
}
export default login;

View File

@ -4,6 +4,7 @@ import gemini from './gemini.js';
import project from './project.js';
import pay from './pay.js';
import order from './order.js';
import user from './user.js';
export default {
...meshy,
...login,
@ -11,4 +12,5 @@ export default {
...project,
...pay,
...order,
...user,
};

View File

@ -0,0 +1,5 @@
const login = {
MODEL_LIMITS:{url:'/api-core/front/user/model-limits',method:'GET'},// 模型限制
USER_STATISTICS:{url:'/api-core/front/user/statistics',method:'GET'},// 用户统计
}
export default login;

View File

@ -263,6 +263,10 @@ export class FileServer {
}
//上传文件
async uploadFile(url) {
// 如果是网络路径直接返回
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
return Promise.resolve(url);
}
// 判断参数是否为File对象如果是则先转换为base64
if (url instanceof File) {
try {
@ -274,11 +278,7 @@ export class FileServer {
}
const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key
return new Promise(async (resolve, reject) => {
// 如果是网络路径直接返回
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
resolve(url);
return;
}
if(FileServer.fileCacheMap.has(cacheKey)&&FileServer.fileCacheMap.get(cacheKey)!='loading'){
// resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey)));
resolve(FileServer.fileCacheMap.get(cacheKey));

View File

@ -19,10 +19,11 @@ const getPorjectType = () => {
};
export class GiminiServer extends FileServer {
RULE = getPorjectType();
static pollingEnabled = true;
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS = 4;
static MAX_CONCURRENT_TASKS =99;
constructor() {
super();
}
@ -153,8 +154,8 @@ export class GiminiServer extends FileServer {
})
return Promise.reject('Concurrent limit reached');
}
const taskId = new Date().getTime();
GiminiServer.taskQueue.set(taskId, taskId);
const taskQueue = new Date().getTime();
GiminiServer.taskQueue.set(taskQueue, taskQueue);
return new Promise(async (resolve, reject) => {
// 标准化输入:确保 baseImages 是数组
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
@ -168,34 +169,52 @@ export class GiminiServer extends FileServer {
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image,'url');
} ));
let promptStr = '';
if(config.aspect_ratio&&config.aspect_ratio=="9:16"){
promptStr = prompt.trim()+'必须返回4张图片不要放在一张图片里必须分开返回'
}else{
promptStr = prompt.trim();
}
const params = {
"aspect_ratio": "9:16",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
// "parameters":{
// "sampleCount": 4,
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-3-pro-image-preview",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: prompt }
{ text: promptStr }
]
}
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
if(response.data.error){
reject(response.data.error.message);
return;
}
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}
if(response.code!=0){
reject(response.msg);
return;
}
const taskResult = {
taskId:response.data.id,
taskQueue:taskQueue
}
resolve(taskResult);
return
//查询任务
return await this.getTaskGinimi(response.data.id,(imgurl)=>{
let resultImg = imgurl[0].url
resolve(resultImg);
},(err)=>{
reject(err);
});
let data = response.data;
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
let resultImg = data?.urls[0]?.url
@ -206,21 +225,50 @@ export class GiminiServer extends FileServer {
console.log(error, 'errorerrorerrorerrorerrorerror');
} finally {
// 任务完成后从队列中移除
GiminiServer.taskQueue.delete(taskId);
GiminiServer.taskQueue.delete(taskQueue);
}
})
}
//模型生图功能
async handleGenerateImage(referenceImages = [], prompt = '',config) {
return new Promise(async (resolve,reject) => {
// let result = await this.generateImageFromMultipleImages(referenceImages, prompt);
try {
let result = await this.generateImageFromMultipleImagesOnline(referenceImages, prompt,config);
resolve(result);
} catch (error) {
reject(error);
//根据任务id查询任务状态
async getTaskGinimi(taskId,taskQueue,successCallback,errorCallback){
if(!GiminiServer.taskQueue.has(taskQueue)){//如果队列不存在,创建一个
GiminiServer.taskQueue.set(taskQueue, taskQueue);
}
})
const tUrl = this.RULE=='admin'?adminApi.default.GET_TASK_GINIMI_ADMIN:clientApi.default.GET_TASK_GINIMI;
const requestUrl = {
url:tUrl.url.replace('TASKID',taskId).replace('TASKQUEUE',taskQueue),
method:tUrl.method
}
const response = await requestUtils.common(requestUrl);
if(response.code!=0){
errorCallback&&errorCallback('Failed to generate image');
return Promise.reject('Failed to generate image');
}
const status = response?.data?.status;
switch (status) {
case 1:
const result = response?.data?.result?.urls || [];
successCallback&&successCallback(result);
break;
case 2:
errorCallback&&errorCallback(response?.data?.response_data?.error?.type || 'Image generation failed');
break;
case 3:
Promise.reject('Image generation failed');
break;
default:
// 等待三秒
if(!GiminiServer.pollingEnabled){
return Promise.reject('Image generation failed');
}
await new Promise(resolve => setTimeout(resolve, 3000));
this.getTaskGinimi(taskId,taskQueue,successCallback,errorCallback);
break;
}
}
//模型生图功能
handleGenerateImage(referenceImages = [], prompt = '',config) {
return this.generateImageFromMultipleImagesOnline(referenceImages, prompt,config);
}
}

View File

@ -19,7 +19,7 @@ export class MeshyServer extends FileServer {
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS = 1;
static MAX_CONCURRENT_TASKS = 3;
static pollingEnabled = true;
constructor() {
super();
@ -56,17 +56,11 @@ export class MeshyServer extends FileServer {
// let imgurl = 'https://api.deotaland.ai/upload/aabf8b4a8df447fa8c3e3f7978c523cc.png';
params.payload.image_url = imgurl;
const requestUrl = this.RULE=='admin'?adminApi.default.IMAGE_TO_3DADMIN:clientApi.default.IMAGE_TO_3D;
const response = await requestUtils.common(requestUrl, params);
// const response = {
// "code": 0,
// "message": "",
// "success": true,
// "data": {
// "result": "019aed9d-6e7c-78ed-b621-e84df69bb59c"
// }
// };
// const response = await requestUtils.common(requestUrl, params);
const response = {"code":0,"message":"","success":true,"data":{"id":2215,"message":"任务已提交,正在处理"}}
console.log('创建模型任务响应:', response);
if(response.code==0){
callback&&callback(response?.data?.result,taskId);
callback&&callback(response?.data?.id,taskId);
}
} catch (error) {
console.error('创建模型任务失败:', error);
@ -88,12 +82,27 @@ export class MeshyServer extends FileServer {
method:this.RULE=='admin'?adminApi.default.FIND_TASK_IDADMIN.method:clientApi.default.FIND_TASK_ID.method,
};
let response = await requestUtils.common(requestUrl, {});
// let response = {
// "code": 0,
// "message": "",
// "success": true,
// "data": {
// "id": 2175,
// "status": 0,
// "result": null,
// "response_data": {},
// "provider": "meshy",
// "project_id": 57,
// "created_at": "2025-12-10T04:26:40.148194+00:00"
// }
// }
if(response.code==0){
let data = response?.data
switch (data.status) {
let data = response?.data?.result||{};
let status = response?.data.status;
switch (status) {
case 1:
// let modelurl = data.model_url.replace("https://assets.meshy.ai", "https://api.deotaland.ai/model");
let modelurl = data.result.s3_glb_url;
let modelurl = data.s3_glb_url;
resultTask&&MeshyServer.taskQueue.delete(resultTask);
callback&&callback(modelurl);
break;
@ -111,7 +120,7 @@ export class MeshyServer extends FileServer {
}
// 等待三秒
await new Promise(resolve => setTimeout(resolve, 3000));
progressCallback&&progressCallback(data.progress);
progressCallback&&progressCallback(data.progress||0);
this.getModelTaskStatus({
result:result,
resultTask:resultTask,

View File

@ -118,4 +118,9 @@ export function getMonthRange(date) {
const firstDay = new Date(d.getFullYear(), d.getMonth(), 1)
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0)
return { firstDay, lastDay }
}
//秒数转换为分钟
export function secondsToTime(seconds) {
const minutes = Math.floor(seconds / 60)
return `${minutes}`
}