222
This commit is contained in:
parent
c20f200df8
commit
555fbbe1af
|
|
@ -0,0 +1,4 @@
|
|||
# 开发环境配置
|
||||
VITE_BASE_URL=/api
|
||||
VITE_DEV_MODE=true
|
||||
VITE_LOG_LEVEL=info
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# 生产环境配置
|
||||
VITE_BASE_URL=https://api.deotaland.ai
|
||||
# Vercel 部署环境变量配置示例
|
||||
# 复制此文件为 .env.local 并填入实际值
|
||||
|
||||
# Google AI API Key(用于 AI 功能)
|
||||
VITE_GOOGLE_API_KEY=your_google_api_key_here
|
||||
|
||||
|
||||
# Stripe 支付配置
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=DeotalandAI
|
||||
VITE_APP_DESCRIPTION=AI-Powered Creation Platform
|
||||
|
||||
# 开发环境配置
|
||||
VITE_DEV_MODE=false
|
||||
VITE_LOG_LEVEL=error
|
||||
|
||||
# 生产环境配置(在 Vercel 中设置)
|
||||
# NODE_ENV=production
|
||||
# VERCEL=true
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 903 KiB |
|
|
@ -3,6 +3,15 @@
|
|||
<div v-if="loading" class="loading-overlay">
|
||||
<el-icon class="loading-icon" size="32"><Loading /></el-icon>
|
||||
<p>{{ loadingText }}</p>
|
||||
<div class="progress-container">
|
||||
<el-progress
|
||||
:percentage="loadingProgress"
|
||||
:stroke-width="6"
|
||||
:show-text="true"
|
||||
:color="'#6B46C1'"
|
||||
style="width: 200px; margin-top: 16px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-overlay">
|
||||
|
|
@ -48,18 +57,47 @@
|
|||
>
|
||||
<el-icon><Position /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="exportModel"
|
||||
:title="t('modelViewer.exportModel')"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- 导出控制组 -->
|
||||
<div v-if="showExport" class="control-group export-dropdown" :class="{ exporting: isExporting }">
|
||||
<el-dropdown @command="handleExportCommand" trigger="click" :disabled="isExporting">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:title="t('modelViewer.exportModel')"
|
||||
:loading="isExporting"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
<span style="margin-left: 4px;">{{ isExporting ? t('modelViewer.exportInProgress') : t('modelViewer.exportModel') }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="glb">
|
||||
<span class="format-dot" style="color: #67c23a;">●</span>
|
||||
{{ t('modelViewer.exportAsGLB') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="obj">
|
||||
<span class="format-dot" style="color: #409eff;">●</span>
|
||||
{{ t('modelViewer.exportAsOBJ') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="stl">
|
||||
<span class="format-dot" style="color: #e6a23c;">●</span>
|
||||
{{ t('modelViewer.exportAsSTL') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="fbx">
|
||||
<span class="format-dot" style="color: #f56c6c;">●</span>
|
||||
{{ t('modelViewer.exportAsFBX') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 模型信息 -->
|
||||
<div v-if="modelInfo" class="model-info">
|
||||
<div v-if="modelInfo&&showInfo" class="model-info">
|
||||
<p>{{ t('modelViewer.modelInfo') }}: {{ modelInfo }}</p>
|
||||
<p>{{ t('modelViewer.fileSize') }}: {{ formatFileSize(fileSize) }}</p>
|
||||
</div>
|
||||
|
|
@ -73,15 +111,27 @@ import { useI18n } from 'vue-i18n'
|
|||
import * as THREE from 'three'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { Loading, Warning, Refresh, Grid, Position, Download } from '@element-plus/icons-vue'
|
||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
||||
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
showExport:{
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
modelUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
default: ''
|
||||
},
|
||||
showInfo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
|
|
@ -117,6 +167,8 @@ const loading = ref(true)
|
|||
const error = ref('')
|
||||
const modelInfo = ref('')
|
||||
const fileSize = ref(0)
|
||||
const loadingProgress = ref(0)
|
||||
const isExporting = ref(false)
|
||||
|
||||
// Three.js 相关
|
||||
let scene, camera, renderer, controls
|
||||
|
|
@ -211,10 +263,12 @@ const createLaboratoryBackground = () => {
|
|||
|
||||
// 加载模型
|
||||
const loadModel = async (modelPath) => {
|
||||
console.log(modelPath,'modelPathmodelPath');
|
||||
if (!scene) return
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
loadingProgress.value = 0
|
||||
|
||||
try {
|
||||
// 清除现有模型
|
||||
|
|
@ -231,7 +285,13 @@ const loadModel = async (modelPath) => {
|
|||
modelPath,
|
||||
resolve,
|
||||
(progress) => {
|
||||
// 可以添加进度回调
|
||||
// 更新加载进度
|
||||
if (progress.total > 0) {
|
||||
loadingProgress.value = Math.round((progress.loaded / progress.total) * 100)
|
||||
} else {
|
||||
// 如果没有总大小,模拟进度
|
||||
loadingProgress.value = Math.min(90, loadingProgress.value + 10)
|
||||
}
|
||||
},
|
||||
reject
|
||||
)
|
||||
|
|
@ -277,6 +337,7 @@ const loadModel = async (modelPath) => {
|
|||
modelInfo.value = gltf.scene?.name || '3D Model'
|
||||
|
||||
loading.value = false
|
||||
loadingProgress.value = 100 // 确保进度显示100%
|
||||
|
||||
// 如果启用自动旋转
|
||||
if (props.autoRotate) {
|
||||
|
|
@ -286,6 +347,7 @@ const loadModel = async (modelPath) => {
|
|||
} catch (err) {
|
||||
console.error('Error loading model:', err)
|
||||
loading.value = false
|
||||
loadingProgress.value = 0
|
||||
error.value = t('modelViewer.loadError')
|
||||
}
|
||||
}
|
||||
|
|
@ -358,65 +420,227 @@ const centerModel = () => {
|
|||
controls.update()
|
||||
}
|
||||
|
||||
// 导出模型
|
||||
const exportModel = () => {
|
||||
if (!model || !scene) {
|
||||
console.warn('No model to export')
|
||||
// 处理导出命令
|
||||
const handleExportCommand = (command) => {
|
||||
if (isExporting.value) {
|
||||
ElMessage.warning(t('modelViewer.exportInProgress'))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建导出配置
|
||||
const exportOptions = {
|
||||
binary: false,
|
||||
embedImages: true,
|
||||
animations: true,
|
||||
forcePowerOfTwoTextures: false
|
||||
if (!scene || !model) {
|
||||
ElMessage.warning(t('modelViewer.noModelLoaded'))
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以实现导出逻辑
|
||||
// 由于浏览器环境限制,实际的GLTF导出功能需要额外的库支持
|
||||
// 当前实现为模拟导出功能
|
||||
switch (command) {
|
||||
case 'glb':
|
||||
exportAsGLB()
|
||||
break
|
||||
case 'obj':
|
||||
exportAsOBJ()
|
||||
break
|
||||
case 'stl':
|
||||
exportAsSTL()
|
||||
break
|
||||
case 'fbx':
|
||||
exportAsFBX()
|
||||
break
|
||||
default:
|
||||
ElMessage.error('不支持的导出格式')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为GLB格式
|
||||
const exportAsGLB = () => {
|
||||
isExporting.value = true
|
||||
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: t('modelViewer.exportInProgress'),
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
try {
|
||||
// 模拟导出过程
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: t('modelViewer.exportInProgress'),
|
||||
duration: 2000
|
||||
})
|
||||
const exporter = new GLTFExporter()
|
||||
|
||||
setTimeout(() => {
|
||||
// 创建下载链接(这里仅为演示,实际应该生成真正的文件)
|
||||
const dataStr = JSON.stringify({
|
||||
model: '3D Model Data',
|
||||
info: modelInfo.value,
|
||||
fileSize: formatFileSize(fileSize.value),
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(dataBlob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${modelInfo.value || 'model'}_export.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: t('modelViewer.exportSuccess')
|
||||
})
|
||||
}, 1500)
|
||||
exporter.parse(
|
||||
model,
|
||||
(result) => {
|
||||
const blob = new Blob([result], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${modelInfo.value || 'model'}.glb`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: t('modelViewer.exportSuccess')
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
console.error('GLB export failed:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: t('modelViewer.exportFailed')
|
||||
})
|
||||
},
|
||||
{ binary: true }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
console.error('GLB export error:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: t('modelViewer.exportFailed')
|
||||
})
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为OBJ格式
|
||||
const exportAsOBJ = () => {
|
||||
isExporting.value = true
|
||||
|
||||
try {
|
||||
const exporter = new OBJExporter()
|
||||
const result = exporter.parse(model)
|
||||
|
||||
const blob = new Blob([result], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${modelInfo.value || 'model'}.obj`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success(t('modelViewer.exportSuccess'))
|
||||
} catch (error) {
|
||||
console.error('OBJ export error:', error)
|
||||
ElMessage.error(t('modelViewer.exportFailed'))
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为STL格式
|
||||
const exportAsSTL = () => {
|
||||
if (!model || !scene) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: 'No model to export'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isExporting.value = true
|
||||
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: t('modelViewer.exportInProgress'),
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
try {
|
||||
const exporter = new STLExporter()
|
||||
const result = exporter.parse(model)
|
||||
|
||||
const blob = new Blob([result], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${modelInfo.value || 'model'}.stl`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: t('modelViewer.exportSuccess')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('STL export error:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: t('modelViewer.exportFailed')
|
||||
})
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出为FBX格式(转换为GLB并提供说明)
|
||||
const exportAsFBX = () => {
|
||||
if (!model || !scene) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: 'No model to export'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isExporting.value = true
|
||||
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: t('modelViewer.exportInProgress'),
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
try {
|
||||
const exporter = new GLTFExporter()
|
||||
|
||||
exporter.parse(
|
||||
model,
|
||||
(result) => {
|
||||
const blob = new Blob([result], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${modelInfo.value || 'model'}_for_fbx_conversion.glb`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: `${t('modelViewer.exportSuccess')} (GLB format for FBX conversion)`
|
||||
})
|
||||
|
||||
// 显示提示信息
|
||||
setTimeout(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: 'Note: Three.js does not directly support FBX export. The model has been exported as GLB format. You can use Blender or other tools to convert GLB to FBX format.',
|
||||
duration: 5000
|
||||
})
|
||||
}, 2000)
|
||||
},
|
||||
(error) => {
|
||||
console.error('FBX export failed:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: t('modelViewer.exportFailed')
|
||||
})
|
||||
},
|
||||
{ binary: true }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('FBX export error:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: t('modelViewer.exportFailed')
|
||||
})
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +693,7 @@ const handleTouchEnd = () => {
|
|||
|
||||
// 重试加载
|
||||
const retryLoad = () => {
|
||||
const modelToLoad = props.modelUrl || DEFAULT_MODEL
|
||||
const modelToLoad = props.modelUrl
|
||||
loadModel(modelToLoad)
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +728,8 @@ onMounted(async () => {
|
|||
initThreeJS()
|
||||
|
||||
// 加载模型
|
||||
const modelToLoad = props.modelUrl || DEFAULT_MODEL
|
||||
console.log(props.modelUrl);
|
||||
const modelToLoad = props.modelUrl
|
||||
loadModel(modelToLoad)
|
||||
|
||||
// 监听窗口大小变化
|
||||
|
|
@ -629,6 +854,11 @@ defineExpose({
|
|||
display: none;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.model-controls {
|
||||
|
|
@ -646,4 +876,88 @@ defineExpose({
|
|||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导出按钮样式优化 */
|
||||
.export-dropdown .el-button--primary {
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #553C9A 100%);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
.export-dropdown .el-button--primary:hover {
|
||||
background: linear-gradient(135deg, #553C9A 0%, #4C1D95 100%);
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
|
||||
.export-dropdown .el-button--primary:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
/* 导出菜单项样式 */
|
||||
.el-dropdown-menu__item {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: #f5f3ff;
|
||||
color: #6B46C1;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item .format-dot {
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: #2d1b69;
|
||||
color: #A78BFA;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导出过程动画 */
|
||||
@keyframes export-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 0 10px rgba(107, 70, 193, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(107, 70, 193, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.export-dropdown.exporting .el-button--primary {
|
||||
animation: export-pulse 1.5s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 格式按钮悬停效果 */
|
||||
.el-dropdown-menu__item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
<template>
|
||||
<div class="model-display-container">
|
||||
<!-- 初始化蒙层进度条 -->
|
||||
<div
|
||||
v-if="showInitializationOverlay"
|
||||
class="initialization-overlay"
|
||||
:class="{ 'fade-out': !showInitializationOverlay }"
|
||||
>
|
||||
<div class="overlay-content">
|
||||
<div class="initialization-spinner"></div>
|
||||
<div class="initialization-text">{{ $t('model.initializing') }}</div>
|
||||
<div class="progress-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: initializationProgress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ initializationProgress }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型展示区域 -->
|
||||
<div class="model-content" :class="{ 'blur': showInitializationOverlay }">
|
||||
<!-- 模型加载进度条 -->
|
||||
<!-- 模型视图 -->
|
||||
<div v-if="modelUrl" class="model-viewer-wrapper">
|
||||
<ModelViewer
|
||||
:model-url="modelUrl"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:show-export="false"
|
||||
:show-info="false"
|
||||
@load="onModelLoad"
|
||||
@error="onModelError"
|
||||
/>
|
||||
</div>
|
||||
<!-- 模型操作按钮 -->
|
||||
<div class="model-actions" v-if="!isLoading && !showInitializationOverlay">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
circle
|
||||
@click="emit('preview',modelUrl)"
|
||||
:title="$t('model.preview')"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showDelete"
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleDelete"
|
||||
:title="$t('model.delete')"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch,onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, Close } from '@element-plus/icons-vue'
|
||||
import ModelViewer from '@/components/common/ModelViewer.vue'
|
||||
import { MeshyServer } from '@deotaland/utils'
|
||||
const meshServer = new MeshyServer()
|
||||
const props = defineProps({
|
||||
imgUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '150px'
|
||||
},
|
||||
showDelete: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
// 模型URL
|
||||
const modelUrl = ref('')
|
||||
const emit = defineEmits(['preview', 'delete', 'load', 'error'])//preview事件用于预览模型
|
||||
const { t } = useI18n()
|
||||
// 状态管理
|
||||
// 是否显示初始化蒙层
|
||||
const showInitializationOverlay = ref(false)
|
||||
// 初始化进度
|
||||
const initializationProgress = ref(0)
|
||||
// 模型加载完成
|
||||
const onModelLoad = () => {
|
||||
isLoading.value = false
|
||||
loadingProgress.value = 100
|
||||
emit('load')
|
||||
}
|
||||
|
||||
// 模型加载错误
|
||||
const onModelError = (error) => {
|
||||
isLoading.value = false
|
||||
ElMessage.error(t('model.loadError'))
|
||||
emit('error', error)
|
||||
}
|
||||
// 删除模型
|
||||
const handleDelete = () => {
|
||||
emit('delete')
|
||||
}
|
||||
|
||||
// 对外暴露的方法,用于控制内部状态
|
||||
const setLoading = (loading, progress = 0) => {
|
||||
isLoading.value = loading
|
||||
loadingProgress.value = progress
|
||||
}
|
||||
|
||||
const setInitialization = (show, progress = 0) => {
|
||||
showInitializationOverlay.value = show
|
||||
initializationProgress.value = progress
|
||||
}
|
||||
|
||||
const setLoadingProgress = (progress) => {
|
||||
loadingProgress.value = progress
|
||||
}
|
||||
|
||||
const setInitializationProgress = (progress) => {
|
||||
initializationProgress.value = progress
|
||||
}
|
||||
|
||||
// 初始化完成时隐藏蒙层
|
||||
watch(() => initializationProgress.value, (newVal) => {
|
||||
if (newVal >= 100) {
|
||||
setTimeout(() => {
|
||||
showInitializationOverlay.value = false
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
const loadmodelUrl = ()=>{
|
||||
meshServer.createModelTask({
|
||||
image_url:props.imgUrl
|
||||
},(result)=>{
|
||||
showInitializationOverlay.value = true;
|
||||
meshServer.getModelTaskStatus(result,(modelurl)=>{
|
||||
setInitializationProgress(100)
|
||||
modelUrl.value = modelurl;
|
||||
},(error)=>{
|
||||
emit('delete')
|
||||
},(progress)=>{
|
||||
setInitializationProgress(progress)
|
||||
})
|
||||
},()=>{
|
||||
emit('delete')
|
||||
},{
|
||||
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
if(props.imgUrl){
|
||||
loadmodelUrl()
|
||||
}
|
||||
})
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
setLoading,
|
||||
setInitialization,
|
||||
setLoadingProgress,
|
||||
setInitializationProgress
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.model-display-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.model-display-container:hover {
|
||||
border-color: #6B46C1;
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.1);
|
||||
}
|
||||
|
||||
/* 初始化蒙层 */
|
||||
.initialization-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #9333EA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.initialization-overlay.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.initialization-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.initialization-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #f3f4f6;
|
||||
border-top: 3px solid #6B46C1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-container {
|
||||
width: 200px;
|
||||
height: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 12px auto;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #6B46C1 0%, #A78BFA 100%);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-bar.loading {
|
||||
background: linear-gradient(90deg, #6B46C1 0%, #A78BFA 100%);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.initialization-overlay .progress-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 模型内容 */
|
||||
.model-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.model-content.blur {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.model-viewer-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 模型操作按钮 */
|
||||
.model-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.model-actions .el-button {
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.model-actions .el-button:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.model-display-container {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.model-actions {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.model-actions .el-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.model-display-container {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色主题支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.model-display-container {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,7 +32,23 @@ export default {
|
|||
exportModel: 'Export Model',
|
||||
exportInProgress: 'Export in progress...',
|
||||
exportSuccess: 'Export successful',
|
||||
exportFailed: 'Export failed'
|
||||
exportFailed: 'Export failed',
|
||||
exportFormat: 'Export Format',
|
||||
exportAsGLB: 'Export as GLB',
|
||||
exportAsOBJ: 'Export as OBJ',
|
||||
exportAsSTL: 'Export as STL',
|
||||
exportAsFBX: 'Export as FBX',
|
||||
selectFormat: 'Select Export Format',
|
||||
preparingExport: 'Preparing export...'
|
||||
},
|
||||
|
||||
// Model Component
|
||||
model: {
|
||||
initializing: 'Initializing Model...',
|
||||
loading: 'Loading Model...',
|
||||
loadError: 'Failed to load model',
|
||||
preview: 'Preview Model',
|
||||
delete: 'Delete Model'
|
||||
},
|
||||
|
||||
// 应用
|
||||
|
|
@ -433,6 +449,8 @@ export default {
|
|||
previewDialog: 'Model Preview',
|
||||
close: 'Close',
|
||||
loading: 'Loading...',
|
||||
changePreview: 'Change Preview Image',
|
||||
uploadImage: 'Upload Image',
|
||||
previewDescription: 'This is a preview of the original 3D model. You can drag to rotate and scroll to zoom.',
|
||||
disassemblyDescription: 'Model disassembly in progress, please wait...',
|
||||
generateDescription: 'Generating disassembled model files...',
|
||||
|
|
@ -468,7 +486,9 @@ export default {
|
|||
shipSuccess: 'Shipping successful',
|
||||
disassembleConfirm: 'Are you sure to start disassembling this order?',
|
||||
disassembleTitle: 'Disassembly Confirmation',
|
||||
alreadyProcessing: 'This order is already being processed, please do not repeat the operation'
|
||||
alreadyProcessing: 'This order is already being processed, please do not repeat the operation',
|
||||
uploadSuccess: 'Image uploaded successfully',
|
||||
uploadFailed: 'Image upload failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,6 +367,8 @@ export default {
|
|||
previewDialog: '模型预览',
|
||||
close: '关闭',
|
||||
loading: '加载中...',
|
||||
changePreview: '更换预览图',
|
||||
uploadImage: '上传图片',
|
||||
previewDescription: '这是原始3D模型的预览,您可以拖动鼠标旋转、滚轮缩放查看模型细节。',
|
||||
disassemblyDescription: '模型拆件处理中,请稍候...',
|
||||
generateDescription: '正在生成拆件后的模型文件...',
|
||||
|
|
@ -402,7 +404,9 @@ export default {
|
|||
shipSuccess: '发货成功',
|
||||
disassembleConfirm: '确定要开始拆件此订单吗?',
|
||||
disassembleTitle: '拆件确认',
|
||||
alreadyProcessing: '该订单正在拆件中,请勿重复操作'
|
||||
alreadyProcessing: '该订单正在拆件中,请勿重复操作',
|
||||
uploadSuccess: '图片上传成功',
|
||||
uploadFailed: '图片上传失败'
|
||||
}
|
||||
},
|
||||
users: {
|
||||
|
|
@ -506,6 +510,22 @@ export default {
|
|||
exportModel: '导出模型',
|
||||
exportInProgress: '正在导出...',
|
||||
exportSuccess: '导出成功',
|
||||
exportFailed: '导出失败'
|
||||
exportFailed: '导出失败',
|
||||
exportFormat: '导出格式',
|
||||
exportAsGLB: '导出为GLB',
|
||||
exportAsOBJ: '导出为OBJ',
|
||||
exportAsSTL: '导出为STL',
|
||||
exportAsFBX: '导出为FBX',
|
||||
selectFormat: '选择导出格式',
|
||||
preparingExport: '准备导出中...'
|
||||
},
|
||||
|
||||
// 模型组件
|
||||
model: {
|
||||
initializing: '模型初始化中...',
|
||||
loading: '模型加载中... {{progress}}%',
|
||||
loadError: '模型加载失败',
|
||||
preview: '预览',
|
||||
delete: '删除'
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,13 @@ const AdminOrders = () => import('@/views/admin/AdminOrders.vue')
|
|||
const AdminUsers = () => import('@/views/admin/AdminUsers.vue')
|
||||
const AdminContentReview = () => import('@/views/admin/AdminContentReview.vue')
|
||||
const AdminDisassemblyOrders = () => import('@/views/admin/AdminDisassemblyOrders.vue')
|
||||
const AdminDisassemblyDetail = () => import('@/views/admin/AdminDisassemblyDetail.vue')
|
||||
const AdminDisassemblyDetail = () => import('@/views/admin/AdminDisassemblyDetail/AdminDisassemblyDetail.vue')
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
redirect: '/login',
|
||||
redirect: '/admin',
|
||||
meta: {
|
||||
title: '首页重定向'
|
||||
}
|
||||
|
|
@ -133,6 +133,9 @@ const router = createRouter({
|
|||
|
||||
// 路由守卫 - 认证检查
|
||||
router.beforeEach((to, from, next) => {
|
||||
localStorage.setItem('token','123')
|
||||
next();
|
||||
return
|
||||
// 设置页面标题
|
||||
if (to.meta?.title) {
|
||||
document.title = to.meta.title
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { prompt, GiminiServer ,MeshyServer} from '@deotaland/utils';
|
||||
const gimiServer = new GiminiServer();
|
||||
export class AdminDisassemblyDetail {
|
||||
constructor() {
|
||||
}
|
||||
//拆件
|
||||
async disassemble(imgurl,callback) {
|
||||
const result = await gimiServer.handleGenerateImage(imgurl, prompt.Hairseparation)
|
||||
console.log('resultresult',result);
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,16 @@
|
|||
<div class="image-item-horizontal" @click="previewImage(thumbnailUrl)">
|
||||
<img :src="thumbnailUrl" alt="缩略图" />
|
||||
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }}</div>
|
||||
<div class="image-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="changePreviewImage"
|
||||
:disabled="currentStep > 1"
|
||||
>
|
||||
{{ $t('admin.disassemblyOrders.detail.changePreview') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-item-horizontal" @click="previewModel(modelUrl)">
|
||||
<div class="model-preview-horizontal">
|
||||
|
|
@ -74,9 +84,8 @@
|
|||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleDisassembly"
|
||||
@click="startDisassembly"
|
||||
:loading="disassemblyLoading"
|
||||
:disabled="currentStep > 1"
|
||||
>
|
||||
{{ $t('admin.disassemblyOrders.detail.disassembly') }}
|
||||
</el-button>
|
||||
|
|
@ -120,7 +129,7 @@
|
|||
class="generate-model-button"
|
||||
type="success"
|
||||
size="small"
|
||||
@click.stop="generateModelFromImage(index)"
|
||||
@click.stop="generateModelFromImage(image)"
|
||||
>
|
||||
生成模型
|
||||
</el-button>
|
||||
|
|
@ -129,14 +138,6 @@
|
|||
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<el-button
|
||||
@click="handleGenerateStep2"
|
||||
:loading="generateStep2Loading"
|
||||
>
|
||||
生成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
|
|
@ -151,29 +152,18 @@
|
|||
<div v-if="currentStep >= 3" class="step-content">
|
||||
<div class="generated-models">
|
||||
<div
|
||||
v-for="(model, index) in generatedModels"
|
||||
v-for="(imgurl, index) in generatedModels"
|
||||
:key="index"
|
||||
class="model-item"
|
||||
@click="previewModel(model.url)"
|
||||
@mouseenter="showModelActions(index)"
|
||||
@mouseleave="hideModelActions"
|
||||
>
|
||||
<div class="model-wrapper">
|
||||
<div class="model-preview">
|
||||
<el-icon size="48"><View /></el-icon>
|
||||
<span>{{ $t('admin.disassemblyOrders.detail.previewDialog') }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="hoveredModelIndex === index"
|
||||
class="delete-model-button"
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click.stop="deleteModel(index)"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<ModelCom
|
||||
@preview="previewModel"
|
||||
:img-url="imgurl"
|
||||
width="100%"
|
||||
height="150px"
|
||||
:show-delete="true"
|
||||
@delete="deleteModel(index)"
|
||||
/>
|
||||
<div class="image-label">模型 {{ index + 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -244,12 +234,10 @@
|
|||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-dialog v-model="imagePreviewVisible" :title="$t('admin.disassemblyOrders.detail.preview')" width="50%" >
|
||||
<img :src="previewImageUrl" style="width:100%;object-fit: contain;" />
|
||||
</el-dialog>
|
||||
|
||||
<!-- 模型预览对话框 -->
|
||||
<el-dialog v-model="modelPreviewVisible" :title="$t('admin.disassemblyOrders.detail.previewDialog')" width="70%">
|
||||
<ModelViewer
|
||||
|
|
@ -259,6 +247,30 @@
|
|||
<template #footer>
|
||||
<el-button @click="modelPreviewVisible = false">{{ $t('admin.disassemblyOrders.detail.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 更换预览图对话框 -->
|
||||
<el-dialog v-model="showImageUpload" :title="$t('admin.disassemblyOrders.detail.changePreview')" width="30%">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
action="#"
|
||||
:show-file-list="false"
|
||||
:before-upload="handleImageUpload"
|
||||
accept="image/*"
|
||||
:disabled="uploadLoading"
|
||||
>
|
||||
<div class="upload-trigger">
|
||||
<el-icon size="32" v-if="!uploadLoading"><Plus /></el-icon>
|
||||
<el-icon size="32" v-else><Loading /></el-icon>
|
||||
<div class="upload-text">
|
||||
{{ uploadLoading ? $t('common.loading') : $t('admin.disassemblyOrders.detail.uploadImage') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="cancelImageUpload">{{ $t('common.cancel') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -271,18 +283,20 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||
import {
|
||||
ArrowLeft,
|
||||
View,
|
||||
ArrowDown,
|
||||
Close,
|
||||
Calendar,
|
||||
Check,
|
||||
Timer
|
||||
Timer,
|
||||
Plus,
|
||||
Loading
|
||||
} from '@element-plus/icons-vue'
|
||||
import ModelViewer from '@/components/common/ModelViewer.vue'
|
||||
|
||||
import ModelCom from '@/components/modelCom/index.vue'
|
||||
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const Plug = new AdminDisassemblyDetail();
|
||||
// 订单详情
|
||||
const orderDetail = ref({
|
||||
orderNumber: '',
|
||||
|
|
@ -298,9 +312,7 @@ const currentStep = ref(1)
|
|||
|
||||
// 加载状态
|
||||
const disassemblyLoading = ref(false)
|
||||
const generateModelLoading = ref(false)
|
||||
const generateStep2Loading = ref(false)
|
||||
const regenerateStep2Loading = ref(false)
|
||||
const processingCompleteLoading = ref(false)
|
||||
const submitShippingLoading = ref(false)
|
||||
|
||||
|
|
@ -313,22 +325,25 @@ const previewModelUrl = ref('')
|
|||
// 图片删除相关
|
||||
const hoveredImageIndex = ref(-1)
|
||||
|
||||
// 模型悬停状态
|
||||
const hoveredModelIndex = ref(-1)
|
||||
// 更换预览图相关
|
||||
const showImageUpload = ref(false)
|
||||
const uploadLoading = ref(false)
|
||||
|
||||
|
||||
// 模型加载状态管理
|
||||
const modelLoadingStates = ref([])
|
||||
const modelLoadingProgress = ref([])
|
||||
const modelInitializationStates = ref([])
|
||||
const modelInitializationProgress = ref([])
|
||||
|
||||
// 资源路径
|
||||
const thumbnailUrl = ref('/src/assets/demo/suoluetu.png')
|
||||
const modelUrl = ref('/src/assets/demo/model.glb')
|
||||
const generatedModelUrl = ref('/src/assets/demo/model.glb')
|
||||
|
||||
// 存储多个生成的模型
|
||||
const generatedModels = ref([])
|
||||
|
||||
// 拆件后的图片(模拟)
|
||||
const disassembledImages = ref([
|
||||
'/src/assets/demo/suoluetu.png',
|
||||
'/src/assets/demo/suoluetu.png',
|
||||
'/src/assets/demo/suoluetu.png'
|
||||
])
|
||||
|
||||
// 发货表单
|
||||
|
|
@ -412,58 +427,52 @@ const previewImage = (url) => {
|
|||
imagePreviewVisible.value = true
|
||||
}
|
||||
|
||||
// 更换预览图
|
||||
const changePreviewImage = () => {
|
||||
showImageUpload.value = true
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
const handleImageUpload = (file) => {
|
||||
uploadLoading.value = true
|
||||
|
||||
// 模拟文件上传处理
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
// 更新预览图URL
|
||||
thumbnailUrl.value = e.target.result
|
||||
uploadLoading.value = false
|
||||
showImageUpload.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.uploadSuccess') || '图片上传成功')
|
||||
}
|
||||
reader.onerror = () => {
|
||||
uploadLoading.value = false
|
||||
ElMessage.error(t('admin.disassemblyOrders.detail.messages.uploadFailed') || '图片上传失败')
|
||||
}
|
||||
reader.readAsDataURL(file.raw || file)
|
||||
}
|
||||
|
||||
// 取消图片上传
|
||||
const cancelImageUpload = () => {
|
||||
showImageUpload.value = false
|
||||
}
|
||||
|
||||
const startDisassembly = () => {
|
||||
for(let i = 0;i<4;i++){
|
||||
handleDisassembly();
|
||||
}
|
||||
}
|
||||
// 执行拆件
|
||||
const handleDisassembly = () => {
|
||||
disassemblyLoading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 这里应该调用实际的拆件API
|
||||
Plug.disassemble(thumbnailUrl.value, (result) => {
|
||||
// 更新拆件图片
|
||||
disassembledImages.value.push(result)
|
||||
currentStep.value = 2
|
||||
disassemblyLoading.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.disassemblySuccess'))
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
// 重新生成第二步内容
|
||||
const handleRegenerateStep2 = () => {
|
||||
regenerateStep2Loading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 这里应该调用实际的重新生成API
|
||||
// 更新拆件图片
|
||||
disassembledImages.value = [
|
||||
'/src/assets/demo/suoluetu.png',
|
||||
'/src/assets/demo/suoluetu.png',
|
||||
'/src/assets/demo/suoluetu.png'
|
||||
]
|
||||
regenerateStep2Loading.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.regenerateSuccess'))
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 生成模型
|
||||
const handleGenerateModel = () => {
|
||||
generateModelLoading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 这里应该调用实际的生成模型API
|
||||
currentStep.value = 3
|
||||
generateModelLoading.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.generateModelSuccess'))
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 模型操作相关方法
|
||||
const showModelActions = (index) => {
|
||||
hoveredModelIndex.value = index
|
||||
}
|
||||
|
||||
const hideModelActions = () => {
|
||||
hoveredModelIndex.value = -1
|
||||
}
|
||||
|
||||
const deleteModel = (index) => {
|
||||
ElMessageBox.confirm(
|
||||
|
|
@ -476,18 +485,16 @@ const deleteModel = (index) => {
|
|||
}
|
||||
).then(() => {
|
||||
generatedModels.value.splice(index, 1)
|
||||
// 清理对应的加载状态
|
||||
modelLoadingStates.value.splice(index, 1)
|
||||
modelLoadingProgress.value.splice(index, 1)
|
||||
modelInitializationStates.value.splice(index, 1)
|
||||
modelInitializationProgress.value.splice(index, 1)
|
||||
ElMessage.success('模型已删除')
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
const exportModel = (index, format) => {
|
||||
const model = generatedModels.value[index]
|
||||
ElMessage.success(`正在导出模型 ${index + 1} 为 ${format.toUpperCase()} 格式`)
|
||||
// 这里可以添加实际的导出逻辑
|
||||
}
|
||||
|
||||
// 加工完成方法
|
||||
const handleProcessingComplete = () => {
|
||||
ElMessageBox.confirm(
|
||||
|
|
@ -588,39 +595,6 @@ onUnmounted(() => {
|
|||
modelPreviewVisible.value = false
|
||||
})
|
||||
|
||||
// 重新生成第三步内容
|
||||
const handleRegenerateStep3 = () => {
|
||||
regenerateStep3Loading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 清空现有模型并重新生成
|
||||
generatedModels.value = []
|
||||
|
||||
// 生成新模型
|
||||
const newModel = {
|
||||
url: '/src/assets/demo/model.glb',
|
||||
sourceImageIndex: 0,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
generatedModels.value.push(newModel)
|
||||
|
||||
regenerateStep3Loading.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.regenerateSuccess'))
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 显示删除按钮
|
||||
const showDeleteButton = (index) => {
|
||||
hoveredImageIndex.value = index
|
||||
}
|
||||
|
||||
// 隐藏删除按钮
|
||||
const hideDeleteButton = () => {
|
||||
hoveredImageIndex.value = -1
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
const deleteImage = (index) => {
|
||||
ElMessageBox.confirm(t('admin.disassemblyOrders.detail.messages.confirmDelete'), t('admin.disassemblyOrders.detail.messages.confirmTitle'), {
|
||||
|
|
@ -638,15 +612,9 @@ const deleteImage = (index) => {
|
|||
// 生成第二步内容(添加新图片)
|
||||
const handleGenerateStep2 = () => {
|
||||
generateStep2Loading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 生成新图片并添加到现有图片列表
|
||||
const newImage = '/src/assets/demo/suoluetu.png'
|
||||
disassembledImages.value.push(newImage)
|
||||
generateStep2Loading.value = false
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.generateSuccess'))
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 显示图片操作按钮
|
||||
|
|
@ -658,52 +626,13 @@ const showImageActions = (index) => {
|
|||
const hideImageActions = () => {
|
||||
hoveredImageIndex.value = -1
|
||||
}
|
||||
|
||||
// 重新生成单张图片
|
||||
const regenerateSingleImage = (index) => {
|
||||
const loadingMessage = ElMessage({
|
||||
message: t('admin.disassemblyOrders.detail.messages.regeneratingImage'),
|
||||
type: 'info',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 替换指定索引的图片
|
||||
disassembledImages.value[index] = '/src/assets/demo/suoluetu.png'
|
||||
loadingMessage.close()
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.regenerateImageSuccess'))
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 从单张图片生成模型并进入第三步
|
||||
const generateModelFromImage = (index) => {
|
||||
const loadingMessage = ElMessage({
|
||||
message: t('admin.disassemblyOrders.detail.messages.generatingModel'),
|
||||
type: 'info',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 创建新模型对象
|
||||
const newModel = {
|
||||
url: '/src/assets/demo/model.glb',
|
||||
sourceImageIndex: index,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
// 将新模型添加到模型数组中
|
||||
generatedModels.value.push(newModel)
|
||||
|
||||
const generateModelFromImage = async (image) => {
|
||||
generatedModels.value.push(image);
|
||||
// 如果是第一次生成模型,进入第三步
|
||||
if (currentStep.value < 3) {
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
loadingMessage.close()
|
||||
ElMessage.success(t('admin.disassemblyOrders.detail.messages.generateModelSuccess'))
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -1074,6 +1003,13 @@ const generateModelFromImage = (index) => {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
|
@ -1210,6 +1146,45 @@ const generateModelFromImage = (index) => {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 上传容器样式 */
|
||||
.upload-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.image-uploader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.upload-trigger:hover {
|
||||
border-color: #6B46C1;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.disassembly-detail {
|
||||
padding: 12px;
|
||||
|
|
@ -16,6 +16,18 @@ export default defineConfig({
|
|||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true,
|
||||
// 配置代理解决CORS问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.deotaland.ai',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
|
|
|
|||
|
|
@ -138,9 +138,11 @@ import { computed, ref, onMounted, watch, nextTick } from 'vue';
|
|||
import {generateImageFromMultipleImages, generateFourViewSheet } from '../../services/aiService';
|
||||
import { convertImagesToDataURL, convertImageToDataURL } from '../../utils/imageUtils';
|
||||
import promptConfig from '../../components/prompt.js'
|
||||
import { GiminiServer } from '@deotaland/utils';
|
||||
// 引入Element Plus图标库和组件
|
||||
import { Cpu, View, Picture, ChatDotRound, MagicStick, CloseBold } from '@element-plus/icons-vue'
|
||||
import { ElIcon } from 'element-plus'
|
||||
const giminiServer = new GiminiServer();
|
||||
// 控制右侧按钮显示状态
|
||||
const showRightControls = ref(false);
|
||||
// 控制鼠标悬停状态
|
||||
|
|
@ -343,9 +345,6 @@ const handleGenerateImage = async () => {
|
|||
// }
|
||||
try {
|
||||
// 添加随机时间戳来避免缓存
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.random().toString(36).substring(7);
|
||||
const cacheBuster = `${timestamp}_${randomSuffix}`;
|
||||
let imageUrl;
|
||||
// 检查是否有草图参考
|
||||
// if (props.cardData.sketch) {
|
||||
|
|
@ -375,7 +374,7 @@ const handleGenerateImage = async () => {
|
|||
referenceImages.push(props.cardData.selectedExpression.imageUrl);
|
||||
}
|
||||
// 转换所有图片为DataURL格式,确保AI模型能够读取
|
||||
const convertedImages = await convertImagesToDataURL(referenceImages);
|
||||
// const convertedImages = await convertImagesToDataURL(referenceImages);
|
||||
// Style: ${props.cardData.style}, high detail, dynamic pose, cinematic lighting, epic fantasy art, 3D Q-version doll ratio.
|
||||
// Appearance: ${modifiedProfile.appearance}.
|
||||
// Personality: ${modifiedProfile.personality}.
|
||||
|
|
@ -417,11 +416,11 @@ const handleGenerateImage = async () => {
|
|||
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立体效果。
|
||||
图片不要有任何水印,
|
||||
保证生成的任务图片一定要有眼睛
|
||||
保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
||||
`
|
||||
;
|
||||
console.log('提示词构建',prompt);
|
||||
imageUrl = await generateImageFromMultipleImages(convertedImages, prompt);
|
||||
// imageUrl = await generateImageFromMultipleImages(convertedImages, prompt);
|
||||
imageUrl = await giminiServer.handleGenerateImage(referenceImages, prompt);
|
||||
// }
|
||||
// else {
|
||||
// // 使用单图参考或无参考生成
|
||||
|
|
@ -464,8 +463,8 @@ onMounted(async () => {
|
|||
// internalImageUrl.value = demoImage;
|
||||
// return
|
||||
// }
|
||||
internalImageUrl.value = demoImage;
|
||||
return
|
||||
// internalImageUrl.value = demoImage;
|
||||
// return
|
||||
handleGenerateImage();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ const handleGenerateModel = async () => {
|
|||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},(error)=>{
|
||||
console.error('模型生成失败:', error);
|
||||
isGenerating.value = false;
|
||||
progressPercentage.value = 0;
|
||||
},{})
|
||||
};
|
||||
// 组件挂载时,如果有图片URL但没有模型URL,自动生成模型
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -52,5 +52,53 @@ export default {
|
|||
10. **风格:** 整体风格应是干净、简约的3D渲染,专注于提供一个可用于后续设计和定制的空白画布。"
|
||||
`,
|
||||
//发型脱离
|
||||
Hairseparation:``,
|
||||
Hairseparation:`1. Layout & Composition
|
||||
Arrange all separated parts (body, hairstyle, headwear/accessories) horizontally in a single image, with clear spacing between each part.
|
||||
|
||||
Ensure no overlapping or occlusion, and all parts are fully visible.
|
||||
|
||||
Maintain correct scale and proportional alignment between parts for 3D modeling reference.
|
||||
|
||||
2. Body
|
||||
|
||||
Include torso, limbs, clothing, and accessories, excluding head, hair, and headwear.
|
||||
|
||||
Preserve all clothing folds, decorations, and accessory details.
|
||||
|
||||
Output as solid, smooth 3D printable model, suitable for resin printing.
|
||||
|
||||
3. Hairstyle
|
||||
|
||||
Display the hairstyle alone, fully detached from body.
|
||||
|
||||
Preserve full 3D volume, flow, and original design, with slightly thicker strands and simplified geometry for durability.
|
||||
|
||||
Ensure no cropping or obstruction by other parts.
|
||||
|
||||
4. Headwear / Accessories
|
||||
|
||||
Separate hat, headpiece, or other head accessories, displayed in-line with hair and body.
|
||||
|
||||
Maintain correct scale and alignment relative to hair.
|
||||
|
||||
Fully visible, no overlaps.
|
||||
|
||||
5. Rendering & Surface Requirements
|
||||
|
||||
Output as solid, sculpture-like models, no colors, textures, or transparency.
|
||||
|
||||
Surfaces: smooth, matte, clean, like unpainted resin prototype.
|
||||
|
||||
Background: neutral or pure white.
|
||||
|
||||
Perspective: slightly angled front view, clearly showing depth and shape of each part.
|
||||
|
||||
6. Additional Instructions for AI
|
||||
|
||||
Emphasize horizontal arrangement, isolation, and even spacing.
|
||||
|
||||
Highlight structural continuity, 3D volume, and printable geometry.
|
||||
|
||||
Avoid tiny fragile details or intersections that may break or obscure parts.
|
||||
`,
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
const login = {
|
||||
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST'},// 生图模型
|
||||
}
|
||||
export default login;
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import login from './login.js';
|
||||
import meshy from './meshy.js';
|
||||
import gemini from './gemini.js';
|
||||
export default {
|
||||
...meshy,
|
||||
...login,
|
||||
...gemini,
|
||||
};
|
||||
|
|
@ -15,6 +15,8 @@ import { request as requestUtils } from './utils/request.js'
|
|||
import * as adminApi from './api/frontend/index.js';
|
||||
import * as clientApi from './api/frontend/index.js';
|
||||
import { MeshyServer } from './servers/meshyserver.js';
|
||||
import { GiminiServer } from './servers/giminiserver.js';
|
||||
import prompt from './servers/prompt.js'
|
||||
// 合并所有工具函数
|
||||
const deotalandUtils = {
|
||||
string: stringUtils,
|
||||
|
|
@ -28,6 +30,8 @@ const deotalandUtils = {
|
|||
adminApi,
|
||||
clientApi,
|
||||
MeshyServer,
|
||||
GiminiServer,
|
||||
prompt,
|
||||
// 全局常用方法
|
||||
debounce: stringUtils.debounce || createDebounce(),
|
||||
throttle: stringUtils.throttle || createThrottle(),
|
||||
|
|
@ -53,6 +57,8 @@ export {
|
|||
adminApi,
|
||||
clientApi,
|
||||
MeshyServer,
|
||||
prompt,
|
||||
GiminiServer,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,21 +1,115 @@
|
|||
import { requestUtils,clientApi } from "../index";
|
||||
let urlRule = 'https://api.deotaland.aiIMGURL'
|
||||
export class FileServer {
|
||||
//文件上传缓存映射 - 静态属性,所有实例共享
|
||||
static fileCacheMap = new Map();
|
||||
constructor() {
|
||||
}
|
||||
//文件拼接
|
||||
concatUrl(url) {
|
||||
return urlRule.replace('IMGURL',url)
|
||||
}
|
||||
/**
|
||||
* 从URL中提取有效的文件名
|
||||
* @param {*} url 文件URL
|
||||
* @returns 有效的文件名
|
||||
*/
|
||||
extractFileName(url) {
|
||||
// 如果是base64格式,根据MIME类型生成文件名
|
||||
if (url.startsWith('data:')) {
|
||||
const mimeType = url.match(/data:([^;]+)/)?.[1];
|
||||
const extension = this.getExtensionFromMimeType(mimeType);
|
||||
return `uploaded_file_${Date.now()}.${extension}`;
|
||||
}
|
||||
|
||||
// 如果是普通URL,提取文件名并清理
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const fileName = pathname.split('/').pop() || 'uploaded_file';
|
||||
|
||||
// 移除查询参数和哈希
|
||||
const cleanFileName = fileName.split('?')[0].split('#')[0];
|
||||
|
||||
// 如果没有扩展名,尝试从URL路径推断
|
||||
if (!cleanFileName.includes('.')) {
|
||||
const extension = this.inferExtensionFromPath(pathname);
|
||||
return cleanFileName + (extension ? `.${extension}` : '');
|
||||
}
|
||||
|
||||
return cleanFileName || `uploaded_file_${Date.now()}`;
|
||||
} catch (error) {
|
||||
// URL解析失败,使用默认文件名
|
||||
return `uploaded_file_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MIME类型获取文件扩展名
|
||||
*/
|
||||
getExtensionFromMimeType(mimeType) {
|
||||
const mimeMap = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/bmp': 'bmp',
|
||||
'image/webp': 'webp',
|
||||
'image/svg+xml': 'svg',
|
||||
'application/pdf': 'pdf',
|
||||
'text/plain': 'txt',
|
||||
'application/json': 'json'
|
||||
};
|
||||
return mimeMap[mimeType] || 'bin';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL路径推断文件扩展名
|
||||
*/
|
||||
inferExtensionFromPath(pathname) {
|
||||
// 常见的图片路径模式
|
||||
if (pathname.includes('/image/') || pathname.includes('/img/')) return 'jpg';
|
||||
if (pathname.includes('/photo/')) return 'jpg';
|
||||
if (pathname.includes('/picture/')) return 'png';
|
||||
return null;
|
||||
}
|
||||
//轮询获取并发时的线上文件映射
|
||||
async pollFileCacheMap(cacheKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (FileServer.fileCacheMap.get(cacheKey)!='loading') {
|
||||
clearInterval(interval);
|
||||
resolve(FileServer.fileCacheMap.get(cacheKey));
|
||||
}
|
||||
}, 1000); // 每1秒检查一次
|
||||
});
|
||||
}
|
||||
//上传文件
|
||||
async uploadFile(url) {
|
||||
const cacheKey = url.slice(-8);
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if(FileServer.fileCacheMap.has(cacheKey)&&FileServer.fileCacheMap.get(cacheKey)!='loading'){
|
||||
resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey)));
|
||||
return;
|
||||
}
|
||||
if(FileServer.fileCacheMap.get(cacheKey)=='loading'){
|
||||
const loadUrl = await this.pollFileCacheMap(cacheKey);
|
||||
resolve(this.concatUrl(loadUrl));
|
||||
return;
|
||||
}
|
||||
FileServer.fileCacheMap.set(cacheKey,'loading');
|
||||
const file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
|
||||
const formData = new FormData();
|
||||
// 从URL中提取文件名,如果没有则使用默认文件名
|
||||
const fileName = url.split('/').pop() || 'uploaded_file';
|
||||
const fileName = this.extractFileName(url);
|
||||
formData.append('file', file, fileName);
|
||||
try {
|
||||
const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData);
|
||||
if(response.code==0){
|
||||
let data = response.data;
|
||||
if(data.url){
|
||||
// 截取后八位作为缓存 key
|
||||
FileServer.fileCacheMap.set(cacheKey, data.url);
|
||||
resolve(urlRule.replace('IMGURL',data.url));
|
||||
}
|
||||
}
|
||||
|
|
@ -43,4 +137,25 @@ export class FileServer {
|
|||
xhr.send();
|
||||
});
|
||||
}
|
||||
//本地文件或者oss文件转为base64格式
|
||||
async fileToBase64(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(xhr.response);
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
} else {
|
||||
reject(new Error('文件转换失败'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,198 @@
|
|||
import { requestUtils,clientApi} from '../index'
|
||||
import { requestUtils, clientApi } from '../index';
|
||||
import { GoogleGenAI, Type, Modality } from "@google/genai";
|
||||
//拆件提示词
|
||||
const API_KEY = 'AIzaSyBmPgJKMnG7afAXR9JW14I5XSkOd_NwCVM';
|
||||
const ai = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null;
|
||||
import { FileServer } from './fileserver';
|
||||
export class GiminiServer extends FileServer {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL获取MIME类型
|
||||
* @param {*} url 图片URL
|
||||
* @returns MIME类型
|
||||
*/
|
||||
getMimeTypeFromUrl(url) {
|
||||
// 检查url是否为字符串
|
||||
if (typeof url !== 'string') {
|
||||
return 'image/jpeg'; // 默认为jpeg
|
||||
}
|
||||
|
||||
try {
|
||||
const extension = url.split('.').pop().toLowerCase().split('?')[0];
|
||||
const mimeTypes = {
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'gif': 'image/gif',
|
||||
'bmp': 'image/bmp',
|
||||
'webp': 'image/webp',
|
||||
'svg': 'image/svg+xml'
|
||||
};
|
||||
return mimeTypes[extension] || 'image/jpeg'; // 默认为jpeg
|
||||
} catch (error) {
|
||||
return 'image/jpeg'; // 出错时返回默认类型
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 将图片转换为GenerativePart
|
||||
* @param {*} dataUrl 图片base64编码或者url
|
||||
* @param {*} type 图片类型,base64或者url
|
||||
* @returns GenerativePart对象
|
||||
*/
|
||||
dataUrlToGenerativePart =async (dataUrl,type='base64') => {
|
||||
if (!dataUrl) return null;
|
||||
|
||||
// 确保dataUrl是字符串
|
||||
if (typeof dataUrl !== 'string') {
|
||||
throw new Error("dataUrl must be a string");
|
||||
}
|
||||
|
||||
// 处理URL类型
|
||||
if(type === 'url'){
|
||||
return {
|
||||
img_url: dataUrl,
|
||||
img_type: await this.getMimeTypeFromUrl(dataUrl),
|
||||
};
|
||||
}
|
||||
|
||||
// 处理base64类型
|
||||
if(type === 'base64'){
|
||||
dataUrl = await this.fileToBase64(dataUrl);
|
||||
}
|
||||
|
||||
const parts = dataUrl.split(',');
|
||||
const mimeType = parts[0].match(/:(.*?);/)?.[1];
|
||||
const base64Data = parts[1];
|
||||
if (!mimeType || !base64Data) {
|
||||
throw new Error("Invalid data URL format");
|
||||
}
|
||||
return {
|
||||
inlineData: {
|
||||
data: base64Data,
|
||||
mimeType: mimeType,
|
||||
},
|
||||
};
|
||||
};
|
||||
//本地生图模型
|
||||
generateImageFromMultipleImages = async (baseImages, prompt, options = {}) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { maxImages = 5 } = options;
|
||||
// 标准化输入:确保 baseImages 是数组
|
||||
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
|
||||
try {
|
||||
if (images.length > maxImages) {
|
||||
reject(`参考图片数量不能超过 ${maxImages} 张`, ERROR_TYPES.VALIDATION, 'low');
|
||||
}
|
||||
if (!prompt || !prompt.trim()) {
|
||||
reject('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
|
||||
}
|
||||
// 处理多个参考图片
|
||||
const imageParts = await Promise.all(images.map(async image =>{
|
||||
return await this.dataUrlToGenerativePart(image);
|
||||
} ));
|
||||
// 构建请求的 parts 数组
|
||||
const parts = [
|
||||
...imageParts,
|
||||
{ text: prompt }
|
||||
];
|
||||
// 执行AI请求
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash-image',
|
||||
contents: {
|
||||
parts: parts,
|
||||
},
|
||||
config: {
|
||||
responseModalities: [Modality.IMAGE],
|
||||
},
|
||||
});
|
||||
console.log(response,'图片结果');
|
||||
let resultImg ;//返回的图片
|
||||
// 处理响应,提取图片数据
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
const base64ImageBytes = part.inlineData.data;
|
||||
const mimeType = part.inlineData.mimeType;
|
||||
resultImg = `data:${mimeType};base64,${base64ImageBytes}`;
|
||||
resolve(resultImg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
console.log(error, 'errorerrorerrorerrorerrorerror');
|
||||
}
|
||||
})
|
||||
};
|
||||
//线上生成模型
|
||||
async generateImageFromMultipleImagesOnline(baseImages, prompt, options = {}){
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { maxImages = 5 } = options;
|
||||
// 标准化输入:确保 baseImages 是数组
|
||||
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
|
||||
const images = await Promise.all(baseImages.map(async (image) => {
|
||||
// if(image.indexOf('tcww')!=-1){
|
||||
// return this.concatUrl('/upload/3e1e9ac2f08b486faca094671d793e25');
|
||||
// }
|
||||
return await this.uploadFile(image);
|
||||
}));
|
||||
try {
|
||||
if (images.length > maxImages) {
|
||||
reject(`参考图片数量不能超过 ${maxImages} 张`, ERROR_TYPES.VALIDATION, 'low');
|
||||
}
|
||||
if (!prompt || !prompt.trim()) {
|
||||
reject('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
|
||||
}
|
||||
// 处理多个参考图片
|
||||
const imageParts = await Promise.all(images.map(async image =>{
|
||||
return await this.dataUrlToGenerativePart(image,'url');
|
||||
} ));
|
||||
// 构建请求的 parts 数组
|
||||
const params = {
|
||||
aspect_ratio:'9:16',
|
||||
inputs: [
|
||||
...imageParts,
|
||||
{ text: prompt }
|
||||
]
|
||||
}
|
||||
const response = await requestUtils.common(clientApi.default.GENERATE_IMAGE, params);
|
||||
// const response = {
|
||||
// "code": 0,
|
||||
// "message": "",
|
||||
// "success": true,
|
||||
// "data": {
|
||||
// "urls": [
|
||||
// {
|
||||
// "url": "/upload/51831c919193447e86843388ae31fc48.png",
|
||||
// "mime_type": "image/png"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
if(response.code!=0){
|
||||
reject(response.msg, ERROR_TYPES.VALIDATION, 'low');
|
||||
return;
|
||||
}
|
||||
let data = response.data;
|
||||
let resultImg = this.concatUrl(data?.urls[0]?.url || '');
|
||||
// 处理响应,提取图片数据
|
||||
resolve(resultImg);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
console.log(error, 'errorerrorerrorerrorerrorerror');
|
||||
}
|
||||
})
|
||||
}
|
||||
//模型生图功能
|
||||
async handleGenerateImage(referenceImages = [], prompt = '') {
|
||||
return new Promise(async (resolve) => {
|
||||
// let result = await this.generateImageFromMultipleImages(referenceImages, prompt);
|
||||
let result = await this.generateImageFromMultipleImagesOnline(referenceImages, prompt);
|
||||
resolve(result);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,36 +5,38 @@ export class MeshyServer extends FileServer {
|
|||
super();
|
||||
}
|
||||
//提交模型任务返回id
|
||||
async createModelTask(item,callback) {
|
||||
async createModelTask(item={},callback,errorCallback,config={}) {
|
||||
try {
|
||||
let params = {
|
||||
project_id: item.project_id,
|
||||
project_id: item.project_id||0,
|
||||
"payload": {
|
||||
image_url:'',
|
||||
ai_model: 'latest',
|
||||
enable_pbr: true,
|
||||
should_remesh: true,
|
||||
should_texture: true,
|
||||
save_pre_remeshed_model: true
|
||||
save_pre_remeshed_model: true,
|
||||
...config
|
||||
}
|
||||
}
|
||||
let imgurl = await this.uploadFile(item.image_url);
|
||||
// let imgurl = 'https://api.deotaland.ai/upload/aabf8b4a8df447fa8c3e3f7978c523cc.png';
|
||||
params.payload.image_url = imgurl;
|
||||
// const response = await requestUtils.common(clientApi.default.IMAGE_TO_3D, params);
|
||||
const response = {
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "019abe70-d843-7160-bacd-c98afc9e80db"
|
||||
}
|
||||
};
|
||||
const response = await requestUtils.common(clientApi.default.IMAGE_TO_3D, params);
|
||||
// const response = {
|
||||
// "code": 0,
|
||||
// "message": "",
|
||||
// "success": true,
|
||||
// "data": {
|
||||
// "result": "019abf5d-450f-74f3-bc9c-a6a7e8995fd1"
|
||||
// }
|
||||
// };
|
||||
if(response.code==0){
|
||||
callback&&callback(response?.data?.result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建模型任务失败:', error);
|
||||
errorCallback&&errorCallback(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// 提示词工程管理
|
||||
export default {
|
||||
Animalsremovewhitemembranes:`
|
||||
去除所有外部附加物:
|
||||
完全移除动物身上的所有服饰、装饰、装备或配件,包括项圈、铃铛、缎带、鞍具、鞋套、饰品等。
|
||||
去除所有毛发与羽毛:
|
||||
彻底移除动物的毛发、羽毛或鳞片纹理,使表面光滑无纹理,就像剃光后的雕塑体。
|
||||
身体结构:
|
||||
保留动物的核心身体形态与比例。
|
||||
明确表现头部、颈部、躯干、四肢与尾巴的分段结构。
|
||||
表面应平滑无任何皮肤褶皱、肌肉细节或纹理。
|
||||
不包含牙齿、舌头或爪垫等小型结构细节。
|
||||
材质与颜色:
|
||||
整个模型应为纯哑光白色(RGB: 255, 255, 255),
|
||||
无纹理、无光泽、无反光效果。
|
||||
呈现类似光滑石膏或未上色树脂模型的质感。
|
||||
面部特征:
|
||||
简化动物的面部结构,仅保留基础的眼窝、鼻部、嘴部轮廓。
|
||||
不包含眼珠、牙齿、舌头或胡须等细节,使其更具雕塑感。
|
||||
背景:
|
||||
使用干净中性背景(如浅灰或白色),以突出主体形体。
|
||||
姿势:
|
||||
保持动物原有的姿势和动作姿态,不做夸张变形。
|
||||
确保整体轮廓清晰,结构平衡,便于后续3D重建。
|
||||
照明:
|
||||
使用柔和、均匀的光照,避免高对比阴影。
|
||||
目标是让身体形体轮廓清晰可见,而非强调表面细节。
|
||||
风格:
|
||||
整体呈现干净、简约、专业的3D素体风格。
|
||||
画面应看起来像是用于雕塑、建模或原型设计的白模。
|
||||
重点是结构与比例,而非真实质感。
|
||||
`,
|
||||
//人物去白膜
|
||||
Personremovewhitemembranes:`
|
||||
1. **去除所有服装:** 彻底移除人物穿着的所有衣物,包括外套、内搭、裤子和鞋子。
|
||||
2. **去除所有配饰:** 彻底移除人物佩戴的所有配饰,包括墨镜、所有项链、所有戒指,以及任何可见的挂件或装饰品和手拿的物品。
|
||||
3. **去除所有头发:** 彻底移除人物的头发,露出光滑的头部。
|
||||
4. **身体结构:** 仅保留人物的基本身体轮廓和比例。头部、颈部、躯干、手臂和腿部应保持清晰的区分,但表面应光滑,没有任何服装的褶皱、纹理或细节。
|
||||
5. **材质与颜色:** 将整个素体模型渲染为纯哑光白色(RGB: 255, 255, 255),无任何纹理、光泽或反光。目标是实现类似于光滑石膏或未上色树脂模型的质感。
|
||||
6. **面部特征:** 面部应被简化,仅保留基本的眼眶、鼻梁和嘴部轮廓,不包含眼珠、眉毛、睫毛等细节,使其看起来像一个雕塑般的人偶脸。
|
||||
7. **背景:** 使用干净中性背景(如浅灰或白色),以突出主体形体。
|
||||
8. **姿势:** 保持人物的原始姿势和手部位置不变。
|
||||
9. **照明:** 调整照明,使其均匀柔和,避免强烈的阴影和高光,以凸显素体的形状而非细节。
|
||||
10. **风格:** 整体风格应是干净、简约的3D渲染,专注于提供一个可用于后续设计和定制的空白画布。"
|
||||
`,
|
||||
//拆件
|
||||
Hairseparation:`1. Layout & Composition
|
||||
Arrange all separated parts (body, hairstyle, headwear/accessories) horizontally in a single image, with clear spacing between each part.
|
||||
|
||||
Ensure no overlapping or occlusion, and all parts are fully visible.
|
||||
|
||||
Maintain correct scale and proportional alignment between parts for 3D modeling reference.
|
||||
|
||||
2. Body
|
||||
|
||||
Include torso, limbs, clothing, and accessories, excluding head, hair, and headwear.
|
||||
|
||||
Preserve all clothing folds, decorations, and accessory details.
|
||||
|
||||
Output as solid, smooth 3D printable model, suitable for resin printing.
|
||||
|
||||
3. Hairstyle
|
||||
|
||||
Display the hairstyle alone, fully detached from body.
|
||||
|
||||
Preserve full 3D volume, flow, and original design, with slightly thicker strands and simplified geometry for durability.
|
||||
|
||||
Ensure no cropping or obstruction by other parts.
|
||||
|
||||
4. Headwear / Accessories
|
||||
|
||||
Separate hat, headpiece, or other head accessories, displayed in-line with hair and body.
|
||||
|
||||
Maintain correct scale and alignment relative to hair.
|
||||
|
||||
Fully visible, no overlaps.
|
||||
|
||||
5. Rendering & Surface Requirements
|
||||
|
||||
Output as solid, sculpture-like models, no colors, textures, or transparency.
|
||||
|
||||
Surfaces: smooth, matte, clean, like unpainted resin prototype.
|
||||
|
||||
Background: neutral or pure white.
|
||||
|
||||
Perspective: slightly angled front view, clearly showing depth and shape of each part.
|
||||
|
||||
6. Additional Instructions for AI
|
||||
|
||||
Emphasize horizontal arrangement, isolation, and even spacing.
|
||||
|
||||
Highlight structural continuity, 3D volume, and printable geometry.
|
||||
|
||||
Avoid tiny fragile details or intersections that may break or obscure parts.
|
||||
`,
|
||||
}
|
||||
|
|
@ -32,8 +32,10 @@ service.interceptors.request.use(
|
|||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// 将token添加到请求头
|
||||
config.headers['Authorization'] = `${token}`;
|
||||
config.headers['token'] = `${token}`;
|
||||
// config.headers['Authorization'] = `${token}`;
|
||||
// config.headers['token'] = `${token}`;
|
||||
config.headers['Authorization'] = `123`;
|
||||
config.headers['token'] = `123`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue