|
|
@ -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>
|
||||||
|
|
@ -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支持
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// 导出 ImageWrapper 组件
|
||||||
|
export { default as ImageWrapper } from './ImageWrapper.vue'
|
||||||
|
|
@ -7,8 +7,10 @@ export class AdminDisassemblyDetail {
|
||||||
async disassemble(imgurl,callback,errorCallback,config) {
|
async disassemble(imgurl,callback,errorCallback,config) {
|
||||||
try{
|
try{
|
||||||
const result = await gimiServer.handleGenerateImage(imgurl, prompt.Hairseparation,config)
|
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){
|
}catch(error){
|
||||||
errorCallback(error);
|
errorCallback(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click.stop="changePreviewImage"
|
@click.stop="changePreviewImage"
|
||||||
:disabled="currentStep > 1"
|
|
||||||
>
|
>
|
||||||
{{ $t('admin.disassemblyOrders.detail.changePreview') }}
|
{{ $t('admin.disassemblyOrders.detail.changePreview') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
@ -102,39 +101,22 @@
|
||||||
>
|
>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<h3>{{ $t('admin.disassemblyOrders.detail.step2') }}</h3>
|
<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 class="disassembled-images">
|
||||||
<div
|
<div
|
||||||
v-for="(image, index) in disassembledImages"
|
v-for="(item, index) in disassembledImages"
|
||||||
:key="index"
|
:key="item.id"
|
||||||
class="image-item"
|
class="image-item"
|
||||||
@click="previewImage(image)"
|
|
||||||
@mouseenter="showImageActions(index)"
|
|
||||||
@mouseleave="hideImageActions"
|
|
||||||
>
|
>
|
||||||
<div class="image-wrapper">
|
<ImageWrapper
|
||||||
<img :src="image" :alt="`拆件图 ${index + 1}`" />
|
:card-data="item"
|
||||||
<el-button
|
@preview="previewImage"
|
||||||
v-if="hoveredImageIndex === index"
|
@delete="deleteImage(index)"
|
||||||
class="delete-button"
|
@generate-model="generateModelFromImage"
|
||||||
type="danger"
|
@show-actions="showImageActions"
|
||||||
size="small"
|
@hide-actions="hideImageActions"
|
||||||
circle
|
@save="(result)=>saveImage(index,result)"
|
||||||
@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>
|
|
||||||
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div>
|
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -297,8 +279,9 @@ import {
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import ModelViewer from '@/components/common/ModelViewer.vue'
|
import ModelViewer from '@/components/common/ModelViewer.vue'
|
||||||
import ModelCom from '@/components/modelCom/index.vue'
|
import ModelCom from '@/components/modelCom/index.vue'
|
||||||
|
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
|
||||||
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
|
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
|
||||||
import { MeshyServer } from '@deotaland/utils';
|
import { MeshyServer,GiminiServer } from '@deotaland/utils';
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -317,6 +300,11 @@ const saveModel = (index,result,resultTask)=>{
|
||||||
generatedModels.value[index].taskId = result;
|
generatedModels.value[index].taskId = result;
|
||||||
generatedModels.value[index].resultTask = resultTask;
|
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)
|
const currentStep = ref(1)
|
||||||
|
|
@ -338,14 +326,6 @@ const hoveredImageIndex = ref(-1)
|
||||||
// 更换预览图相关
|
// 更换预览图相关
|
||||||
const showImageUpload = ref(false)
|
const showImageUpload = ref(false)
|
||||||
const uploadLoading = ref(false)
|
const uploadLoading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
// 模型加载状态管理
|
|
||||||
const modelLoadingStates = ref([])
|
|
||||||
const modelLoadingProgress = ref([])
|
|
||||||
const modelInitializationStates = ref([])
|
|
||||||
const modelInitializationProgress = ref([])
|
|
||||||
|
|
||||||
// 资源路径
|
// 资源路径
|
||||||
const thumbnailUrl = ref('');
|
const thumbnailUrl = ref('');
|
||||||
const modelUrl = ref('')
|
const modelUrl = ref('')
|
||||||
|
|
@ -467,21 +447,21 @@ const startDisassembly = () => {
|
||||||
}
|
}
|
||||||
// 执行拆件
|
// 执行拆件
|
||||||
const handleDisassembly = () => {
|
const handleDisassembly = () => {
|
||||||
disassemblyLoading.value = true
|
const newItem = {
|
||||||
Plug.disassemble(thumbnailUrl.value,(result) => {
|
id: new Date().getTime(),
|
||||||
// 更新拆件图片
|
thumbnailUrl: thumbnailUrl.value,
|
||||||
disassembledImages.value.push(result)
|
imageUrl: '',
|
||||||
if(currentStep.value==1){
|
taskID: '',
|
||||||
currentStep.value = 2
|
taskQueue: '',
|
||||||
}
|
project_id: orderDetail.value.projectId,
|
||||||
disassemblyLoading.value = false
|
};
|
||||||
},(error) => {
|
// disassembledImages.value = []
|
||||||
disassemblyLoading.value = false
|
// return
|
||||||
ElMessage.error('拆件失败,请稍后重试')
|
// 使用展开运算符创建新数组以确保响应式更新
|
||||||
},{
|
disassembledImages.value = [...disassembledImages.value, newItem];
|
||||||
project_id:orderDetail.value.projectId,
|
if(currentStep.value<=2){
|
||||||
role:'admin',
|
currentStep.value = 2;
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -526,6 +506,7 @@ const orderData = ref({});
|
||||||
// 组件挂载时获取订单信息
|
// 组件挂载时获取订单信息
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
MeshyServer.pollingEnabled = true;
|
MeshyServer.pollingEnabled = true;
|
||||||
|
GiminiServer.pollingEnabled = true;
|
||||||
orderId.value = route.params.id
|
orderId.value = route.params.id
|
||||||
const result = await adminOrders.getOrderDetail({
|
const result = await adminOrders.getOrderDetail({
|
||||||
id:orderId.value
|
id:orderId.value
|
||||||
|
|
@ -562,6 +543,7 @@ onMounted(async () => {
|
||||||
// 组件卸载时清理资源
|
// 组件卸载时清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
MeshyServer.pollingEnabled = false;
|
MeshyServer.pollingEnabled = false;
|
||||||
|
GiminiServer.pollingEnabled = false;
|
||||||
// 清理预览状态
|
// 清理预览状态
|
||||||
imagePreviewVisible.value = false
|
imagePreviewVisible.value = false
|
||||||
modelPreviewVisible.value = false
|
modelPreviewVisible.value = false
|
||||||
|
|
@ -569,25 +551,9 @@ onUnmounted(() => {
|
||||||
|
|
||||||
// 删除图片
|
// 删除图片
|
||||||
const deleteImage = (index) => {
|
const deleteImage = (index) => {
|
||||||
ElMessageBox.confirm(t('admin.disassemblyOrders.detail.messages.confirmDelete'), t('admin.disassemblyOrders.detail.messages.confirmTitle'), {
|
// disassembledImages.value.splice(index, 1)
|
||||||
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(() => {
|
|
||||||
// 用户取消删除
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成第二步内容(添加新图片)
|
|
||||||
const handleGenerateStep2 = () => {
|
|
||||||
generateStep2Loading.value = true
|
|
||||||
// 模拟API调用
|
|
||||||
disassembledImages.value.push(newImage)
|
|
||||||
generateStep2Loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示图片操作按钮
|
// 显示图片操作按钮
|
||||||
const showImageActions = (index) => {
|
const showImageActions = (index) => {
|
||||||
|
|
@ -600,15 +566,19 @@ const hideImageActions = () => {
|
||||||
}
|
}
|
||||||
// 从单张图片生成模型并进入第三步
|
// 从单张图片生成模型并进入第三步
|
||||||
const generateModelFromImage = async (image) => {
|
const generateModelFromImage = async (image) => {
|
||||||
generatedModels.value.push({
|
const newModel = {
|
||||||
id:new Date().getTime(),
|
id: new Date().getTime(),
|
||||||
image:image,
|
image: image,
|
||||||
taskId:''
|
taskId: ''
|
||||||
});
|
};
|
||||||
// 如果是第一次生成模型,进入第三步
|
|
||||||
if (currentStep.value < 3) {
|
// 使用展开运算符创建新数组以确保响应式更新
|
||||||
currentStep.value = 3
|
generatedModels.value = [...generatedModels.value, newModel];
|
||||||
}
|
|
||||||
|
// 如果是第一次生成模型,进入第三步
|
||||||
|
if (currentStep.value < 3) {
|
||||||
|
currentStep.value = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -1082,46 +1052,6 @@ generatedModels.value.push({
|
||||||
align-items: center;
|
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 {
|
.upload-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ export default defineConfig({
|
||||||
// 配置代理解决CORS问题
|
// 配置代理解决CORS问题
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'https://api.deotaland.ai',
|
// target: 'https://api.deotaland.ai',
|
||||||
|
target: 'http://192.168.0.174:9000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 379 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 444 KiB After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -74,17 +74,22 @@ import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plu
|
||||||
import { ElButton, ElIcon, ElInput } from 'element-plus'
|
import { ElButton, ElIcon, ElInput } from 'element-plus'
|
||||||
import ThemeToggle from '../ui/ThemeToggle.vue'
|
import ThemeToggle from '../ui/ThemeToggle.vue'
|
||||||
import LanguageToggle from '../ui/LanguageToggle.vue'
|
import LanguageToggle from '../ui/LanguageToggle.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['openGuideModal'])
|
const emit = defineEmits(['openGuideModal'])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
freeImageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
freeModelCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
projectName: {
|
projectName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'project'
|
default: 'project'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 生图免费次数(模拟数据)
|
|
||||||
const freeImageCount = ref(5)
|
|
||||||
// 模型免费次数(模拟数据)
|
|
||||||
const freeModelCount = ref(3)
|
|
||||||
// 编辑相关状态
|
// 编辑相关状态
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
const editedProjectName = ref('')
|
const editedProjectName = ref('')
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧控件区域 -->
|
<!-- 右侧控件区域 -->
|
||||||
<div class="right-controls-container">
|
<div class="right-controls-container" v-if="formData.internalImageUrl&&!(props.cardData.imgyt)">
|
||||||
<!-- 右侧圆形按钮控件 -->
|
<!-- 右侧圆形按钮控件 -->
|
||||||
<div class="right-circular-controls">
|
<div class="right-circular-controls">
|
||||||
<!-- v-if="props.generateFourView" -->
|
<!-- 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>
|
<el-icon class="btn-icon"><Cpu /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
<!-- v-if="props.generateSmoothWhiteModelStatus" -->
|
<!-- v-if="props.generateSmoothWhiteModelStatus" -->
|
||||||
|
|
@ -66,9 +66,13 @@
|
||||||
</button> -->
|
</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>
|
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
|
||||||
</button>
|
</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">
|
<!-- <button class="control-button share-btn" title="发型脱离" @click="handleHairDetach">
|
||||||
<el-icon class="btn-icon"><MagicStick /></el-icon>
|
<el-icon class="btn-icon"><MagicStick /></el-icon>
|
||||||
|
|
@ -99,12 +103,17 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import demoImage from '@/assets/demo.png'
|
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 { computed, ref, onMounted, watch, nextTick } from 'vue';
|
||||||
import { GiminiServer } from '@deotaland/utils';
|
import { GiminiServer } from '@deotaland/utils';
|
||||||
import humanTypeImg from '@/assets/sketches/tcww.png'
|
// import humanTypeImg from '@/assets/sketches/tcww.png'
|
||||||
import anTypeImg from '@/assets/sketches/dwww.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图标库和组件
|
// 引入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'
|
import { ElIcon,ElMessage } from 'element-plus'
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
internalImageUrl: '',//内部图片URL
|
internalImageUrl: '',//内部图片URL
|
||||||
|
|
@ -116,7 +125,7 @@ const showRightControls = ref(false);
|
||||||
// 控制鼠标悬停状态
|
// 控制鼠标悬停状态
|
||||||
const isHovered = ref(false);
|
const isHovered = ref(false);
|
||||||
// 图片比例
|
// 图片比例
|
||||||
const imageAspectRatio = ref(16 / 9); // 默认比例
|
const imageAspectRatio = ref(9/16); // 默认比例
|
||||||
// 控制文本输入框显示状态
|
// 控制文本输入框显示状态
|
||||||
const showTextInput = ref(false);
|
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 = () => {
|
const handleTextInputConfirm = () => {
|
||||||
// 触发创建新卡片事件,传递用户输入的文本内容
|
// 触发创建新卡片事件,传递用户输入的文本内容
|
||||||
|
|
@ -167,7 +183,6 @@ const handleTextInputCancel = () => {
|
||||||
showTextInput.value = false;
|
showTextInput.value = false;
|
||||||
textInputValue.value = '';
|
textInputValue.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 定义组件属性
|
// 定义组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 图片URL
|
// 图片URL
|
||||||
|
|
@ -175,7 +190,7 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
// 卡片宽度(默认200px)
|
// 卡片宽度(默认200px,横版图片自动调整为400px)
|
||||||
cardWidth: {
|
cardWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 200
|
default: 200
|
||||||
|
|
@ -190,11 +205,6 @@ const props = defineProps({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 8
|
default: 8
|
||||||
},
|
},
|
||||||
// 是否使用固定比例(9:16),默认为false,使用图片实际比例
|
|
||||||
useFixedRatio: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 新增:卡片数据对象,包含所有生图相关参数
|
// 新增:卡片数据对象,包含所有生图相关参数
|
||||||
cardData: {
|
cardData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -212,102 +222,136 @@ const emit = defineEmits(['generate-model-requested', 'create-new-card','create-
|
||||||
|
|
||||||
// 处理图片生成
|
// 处理图片生成
|
||||||
const handleGenerateImage = async () => {
|
const handleGenerateImage = async () => {
|
||||||
|
const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
|
||||||
try {
|
try {
|
||||||
let imageUrl;
|
|
||||||
// 使用多图参考生成
|
// 使用多图参考生成
|
||||||
const referenceImages = [];
|
let referenceImages = [];
|
||||||
// 如果有灵感图片,也添加到参考图片列表
|
// 如果有灵感图片,也添加到参考图片列表
|
||||||
if (props?.cardData?.inspirationImage) {
|
if (props?.cardData?.inspirationImage) {
|
||||||
referenceImages.push(props.cardData.inspirationImage);
|
referenceImages.push(props.cardData.inspirationImage);
|
||||||
}
|
}
|
||||||
// 如果有IP类型参考图,也添加到参考图片列表
|
|
||||||
// if (props?.cardData?.ipTypeImg) {
|
|
||||||
// }
|
|
||||||
if(props.cardData.diyPromptText){
|
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{
|
}else{
|
||||||
referenceImages.push(humanTypeImg);
|
referenceImages.push(humanTypeImg);
|
||||||
referenceImages.push(anTypeImg);
|
referenceImages.push(anTypeImg);
|
||||||
}
|
}
|
||||||
|
// referenceImages.push(cz2);
|
||||||
// referenceImages.push(humanTypeImg);
|
// referenceImages.push(humanTypeImg);
|
||||||
// if(props?.cardData?.selectedExpression){
|
// if(props?.cardData?.selectedExpression){
|
||||||
// referenceImages.push(props.cardData.selectedExpression.imageUrl);
|
// referenceImages.push(props.cardData.selectedExpression.imageUrl);
|
||||||
// }
|
// }
|
||||||
// 忽略第二张参考图
|
// 忽略第二张参考图
|
||||||
const prompt = props.cardData.diyPromptText|| `
|
let prompt = props.cardData.diyPromptText|| `
|
||||||
如果有三张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计和特征进行设计动作参考第三张参考图,角色不要是白膜,颜色要还原第一张参考图并且忽略一下写的所有规则,
|
首先保证生成的角色符合以下要求
|
||||||
如果有三张参考图并且第一张参考图是人的话,则忽略上一个规则,按照以下规则:
|
要求1:一个通体由单一纯色木材雕刻而成的角色,全身包括服装、皮肤、头发均为木质材质,衣服一定不要有其他颜色全部统一,无布料、无皮肤、无金属,表面光滑,颜色均匀一致,无纹理变化,整体呈现木质雕塑或木偶风格,极简设计,纯色(例如:暖棕色)。
|
||||||
设计动作一定要参考第二张参考图,让人物动作与第二张参考图中的人物动作完全一致。
|
如果至少有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考最后一张参考图,
|
||||||
|
角色肤色和衣服材质都为纯色一种颜色如下:
|
||||||
|
保证角色全身都为木头材质颜色,并且要带一些木头纹理,颜色为#bfa888
|
||||||
|
重点:保证角色所有的服饰衣服都为木头材质颜色,并且要带一些木头纹理,颜色为#bfa888
|
||||||
|
一个通体由单一纯色木材雕刻而成的角色,全身包括服装、皮肤、头发均为木质材质,无布料、无皮肤、无金属,表面光滑,颜色均匀一致,无纹理变化,整体呈现木质雕塑或木偶风格,极简设计,纯色(例如:暖棕色)
|
||||||
|
如果至少有两张参考图并且第一张参考图是人的话,则忽略上一个规则,动作参考倒数第二张参考图,按照以下规则:
|
||||||
|
设计动作参考人物参考图,让人物动作与参考图中的人物动作完全一致。
|
||||||
A full-body character portrait
|
A full-body character portrait
|
||||||
Ensure the output image has a portrait aspect ratio of 9:16.
|
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 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征),色彩搭配和谐且富有层次.
|
角色特征:Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征),色彩搭配和谐且富有层次.
|
||||||
|
Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
|
||||||
|
如果参考图是动物,使用疯狂动物城的动物风格设计,动物的特征要保留。
|
||||||
|
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
|
||||||
Note: The image should not have white borders.
|
Note: The image should not have white borders.
|
||||||
完整度:不要简化、修改或重新设计服装,需忠于原设计。
|
去除原图中复杂的背景,只保留人物角色的主体。
|
||||||
适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性。
|
适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性,手指头轮廓清晰,重点:保证角色全身包括衣服都为木头材质颜色,并且要带一些木头纹理,颜色为#bfa888。
|
||||||
排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
|
|
||||||
【3D打印结构优化】
|
【3D打印结构优化】
|
||||||
模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
||||||
不生成透明或复杂内构。
|
不生成透明或复杂内构。
|
||||||
保持厚度和连贯性,适合打印。
|
保持厚度和连贯性,适合打印。
|
||||||
|
服装请还原参考图本来的服装比例。
|
||||||
【材质处理】
|
【材质处理】
|
||||||
|
身材请还原参考图本来的身材比例。
|
||||||
整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
||||||
模型应呈现专业3D打印白模效果。
|
模型应呈现专业3D效果。
|
||||||
Adjust the character’s 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打印。
|
||||||
|
确保头发具备足够的厚度与结构完整性,避免在打印过程中出现脆弱断裂,同时保留原有的可爱美感。
|
||||||
|
头发纹理细节需针对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,
|
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.internalImageUrl = imageUrl;
|
||||||
formData.value.status = 'success';
|
formData.value.status = 'success';
|
||||||
saveProject();
|
saveProject();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ElMessage.error('生成图片失败,请稍后重试');
|
|
||||||
emit('delete');
|
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 = ()=>{
|
const init = ()=>{
|
||||||
let status = props.cardData.status;
|
if(props.cardData.imageUrl){
|
||||||
switch (status) {
|
|
||||||
case 'loading':
|
|
||||||
handleGenerateImage();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
formData.value.internalImageUrl = 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', {
|
emit('save-project', {
|
||||||
imageUrl:formData.value.internalImageUrl,
|
imageUrl:formData.value.internalImageUrl,
|
||||||
|
taskId:taskResult?.taskId||'',
|
||||||
|
taskQueue:taskResult?.taskQueue||'',
|
||||||
// status:formData.value.status,
|
// status:formData.value.status,
|
||||||
status:'success',
|
status:'success',
|
||||||
});
|
});
|
||||||
|
|
@ -331,13 +375,12 @@ const handleGenerateModel = async () => {
|
||||||
console.error('生成3D模型失败:', error);
|
console.error('生成3D模型失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 计算卡片样式,根据图片实际比例或固定比例调整
|
// 计算卡片样式,根据图片实际比例调整
|
||||||
const cardStyle = computed(() => {
|
const cardStyle = computed(() => {
|
||||||
const width = props.cardWidth;
|
// 如果图片宽度比例大于高度比例(横版图片),使用400px,否则使用props传入的宽度
|
||||||
// 根据useFixedRatio属性决定使用固定比例还是图片实际比例
|
const width = imageAspectRatio.value > 1 ? 400 : props.cardWidth;
|
||||||
const height = props.useFixedRatio
|
// 使用图片实际比例,调整高度
|
||||||
? (width * 16) / 9 // 固定9:16比例
|
const height = width / imageAspectRatio.value;
|
||||||
: width / imageAspectRatio.value; // 使用图片实际比例
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
|
|
@ -352,6 +395,7 @@ const handleImageError = (event) => {
|
||||||
};
|
};
|
||||||
// 处理图片加载完成,获取图片实际比例
|
// 处理图片加载完成,获取图片实际比例
|
||||||
const handleImageLoad = (event) => {
|
const handleImageLoad = (event) => {
|
||||||
|
console.log(event,'eventeventevent');
|
||||||
const img = event.target;
|
const img = event.target;
|
||||||
const width = img.naturalWidth;
|
const width = img.naturalWidth;
|
||||||
const height = img.naturalHeight;
|
const height = img.naturalHeight;
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,102 @@
|
||||||
export const tsc1 = `
|
// export const tsc1 = `
|
||||||
重点:后两张参考图分别是人形和动物形的参考图片。
|
// 重点:后两张参考图分别是人形和动物形的参考图片。
|
||||||
4.如果有三张参考图:根据第一张参考图判断是人还是动物,选择后两张对应参考图。
|
// 4.如果有三张参考图:根据第一张参考图判断是人还是动物,选择后两张对应参考图。
|
||||||
5.如果有二张参考图:根据提示词判断是人形还是动物形,选择后两张对应参考图。
|
// 5.如果有二张参考图:根据提示词判断是人形还是动物形,选择后两张对应参考图。
|
||||||
6.如果有三张参考图并且第一张是动物形状:参考zootopia的人物设计风格特征进行设计,并且忽略下面的所有规则。
|
// 6.如果有三张参考图并且第一张是动物形状:参考zootopia的人物设计风格特征进行设计,并且忽略下面的所有规则。
|
||||||
7.如果有三张参考图并且第一张是人形状:Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感,色彩搭配和谐且富有层次,
|
// 7.如果有三张参考图并且第一张是人形状:Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感,色彩搭配和谐且富有层次,
|
||||||
`
|
// `
|
||||||
export const demo = `
|
// export const demo = `
|
||||||
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
|
// ${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
|
||||||
如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计,忽略第二张参考图并且忽略一下所有规则,
|
// 如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计,忽略第二张参考图并且忽略一下所有规则,
|
||||||
,如果不是,这条规则就忽略。
|
// ,如果不是,这条规则就忽略。
|
||||||
1.A full-body character portrait
|
// 1.A full-body character portrait
|
||||||
2.Ensure the output image has a portrait aspect ratio of 9:16.
|
// 2.Ensure the output image has a portrait aspect ratio of 9:16.
|
||||||
3.Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
|
// 3.Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
|
||||||
完整度:不要简化、修改或重新设计服装,需忠于原设计,
|
// 完整度:不要简化、修改或重新设计服装,需忠于原设计,
|
||||||
适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性。
|
// 适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性。
|
||||||
排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
|
// 排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
|
||||||
8.【3D打印结构优化】
|
// 8.【3D打印结构优化】
|
||||||
模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
// 模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
||||||
不生成透明或复杂内构。
|
// 不生成透明或复杂内构。
|
||||||
保持厚度和连贯性,适合打印。
|
// 保持厚度和连贯性,适合打印。
|
||||||
【材质处理】
|
// 【材质处理】
|
||||||
整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
// 整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
||||||
模型应呈现专业3D打印白模效果。
|
// 模型应呈现专业3D打印白模效果。
|
||||||
Adjust the character’s 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.
|
// Adjust the character’s 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立体效果。
|
// 调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
|
||||||
图片不要有任何水印,
|
// 图片不要有任何水印,
|
||||||
保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
// 保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
||||||
Note: The image should not have white borders.
|
// Note: The image should not have white borders.
|
||||||
${props?.cardData?.selectedHairColor?
|
// ${props?.cardData?.selectedHairColor?
|
||||||
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
|
// `Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
|
||||||
${props?.cardData?.selectedSkinColor?
|
// ${props?.cardData?.selectedSkinColor?
|
||||||
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
|
// `Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
|
||||||
${props?.cardData?.sketch?
|
// ${props?.cardData?.sketch?
|
||||||
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
|
// `Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
|
||||||
${props?.cardData?.selectedMaterial?
|
// ${props?.cardData?.selectedMaterial?
|
||||||
`Replace the material/texture of the character with the reference image I provided.
|
// `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).
|
// 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.`:``}
|
// Ensure the new material seamlessly integrates with the character's design.`:``}
|
||||||
${props?.cardData?.selectedColor?
|
// ${props?.cardData?.selectedColor?
|
||||||
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
|
// `Material Color:${props?.cardData?.selectedColor?.hex}`:``}
|
||||||
${props?.cardData?.selectedExpression?
|
// ${props?.cardData?.selectedExpression?
|
||||||
`The facial expression is completely based on the picture I gave you`:``}
|
// `The facial expression is completely based on the picture I gave you`:``}
|
||||||
${true?`
|
// ${true?`
|
||||||
如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
|
// 如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
|
||||||
发型必须适合3d打印
|
// 发型必须适合3d打印
|
||||||
请严格按照参考图中的角色服装进行完整还原。
|
// 请严格按照参考图中的角色服装进行完整还原。
|
||||||
服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
|
// 服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
|
||||||
眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
|
// 眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
|
||||||
`:``}
|
// `:``}
|
||||||
|
|
||||||
`
|
// `
|
||||||
|
|
||||||
// 没有动物参考图情况
|
// 没有动物参考图情况
|
||||||
const demo2 = `
|
// const demo2 = `
|
||||||
如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考第二张参考图,并且忽略一下写的所有规则,
|
// 如果有两张参考图并且第一张参考图是动物,则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考第二张参考图,并且忽略一下写的所有规则,
|
||||||
如果有两张参考图并且第一张参考图是人的话,则忽略上一个规则,按照以下规则:
|
// 如果有两张参考图并且第一张参考图是人的话,则忽略上一个规则,按照以下规则:
|
||||||
设计动作一定要参考第二张参考图,让人物动作与第二张参考图中的人物动作完全一致。
|
// 设计动作一定要参考第二张参考图,让人物动作与第二张参考图中的人物动作完全一致。
|
||||||
A full-body character portrait
|
// A full-body character portrait
|
||||||
Ensure the output image has a portrait aspect ratio of 9:16.
|
// Ensure the output image has a portrait aspect ratio of 9:16.
|
||||||
Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
|
// Style:潮玩盲盒角色设计,采用 3D 立体建模渲染,呈现细腻的质感与精致的细节。
|
||||||
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
|
// ${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
|
||||||
${props?.cardData?.selectedHairColor?
|
// ${props?.cardData?.selectedHairColor?
|
||||||
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
|
// `Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
|
||||||
${props?.cardData?.selectedSkinColor?
|
// ${props?.cardData?.selectedSkinColor?
|
||||||
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
|
// `Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
|
||||||
${props?.cardData?.sketch?
|
// ${props?.cardData?.sketch?
|
||||||
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
|
// `Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
|
||||||
${props?.cardData?.selectedMaterial?
|
// ${props?.cardData?.selectedMaterial?
|
||||||
`Replace the material/texture of the character with the reference image I provided.
|
// `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).
|
// 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.`:``}
|
// Ensure the new material seamlessly integrates with the character's design.`:``}
|
||||||
${props?.cardData?.selectedColor?
|
// ${props?.cardData?.selectedColor?
|
||||||
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
|
// `Material Color:${props?.cardData?.selectedColor?.hex}`:``}
|
||||||
${props?.cardData?.selectedExpression?
|
// ${props?.cardData?.selectedExpression?
|
||||||
`The facial expression is completely based on the picture I gave you`:``}
|
// `The facial expression is completely based on the picture I gave you`:``}
|
||||||
${props?.cardData?.inspirationImage?`
|
// ${props?.cardData?.inspirationImage?`
|
||||||
如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
|
// 如果参考图是人形并且有发型(有的动物参考图可能没有发型可以忽略),发型需要与第一张参考图完全一致,严格按照参考图中的头发形态、方向、长度、体积进行完整还原.
|
||||||
发型必须适合3d打印
|
// 发型必须适合3d打印
|
||||||
请严格按照参考图中的角色服装进行完整还原。
|
// 请严格按照参考图中的角色服装进行完整还原。
|
||||||
服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
|
// 服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
|
||||||
眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
|
// 眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
|
||||||
`:``}
|
// `:``}
|
||||||
角色特征:Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征),色彩搭配和谐且富有层次.
|
// 角色特征:Q 版萌系造型,头身比例夸张(大头小身),神态纯真,服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征),色彩搭配和谐且富有层次.
|
||||||
Note: The image should not have white borders.
|
// Note: The image should not have white borders.
|
||||||
完整度:不要简化、修改或重新设计服装,需忠于原设计。
|
// 完整度:不要简化、修改或重新设计服装,需忠于原设计。
|
||||||
适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性。
|
// 适配3D打印:请保持服装边缘、装饰等细节略微加厚、避免过细结构,以提高打印稳定性。
|
||||||
排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
|
// 排除项:禁止添加额外装饰、图案、文字、徽章或修改风格。
|
||||||
【3D打印结构优化】
|
// 【3D打印结构优化】
|
||||||
模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
// 模型用于3D打印,必须保持结构厚实、稳定,无细小悬空部件或过薄结构。
|
||||||
不生成透明或复杂内构。
|
// 不生成透明或复杂内构。
|
||||||
保持厚度和连贯性,适合打印。
|
// 保持厚度和连贯性,适合打印。
|
||||||
【材质处理】
|
// 【材质处理】
|
||||||
整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
// 整体需光滑、稳固、边缘柔和,防止打印时断裂。
|
||||||
模型应呈现专业3D打印白模效果。
|
// 模型应呈现专业3D打印白模效果。
|
||||||
Adjust the character’s 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.
|
// Adjust the character’s 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立体效果。
|
// 调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
|
||||||
图片不要有任何水印,
|
// 图片不要有任何水印,
|
||||||
保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
// 保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
||||||
`
|
// `
|
||||||
|
//场景图
|
||||||
|
export const cjt = `将玩偶放在桌子玻璃正中间上,原图放在图中桌面上的相框里面[CJT_DEOTA]`
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<!-- 加载动画覆盖层 -->
|
<!-- 加载动画覆盖层 -->
|
||||||
<div v-if="isLoading" class="loading-overlay">
|
<div v-if="isLoading" class="loading-overlay">
|
||||||
<div class="loading-spinner"></div>
|
<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-progress">{{ Math.round(loadingProgress) }}%</div>
|
||||||
<div class="loading-bar">
|
<div class="loading-bar">
|
||||||
<div class="loading-progress-bar" :style="{ width: loadingProgress + '%' }"></div>
|
<div class="loading-progress-bar" :style="{ width: loadingProgress + '%' }"></div>
|
||||||
|
|
@ -310,7 +310,7 @@ const loadModel = (url) => {
|
||||||
(xhr) => {
|
(xhr) => {
|
||||||
const progress = (xhr.loaded / xhr.total) * 100;
|
const progress = (xhr.loaded / xhr.total) * 100;
|
||||||
loadingProgress.value = progress;
|
loadingProgress.value = progress;
|
||||||
console.log(progress + '% loaded');
|
// console.log(progress + '% loaded');
|
||||||
},
|
},
|
||||||
// 加载错误回调
|
// 加载错误回调
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ const formData = ref({
|
||||||
|
|
||||||
const textareaRef = ref(null);
|
const textareaRef = ref(null);
|
||||||
const fileInput = ref(null); // 文件输入引用
|
const fileInput = ref(null); // 文件输入引用
|
||||||
const generateCount = ref(4); // 生成数量,默认为1
|
const generateCount = ref(1); // 生成数量,默认为1
|
||||||
const isOptimizing = ref(false); // 优化状态
|
const isOptimizing = ref(false); // 优化状态
|
||||||
const isDragOver = ref(false); // 拖拽状态
|
const isDragOver = ref(false); // 拖拽状态
|
||||||
const isUploading = ref(false); // 图片上传状态
|
const isUploading = ref(false); // 图片上传状态
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
|
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
const ModernHome = () => import('../views/ModernHome.vue')
|
const ModernHome = () => import('../views/ModernHome/ModernHome.vue')
|
||||||
const List = () => import('../views/List.vue')
|
const List = () => import('../views/List.vue')
|
||||||
const Login = () => import('../views/Login/Login.vue')
|
const Login = () => import('../views/Login/Login.vue')
|
||||||
const Register = () => import('../views/Register.vue')
|
const Register = () => import('../views/Register.vue')
|
||||||
|
|
@ -131,8 +131,10 @@ const router = createRouter({
|
||||||
// 路由守卫
|
// 路由守卫
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
// window.localStorage.setItem('token','123')
|
if(window.location.hostname=='localhost'){
|
||||||
// return next()
|
window.localStorage.setItem('token','123')
|
||||||
|
return next()
|
||||||
|
}
|
||||||
if (to.meta.requiresAuth) {
|
if (to.meta.requiresAuth) {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
// 如果没有 token,跳转到登录页
|
// 如果没有 token,跳转到登录页
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 统计卡片区 -->
|
<!-- 统计卡片区 -->
|
||||||
<section class="stats-section">
|
<section class="stats-section">
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
|
|
@ -88,7 +87,7 @@
|
||||||
:style="{ '--delay': index * 0.1 + 's' }"
|
:style="{ '--delay': index * 0.1 + 's' }"
|
||||||
>
|
>
|
||||||
<div class="stat-icon" :style="{ '--icon-color': stat.color }">
|
<div class="stat-icon" :style="{ '--icon-color': stat.color }">
|
||||||
<component :is="stat.icon" />
|
<component :is="iconComponents[stat.icon]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
|
|
@ -98,7 +97,7 @@
|
||||||
<div class="stat-label">{{ stat.label }}</div>
|
<div class="stat-label">{{ stat.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-trend" :class="stat.trend">
|
<div class="stat-trend" :class="stat.trend">
|
||||||
<component :is="stat.trendIcon" />
|
<component :is="iconComponents[stat.trendIcon]" />
|
||||||
<span>{{ stat.trendValue }}</span>
|
<span>{{ stat.trendValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,14 +106,15 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
|
||||||
import CountUp from 'vue-countup-v3'
|
import CountUp from 'vue-countup-v3'
|
||||||
|
import { ModernHome } from './index.js'
|
||||||
|
import { dateUtils } from '@deotaland/utils';
|
||||||
|
const modernHome = new ModernHome();
|
||||||
// 图标组件
|
// 图标组件
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
|
|
@ -133,122 +133,108 @@ import {
|
||||||
Cpu
|
Cpu
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n()
|
||||||
name: 'ModernHome',
|
const router = useRouter()
|
||||||
components: {
|
const authStore = useAuthStore()
|
||||||
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 statsData = computed(() => [
|
const iconComponents = {
|
||||||
{
|
MagicStick,
|
||||||
id: 'creations',
|
Star,
|
||||||
value: 156,
|
Clock,
|
||||||
unit: '',
|
Document,
|
||||||
label: t('home.stats.creations'),
|
ArrowUp,
|
||||||
icon: 'MagicStick',
|
ArrowDown,
|
||||||
color: '#6B46C1',
|
Minus,
|
||||||
trend: 'up',
|
VideoPlay,
|
||||||
trendIcon: 'ArrowUp',
|
ChatDotRound,
|
||||||
trendValue: '12%'
|
Tickets,
|
||||||
},
|
Setting,
|
||||||
{
|
Picture,
|
||||||
id: 'credits',
|
Cpu
|
||||||
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 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -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, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
|
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
|
||||||
<!-- 顶部固定头部组件 -->
|
<!-- 顶部固定头部组件 -->
|
||||||
<div class="header-wrapper">
|
<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>
|
||||||
<!-- 导入的侧边栏组件 -->
|
<!-- 导入的侧边栏组件 -->
|
||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
|
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
|
||||||
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
|
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
|
||||||
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
|
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
|
||||||
|
@create-remaining-image-data="handleCreateRemainingImageData"
|
||||||
v-if="card.type === 'image'"
|
v-if="card.type === 'image'"
|
||||||
:cardData="card"
|
:cardData="card"
|
||||||
/>
|
/>
|
||||||
|
|
@ -124,9 +125,15 @@ import CharacterImportModal from '../../components/CharacterImportModal/index.vu
|
||||||
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
|
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
|
||||||
import GuideModal from '../../components/GuideModal/index.vue';
|
import GuideModal from '../../components/GuideModal/index.vue';
|
||||||
import {useRoute,useRouter} from 'vue-router';
|
import {useRoute,useRouter} from 'vue-router';
|
||||||
import {MeshyServer} from '@deotaland/utils';
|
import {MeshyServer,GiminiServer} from '@deotaland/utils';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import {Project} from './index';
|
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 router = useRouter();
|
||||||
const PluginProject = new Project();
|
const PluginProject = new Project();
|
||||||
// 弹窗相关状态
|
// 弹窗相关状态
|
||||||
|
|
@ -140,6 +147,12 @@ const cleanupFunctions = ref({});
|
||||||
const projectId = ref(null);
|
const projectId = ref(null);
|
||||||
//项目数据
|
//项目数据
|
||||||
const projectInfo = ref({});
|
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([
|
const cards = ref([
|
||||||
|
|
||||||
|
|
@ -156,6 +169,8 @@ const handleSaveProject = (index,item,type='image')=>{
|
||||||
switch(type){
|
switch(type){
|
||||||
case 'image':
|
case 'image':
|
||||||
cardItem.imageUrl = item.imageUrl;
|
cardItem.imageUrl = item.imageUrl;
|
||||||
|
cardItem.taskId = item.taskId;
|
||||||
|
cardItem.taskQueue = item.taskQueue;
|
||||||
break;
|
break;
|
||||||
case 'model':
|
case 'model':
|
||||||
cardItem.modelUrl = item.modelUrl;
|
cardItem.modelUrl = item.modelUrl;
|
||||||
|
|
@ -164,6 +179,7 @@ const handleSaveProject = (index,item,type='image')=>{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cardItem.status = item.status;
|
cardItem.status = item.status;
|
||||||
|
getGenerateCount();
|
||||||
console.log(cards.value,'保存项目');
|
console.log(cards.value,'保存项目');
|
||||||
}
|
}
|
||||||
const createProject = async ()=>{
|
const createProject = async ()=>{
|
||||||
|
|
@ -180,6 +196,7 @@ const createProject = async ()=>{
|
||||||
const getProjectInfo = async (id)=>{
|
const getProjectInfo = async (id)=>{
|
||||||
const data = await PluginProject.getProject(id);
|
const data = await PluginProject.getProject(id);
|
||||||
projectInfo.value = {...data};
|
projectInfo.value = {...data};
|
||||||
|
PluginProject.addProjectDuration(data.duration_seconds);
|
||||||
// 为没有id的卡片添加唯一id
|
// 为没有id的卡片添加唯一id
|
||||||
cards.value = [...projectInfo.value.details.node_card].map(card => ({
|
cards.value = [...projectInfo.value.details.node_card].map(card => ({
|
||||||
...card,
|
...card,
|
||||||
|
|
@ -188,6 +205,7 @@ const getProjectInfo = async (id)=>{
|
||||||
}
|
}
|
||||||
//更新项目信息
|
//更新项目信息
|
||||||
const updateProjectInfo = async (newProjectInfo)=>{
|
const updateProjectInfo = async (newProjectInfo)=>{
|
||||||
|
newProjectInfo.duration_seconds = PluginProject.duration_seconds;
|
||||||
PluginProject.updateProject(projectId.value,newProjectInfo);
|
PluginProject.updateProject(projectId.value,newProjectInfo);
|
||||||
}
|
}
|
||||||
// 处理模型卡片点击事件
|
// 处理模型卡片点击事件
|
||||||
|
|
@ -356,7 +374,6 @@ const createSmartCard = (cardConfig,index=null) => {
|
||||||
// 分配最高层级
|
// 分配最高层级
|
||||||
zIndex: cards.value.length+1,
|
zIndex: cards.value.length+1,
|
||||||
};
|
};
|
||||||
console.log(newCard);
|
|
||||||
return newCard;
|
return newCard;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -409,16 +426,20 @@ const handleCreateFourViewCard = (index,params) => {
|
||||||
}
|
}
|
||||||
// 处理提示词卡片生成
|
// 处理提示词卡片生成
|
||||||
const handleCreatePromptCard = (index,params) => {
|
const handleCreatePromptCard = (index,params) => {
|
||||||
const { img, diyPromptText } = params;
|
const { img, diyPromptText,imgyt,cardData } = params;
|
||||||
// 创建新的提示词卡片对象
|
// 创建新的提示词卡片对象
|
||||||
const newCard = createSmartCard({
|
const newCard = createSmartCard({
|
||||||
imageUrl: img,
|
diyPromptImg:img,
|
||||||
diyPromptText:diyPromptText,
|
diyPromptText:diyPromptText,
|
||||||
status:'loading',
|
status:'loading',
|
||||||
|
imgyt:imgyt||'',
|
||||||
type:'image',
|
type:'image',
|
||||||
|
inspirationImage:cardData.inspirationImage||'',
|
||||||
},index);
|
},index);
|
||||||
|
console.log(newCard,'newCardnewCard');
|
||||||
// 添加到卡片数组
|
// 添加到卡片数组
|
||||||
cards.value.push(newCard);
|
cards.value.push(newCard);
|
||||||
|
console.log(cards.value);
|
||||||
}
|
}
|
||||||
// 处理图片生成请求
|
// 处理图片生成请求
|
||||||
const handleGenerateRequested = async (params) => {
|
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) => {
|
const handleGenerateModelRequested = (index,params) => {
|
||||||
// 接收IPCard组件传递的参数,创建新的modelCard组件实例
|
// 接收IPCard组件传递的参数,创建新的modelCard组件实例
|
||||||
|
|
@ -461,7 +494,6 @@ const handleGenerateModelRequested = (index,params) => {
|
||||||
// 将新模型卡片添加到cards数组中
|
// 将新模型卡片添加到cards数组中
|
||||||
cards.value.push(newModelCard);
|
cards.value.push(newModelCard);
|
||||||
|
|
||||||
console.log('已创建新的modelCard实例:', newModelCard);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理模型生成事件
|
// 处理模型生成事件
|
||||||
|
|
@ -484,8 +516,6 @@ const handleModelGenerated = (modelData) => {
|
||||||
// 将新卡片添加到cards数组中
|
// 将新卡片添加到cards数组中
|
||||||
cards.value.push(newCard);
|
cards.value.push(newCard);
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
console.log('新3D模型已添加到场景中:', newCard);
|
|
||||||
};
|
};
|
||||||
// 场景和元素拖拽相关状态
|
// 场景和元素拖拽相关状态
|
||||||
const isElementDragging = ref(false);
|
const isElementDragging = ref(false);
|
||||||
|
|
@ -773,10 +803,12 @@ const init = ()=>{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getProjectInfo(projectId.value);
|
getProjectInfo(projectId.value);
|
||||||
|
getGenerateCount();
|
||||||
}
|
}
|
||||||
// 组件挂载时添加事件监听器
|
// 组件挂载时添加事件监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
MeshyServer.pollingEnabled = true;
|
MeshyServer.pollingEnabled = true;
|
||||||
|
GiminiServer.pollingEnabled = true;
|
||||||
// 每次进入都显示引导弹窗
|
// 每次进入都显示引导弹窗
|
||||||
// showGuideModal.value = true;
|
// showGuideModal.value = true;
|
||||||
init();
|
init();
|
||||||
|
|
@ -795,6 +827,8 @@ onMounted(() => {
|
||||||
// 组件卸载时移除事件监听器,避免内存泄漏
|
// 组件卸载时移除事件监听器,避免内存泄漏
|
||||||
onUnmounted(() => {// 禁用轮询
|
onUnmounted(() => {// 禁用轮询
|
||||||
MeshyServer.pollingEnabled = false;
|
MeshyServer.pollingEnabled = false;
|
||||||
|
GiminiServer.pollingEnabled = false;
|
||||||
|
clearInterval(Project.timer);
|
||||||
if (cleanupFunctions.value) {
|
if (cleanupFunctions.value) {
|
||||||
Object.values(cleanupFunctions.value).forEach(cleanup => {
|
Object.values(cleanupFunctions.value).forEach(cleanup => {
|
||||||
if (typeof cleanup === 'function') {
|
if (typeof cleanup === 'function') {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { clientApi, requestUtils } from '@deotaland/utils';
|
import { clientApi, requestUtils } from '@deotaland/utils';
|
||||||
export class Project{
|
export class Project{
|
||||||
|
static timer = null;
|
||||||
|
//项目时常
|
||||||
|
duration_seconds = 0;
|
||||||
constructor() {
|
constructor() {
|
||||||
// 用于防抖处理的定时器
|
// 用于防抖处理的定时器
|
||||||
this.updateTimer = null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<Bg>
|
<Bg>
|
||||||
<!-- <div :style="getResponsiveWidthStyle()">
|
<div :style="getResponsiveWidthStyle()">
|
||||||
<div style="position: sticky;top: 0;">
|
<div style="position: sticky;top: 0;">
|
||||||
<spline :scene="'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode'"/>
|
<spline :scene="'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode'"/>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
<div style="position: relative;" class="min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
|
<div style="position: relative;pointer-events:none;" class="min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<header
|
<header
|
||||||
:class="[
|
: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'
|
isScrolled ? 'bg-black/90 backdrop-blur-md py-4' : 'bg-transparent py-6'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
|
|
@ -115,11 +115,11 @@
|
||||||
</div>
|
</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="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="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) --- -->
|
<!-- --- 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="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) --- -->
|
<!-- --- CENTER HERO IMAGE (Always Visible) --- -->
|
||||||
<!-- :style="{ scale: scale*1.2 }" -->
|
<!-- :style="{ scale: scale*1.2 }" -->
|
||||||
|
|
@ -300,7 +300,7 @@
|
||||||
</section>
|
</section>
|
||||||
<MotionCom>
|
<MotionCom>
|
||||||
<!-- Companionship Section -->
|
<!-- 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">
|
<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">
|
<h2 class="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
|
||||||
|
|
@ -317,7 +317,7 @@
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://deotaland.com"
|
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') }}
|
{{ t('companionship.examples') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -444,7 +444,7 @@
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<!-- Footer -->
|
<!-- 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="container mx-auto px-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-start gap-12">
|
<div class="flex flex-col md:flex-row justify-between items-start gap-12">
|
||||||
|
|
||||||
|
|
@ -726,40 +726,10 @@ const navLinks = [
|
||||||
{ name: 'Land', href: '#' },
|
{ name: 'Land', href: '#' },
|
||||||
{ name: 'Pricing', 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
|
// Creation Canvas Images
|
||||||
const refImage = dog;
|
const refImage = dog;
|
||||||
const model3dImage = qdog;
|
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
|
// Robot Cards
|
||||||
const 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: 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' },
|
{ 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
|
// Strong Engine Logos
|
||||||
const logos = [
|
const logos = [
|
||||||
"AI Engine 1", "3D Engine 2", "Voice Engine 3", "Memory Engine 4",
|
"AI Engine 1", "3D Engine 2", "Voice Engine 3", "Memory Engine 4",
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ export default defineConfig({
|
||||||
// 配置代理解决CORS问题
|
// 配置代理解决CORS问题
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'https://api.deotaland.ai',
|
// target: 'https://api.deotaland.ai',
|
||||||
|
target: 'http://192.168.0.174:9000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const login = {
|
const login = {
|
||||||
GENERATE_IMAGE_ADMIN:{url:'/api-core/admin/gemini/generate-image',method:'POST'},// 生图模型
|
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;
|
export default login;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const login = {
|
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;
|
export default login;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import gemini from './gemini.js';
|
||||||
import project from './project.js';
|
import project from './project.js';
|
||||||
import pay from './pay.js';
|
import pay from './pay.js';
|
||||||
import order from './order.js';
|
import order from './order.js';
|
||||||
|
import user from './user.js';
|
||||||
export default {
|
export default {
|
||||||
...meshy,
|
...meshy,
|
||||||
...login,
|
...login,
|
||||||
|
|
@ -11,4 +12,5 @@ export default {
|
||||||
...project,
|
...project,
|
||||||
...pay,
|
...pay,
|
||||||
...order,
|
...order,
|
||||||
|
...user,
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -263,6 +263,10 @@ export class FileServer {
|
||||||
}
|
}
|
||||||
//上传文件
|
//上传文件
|
||||||
async uploadFile(url) {
|
async uploadFile(url) {
|
||||||
|
// 如果是网络路径直接返回
|
||||||
|
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
|
||||||
|
return Promise.resolve(url);
|
||||||
|
}
|
||||||
// 判断参数是否为File对象,如果是则先转换为base64
|
// 判断参数是否为File对象,如果是则先转换为base64
|
||||||
if (url instanceof File) {
|
if (url instanceof File) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -274,11 +278,7 @@ export class FileServer {
|
||||||
}
|
}
|
||||||
const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key
|
const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key
|
||||||
return new Promise(async (resolve, reject) => {
|
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'){
|
if(FileServer.fileCacheMap.has(cacheKey)&&FileServer.fileCacheMap.get(cacheKey)!='loading'){
|
||||||
// resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey)));
|
// resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey)));
|
||||||
resolve(FileServer.fileCacheMap.get(cacheKey));
|
resolve(FileServer.fileCacheMap.get(cacheKey));
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,11 @@ const getPorjectType = () => {
|
||||||
};
|
};
|
||||||
export class GiminiServer extends FileServer {
|
export class GiminiServer extends FileServer {
|
||||||
RULE = getPorjectType();
|
RULE = getPorjectType();
|
||||||
|
static pollingEnabled = true;
|
||||||
// 任务并发队列
|
// 任务并发队列
|
||||||
static taskQueue = new Map();
|
static taskQueue = new Map();
|
||||||
//最高并发限制
|
//最高并发限制
|
||||||
static MAX_CONCURRENT_TASKS = 4;
|
static MAX_CONCURRENT_TASKS =99;
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -153,8 +154,8 @@ export class GiminiServer extends FileServer {
|
||||||
})
|
})
|
||||||
return Promise.reject('Concurrent limit reached');
|
return Promise.reject('Concurrent limit reached');
|
||||||
}
|
}
|
||||||
const taskId = new Date().getTime();
|
const taskQueue = new Date().getTime();
|
||||||
GiminiServer.taskQueue.set(taskId, taskId);
|
GiminiServer.taskQueue.set(taskQueue, taskQueue);
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
// 标准化输入:确保 baseImages 是数组
|
// 标准化输入:确保 baseImages 是数组
|
||||||
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
|
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
|
||||||
|
|
@ -168,34 +169,52 @@ export class GiminiServer extends FileServer {
|
||||||
if (images.length > 5) {
|
if (images.length > 5) {
|
||||||
reject(`参考图片数量不能超过5张`);
|
reject(`参考图片数量不能超过5张`);
|
||||||
}
|
}
|
||||||
if (!prompt || !prompt.trim()) {
|
|
||||||
reject('请提供图片生成提示词');
|
|
||||||
}
|
|
||||||
// 处理多个参考图片
|
// 处理多个参考图片
|
||||||
const imageParts = await Promise.all(images.map(async image =>{
|
const imageParts = await Promise.all(images.map(async image =>{
|
||||||
return await this.dataUrlToGenerativePart(image,'url');
|
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 = {
|
const params = {
|
||||||
"aspect_ratio": "9:16",
|
// "parameters":{
|
||||||
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
|
// "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",
|
"location": "global",
|
||||||
"vertexai": true,
|
"vertexai": true,
|
||||||
...config,
|
...config,
|
||||||
inputs: [
|
inputs: [
|
||||||
...imageParts,
|
...imageParts,
|
||||||
{ text: prompt }
|
{ text: promptStr }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
|
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
|
||||||
const response = await requestUtils.common(requestUrl, params);
|
const response = await requestUtils.common(requestUrl, params);
|
||||||
if(response.data.error){
|
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}
|
||||||
reject(response.data.error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(response.code!=0){
|
if(response.code!=0){
|
||||||
reject(response.msg);
|
reject(response.msg);
|
||||||
return;
|
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 data = response.data;
|
||||||
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
|
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
|
||||||
let resultImg = data?.urls[0]?.url
|
let resultImg = data?.urls[0]?.url
|
||||||
|
|
@ -206,21 +225,50 @@ export class GiminiServer extends FileServer {
|
||||||
console.log(error, 'errorerrorerrorerrorerrorerror');
|
console.log(error, 'errorerrorerrorerrorerrorerror');
|
||||||
} finally {
|
} finally {
|
||||||
// 任务完成后从队列中移除
|
// 任务完成后从队列中移除
|
||||||
GiminiServer.taskQueue.delete(taskId);
|
GiminiServer.taskQueue.delete(taskQueue);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//模型生图功能
|
//根据任务id查询任务状态
|
||||||
async handleGenerateImage(referenceImages = [], prompt = '',config) {
|
async getTaskGinimi(taskId,taskQueue,successCallback,errorCallback){
|
||||||
return new Promise(async (resolve,reject) => {
|
if(!GiminiServer.taskQueue.has(taskQueue)){//如果队列不存在,创建一个
|
||||||
// let result = await this.generateImageFromMultipleImages(referenceImages, prompt);
|
GiminiServer.taskQueue.set(taskQueue, taskQueue);
|
||||||
try {
|
|
||||||
let result = await this.generateImageFromMultipleImagesOnline(referenceImages, prompt,config);
|
|
||||||
resolve(result);
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
}
|
||||||
})
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export class MeshyServer extends FileServer {
|
||||||
// 任务并发队列
|
// 任务并发队列
|
||||||
static taskQueue = new Map();
|
static taskQueue = new Map();
|
||||||
//最高并发限制
|
//最高并发限制
|
||||||
static MAX_CONCURRENT_TASKS = 1;
|
static MAX_CONCURRENT_TASKS = 3;
|
||||||
static pollingEnabled = true;
|
static pollingEnabled = true;
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -56,17 +56,11 @@ export class MeshyServer extends FileServer {
|
||||||
// let imgurl = 'https://api.deotaland.ai/upload/aabf8b4a8df447fa8c3e3f7978c523cc.png';
|
// let imgurl = 'https://api.deotaland.ai/upload/aabf8b4a8df447fa8c3e3f7978c523cc.png';
|
||||||
params.payload.image_url = imgurl;
|
params.payload.image_url = imgurl;
|
||||||
const requestUrl = this.RULE=='admin'?adminApi.default.IMAGE_TO_3DADMIN:clientApi.default.IMAGE_TO_3D;
|
const requestUrl = this.RULE=='admin'?adminApi.default.IMAGE_TO_3DADMIN:clientApi.default.IMAGE_TO_3D;
|
||||||
const response = await requestUtils.common(requestUrl, params);
|
// const response = await requestUtils.common(requestUrl, params);
|
||||||
// const response = {
|
const response = {"code":0,"message":"","success":true,"data":{"id":2215,"message":"任务已提交,正在处理"}}
|
||||||
// "code": 0,
|
console.log('创建模型任务响应:', response);
|
||||||
// "message": "",
|
|
||||||
// "success": true,
|
|
||||||
// "data": {
|
|
||||||
// "result": "019aed9d-6e7c-78ed-b621-e84df69bb59c"
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
if(response.code==0){
|
if(response.code==0){
|
||||||
callback&&callback(response?.data?.result,taskId);
|
callback&&callback(response?.data?.id,taskId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建模型任务失败:', 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,
|
method:this.RULE=='admin'?adminApi.default.FIND_TASK_IDADMIN.method:clientApi.default.FIND_TASK_ID.method,
|
||||||
};
|
};
|
||||||
let response = await requestUtils.common(requestUrl, {});
|
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){
|
if(response.code==0){
|
||||||
let data = response?.data
|
let data = response?.data?.result||{};
|
||||||
switch (data.status) {
|
let status = response?.data.status;
|
||||||
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
// let modelurl = data.model_url.replace("https://assets.meshy.ai", "https://api.deotaland.ai/model");
|
// 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);
|
resultTask&&MeshyServer.taskQueue.delete(resultTask);
|
||||||
callback&&callback(modelurl);
|
callback&&callback(modelurl);
|
||||||
break;
|
break;
|
||||||
|
|
@ -111,7 +120,7 @@ export class MeshyServer extends FileServer {
|
||||||
}
|
}
|
||||||
// 等待三秒
|
// 等待三秒
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
progressCallback&&progressCallback(data.progress);
|
progressCallback&&progressCallback(data.progress||0);
|
||||||
this.getModelTaskStatus({
|
this.getModelTaskStatus({
|
||||||
result:result,
|
result:result,
|
||||||
resultTask:resultTask,
|
resultTask:resultTask,
|
||||||
|
|
|
||||||
|
|
@ -119,3 +119,8 @@ export function getMonthRange(date) {
|
||||||
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0)
|
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0)
|
||||||
return { firstDay, lastDay }
|
return { firstDay, lastDay }
|
||||||
}
|
}
|
||||||
|
//秒数转换为分钟
|
||||||
|
export function secondsToTime(seconds) {
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
return `${minutes}`
|
||||||
|
}
|
||||||