This commit is contained in:
13121765685 2025-11-27 12:05:42 +08:00
parent c20f200df8
commit 555fbbe1af
22 changed files with 1521 additions and 275 deletions

View File

@ -0,0 +1,4 @@
# 开发环境配置
VITE_BASE_URL=/api
VITE_DEV_MODE=true
VITE_LOG_LEVEL=info

View File

@ -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

View File

@ -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
}
}
// FBXGLB
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>

View File

@ -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>

View File

@ -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'
}
}
}

View File

@ -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: '删除'
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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'),

View File

@ -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);
}
// DataURLAI
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 characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturingwith smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
图片不要有任何水印,
保证生成的任务图片一定要有眼睛
保证生成的任务图片一定要有眼睛一定要有嘴巴
`
;
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();
}
});

View File

@ -163,7 +163,11 @@ const handleGenerateModel = async () => {
}
})
}
})
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
},{})
};
// URLURL
onMounted(() => {

View File

@ -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.
`,
}

View File

@ -0,0 +1,4 @@
const login = {
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST'},// 生图模型
}
export default login;

View File

@ -1,6 +1,8 @@
import login from './login.js';
import meshy from './meshy.js';
import gemini from './gemini.js';
export default {
...meshy,
...login,
...gemini,
};

View File

@ -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,
}
/**

View File

@ -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();
});
}
}

View File

@ -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);
})
}
}

View File

@ -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;
}
}

View File

@ -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.
`,
}

View File

@ -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;
},