This commit is contained in:
13121765685 2025-12-03 18:42:53 +08:00
parent 555fbbe1af
commit eaa8f5de1f
56 changed files with 5430 additions and 1833 deletions

View File

@ -26,6 +26,7 @@
"eslint": "^9.15.0",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.3.3",
"terser": "^5.44.1",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.2"

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
import AdminLogin from '@/views/AdminLogin/AdminLogin.vue'
@ -120,7 +120,8 @@ const routes = [
]
const router = createRouter({
history: createWebHistory(),
// history: createWebHistory(),
history: createWebHashHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {

View File

@ -4,9 +4,13 @@ export class AdminDisassemblyDetail {
constructor() {
}
//拆件
async disassemble(imgurl,callback) {
async disassemble(imgurl,callback,errorCallback) {
try{
const result = await gimiServer.handleGenerateImage(imgurl, prompt.Hairseparation)
console.log('resultresult',result);
callback(result)
}catch(error){
errorCallback(error);
}
}
}

View File

@ -470,6 +470,9 @@ const handleDisassembly = () => {
disassembledImages.value.push(result)
currentStep.value = 2
disassemblyLoading.value = false
},(error) => {
disassemblyLoading.value = false
ElMessage.error('拆件失败,请稍后重试')
})
}

View File

@ -16,6 +16,18 @@ export default defineConfig({
resolvers: [ElementPlusResolver()],
}),
],
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
format: {
comments: false
}
}
},
server: {
port: 3000,
host: true,

View File

@ -1,4 +1,20 @@
# 开发环境变量配置
# Google AI API Key用于 AI 功能)
VITE_GOOGLE_API_KEY=your_google_api_key_here
# 基础API地址生产环境
VITE_BASE_URL=https://api.deotaland.ai
# 基础API地址备用
VITE_APP_BASE_API=https://api.deotaland.ai
# Stripe 支付配置
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51SUf06BzlmfuPpixQn3nBDvLcO2qTyeqseM1wcwPcTfGo2Rivggc0axNbFyPrVCfoKIfWuuzIeBzUQl3Fn4Hz0Ea008vLhvv5g
# 应用配置
VITE_APP_TITLE=DeotalandAI
VITE_APP_DESCRIPTION=AI-Powered Creation Platform
# 开发环境配置
VITE_BASE_URL=/api
VITE_DEV_MODE=true
VITE_LOG_LEVEL=info
VITE_LOG_LEVEL=debug

View File

@ -5,19 +5,14 @@ VITE_BASE_URL=https://api.deotaland.ai
# 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_STRIPE_PUBLISHABLE_KEY=pk_test_51SUf06BzlmfuPpixQn3nBDvLcO2qTyeqseM1wcwPcTfGo2Rivggc0axNbFyPrVCfoKIfWuuzIeBzUQl3Fn4Hz0Ea008vLhvv5g
# 应用配置
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

View File

@ -12,6 +12,7 @@
</head>
<body>
<div id="app"></div>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -35,6 +35,7 @@
"devDependencies": {
"@iconify-json/feather": "^1.2.1",
"@vitejs/plugin-vue": "^6.0.1",
"terser": "^5.44.1",
"unplugin-auto-import": "^20.2.0",
"unplugin-icons": "^22.5.0",
"unplugin-vue-components": "^30.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -2,7 +2,7 @@
<div v-if="show" class="guide-modal-overlay" @click="handleOverlayClick">
<div class="guide-modal-container" @click.stop>
<!-- 关闭按钮 -->
<button class="close-button" @click="closeModal" title="跳过引导">
<button class="close-button" @click="closeModal" :title="t('header.skipGuide')">
<span class="close-icon">×</span>
</button>
@ -54,7 +54,7 @@
class="action-button secondary"
@click="prevStep"
>
上一步
{{ t('header.previous') }}
</button>
<!-- 跳过按钮 -->
@ -62,7 +62,7 @@
class="action-button skip"
@click="skipGuide"
>
跳过引导
{{ t('header.skipGuide') }}
</button>
<!-- 下一步/完成按钮 -->
@ -70,7 +70,7 @@
class="action-button primary"
@click="nextStep"
>
{{ isLastStep ? '开始创作' : '下一步' }}
{{ isLastStep ? t('header.startCreating') : t('header.next') }}
</button>
</div>
</div>
@ -80,7 +80,7 @@
<!-- 步骤指示器 -->
<div class="step-indicator">
<span class="step-text">{{ currentStep + 1 }} / {{ guideSteps.length }}</span>
<span class="step-text">{{ t('header.step') }} {{ currentStep + 1 }} / {{ guideSteps.length }}</span>
</div>
</div>
</div>
@ -88,6 +88,7 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
// props
const props = defineProps({
@ -100,38 +101,41 @@ const props = defineProps({
// emits
const emit = defineEmits(['close', 'complete']);
//
const { t } = useI18n();
//
const currentStep = ref(0);
//
const guideSteps = ref([
const guideSteps = computed(() => [
{
id: 1,
title: '参考图片',
description: '选择您喜欢的图片作为创作参考',
title: t('guideModal.step1.title'),
description: t('guideModal.step1.description'),
image: new URL('@/assets/step/creatProject/step1.png', import.meta.url).href,
tips: '点击生成按钮后平台会根据您的选择生成相应的3D模型。'
tips: t('guideModal.step1.tips')
},
{
id: 2,
title: '模型生成/文字优化',
description: '根据您的参考图片平台会生成对应的3D模型。',
title: t('guideModal.step2.title'),
description: t('guideModal.step2.description'),
image: new URL('@/assets/step/creatProject/step2.png', import.meta.url).href,
tips: '您也可以输入文字描述,平台会根据您的需求进行图片优化。'
tips: t('guideModal.step2.tips')
},
{
id: 3,
title: '查看详情',
description: '点击查看详情按钮您可以查看更多关于您创作的3D模型的信息。',
title: t('guideModal.step3.title'),
description: t('guideModal.step3.description'),
image: new URL('@/assets/step/creatProject/step3.png', import.meta.url).href,
tips: ''
tips: t('guideModal.step3.tips')
},
{
id: 4,
title: '定制到家',
description:'根据您的需求平台会为您定制专属的3D模型机器人确保符合您的要求。',
title: t('guideModal.step4.title'),
description: t('guideModal.step4.description'),
image: new URL('@/assets/step/creatProject/step4.png', import.meta.url).href,
tips: '您可以优先在智能体中配置模型角色',
tips: t('guideModal.step4.tips')
}
]);

View File

@ -1,7 +1,7 @@
<template>
<header class="header-component">
<div class="back-button-area">
<button class="back-btn" @click="handleBack">
<button class="back-btn" @click="handleBack" :title="t('header.back')">
<el-icon class="back-icon">
<ArrowLeft />
</el-icon>
@ -11,7 +11,7 @@
<div class="project-name-section">
<div class="project-title-container" v-if="!isEditing">
<h1 class="project-title">{{ projectName }}</h1>
<button class="edit-btn" @click="startEditing" title="编辑项目名称">
<button class="edit-btn" @click="startEditing" :title="t('header.editProjectName')">
<el-icon class="edit-icon">
<Edit />
</el-icon>
@ -24,10 +24,10 @@
class="project-name-input"
size="default"
maxlength="50"
placeholder="请输入项目名称"
:placeholder="t('header.projectNamePlaceholder')"
@keyup.enter="handleSave"
/>
<button class="save-btn" @click="handleSave" title="保存">
<button class="save-btn" @click="handleSave" :title="t('header.saveProjectName')">
<el-icon class="save-icon">
<Check />
</el-icon>
@ -36,28 +36,25 @@
</div>
<div class="header-right-section">
<div class="points-display">
<el-icon class="points-icon">
<Star />
</el-icon>
<span class="points-text">{{ userPoints }}</span>
<div class="free-counts-display">
<div class="image-count">
<el-icon class="count-icon">
<Picture />
</el-icon>
<span class="count-text">{{ t('header.imageFreeCount') }}: {{ freeImageCount }}{{ t('header.times') }}</span>
</div>
<div class="model-count">
<el-icon class="count-icon">
<MagicStick />
</el-icon>
<span class="count-text">{{ t('header.modelFreeCount') }}: {{ freeModelCount }}{{ t('header.times') }}</span>
</div>
</div>
<div class="membership-section">
<el-button
:class="['membership-btn', { 'member': isMember }]"
@click="handleMembershipClick"
size="small"
>
{{ isMember ? '会员特权' : '开通会员' }}
</el-button>
</div>
<LanguageToggle
position="top-right"
/>
<button class="guide-btn" @click="emit('openGuideModal')" title="使用指南">
<button class="guide-btn" @click="emit('openGuideModal')" :title="t('header.guide')">
<el-icon class="guide-icon">
<Guide />
</el-icon>
@ -73,19 +70,21 @@
<script setup>
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { Star, ArrowLeft, Edit, Check, Guide } from '@element-plus/icons-vue'
import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plus/icons-vue'
import { ElButton, ElIcon, ElInput } from 'element-plus'
import ThemeToggle from '../ui/ThemeToggle.vue'
import LanguageToggle from '../ui/LanguageToggle.vue'
const emit = defineEmits(['openGuideModal'])
// API
const projectName = ref('数字创作平台')
//
const userPoints = ref(1280)
//
const isMember = ref(true)
//
const showGuideModal = ref(false)
const props = defineProps({
projectName: {
type: String,
default: 'project'
}
})
//
const freeImageCount = ref(5)
//
const freeModelCount = ref(3)
//
const isEditing = ref(false)
const editedProjectName = ref('')
@ -94,26 +93,7 @@ const editInput = ref(null)
//
const { t, locale } = useI18n()
//
const updateProjectName = () => {
projectName.value = locale.value === 'en' ? 'Digital Creation Platform' : '数字创作平台'
}
//
import { watch } from 'vue'
watch(() => locale.value, updateProjectName)
//
updateProjectName()
//
const handleMembershipClick = () => {
// /
console.log('会员按钮点击')
}
// LanguageToggle
//
const handleBack = () => {
//
@ -128,7 +108,7 @@ const handleBack = () => {
//
const startEditing = () => {
isEditing.value = true
editedProjectName.value = projectName.value
editedProjectName.value = props.projectName
nextTick(() => {
if (editInput.value) {
editInput.value.focus()
@ -139,21 +119,13 @@ const startEditing = () => {
//
const handleSave = () => {
const name = editedProjectName.value.trim()
if (name && name !== projectName.value) {
projectName.value = name
//
console.log('项目名称更新为:', name)
if (name && name !== props.projectName) {
emit('updateProjectInfo', { title: name })
}
isEditing.value = false
}
//
const handleBlur = () => {
//
setTimeout(() => {
handleSave()
}, 100)
}
</script>
<style scoped>
@ -414,7 +386,14 @@ html.dark .project-name-input :deep(.el-input__wrapper:focus) {
flex: 1;
}
.points-display {
.free-counts-display {
display: flex;
align-items: center;
gap: 12px;
}
.image-count,
.model-count {
display: flex;
align-items: center;
gap: 6px;
@ -426,46 +405,16 @@ html.dark .project-name-input :deep(.el-input__wrapper:focus) {
color: #6B7280;
}
.points-icon {
.count-icon {
color: #6B46C1;
font-size: 18px;
font-size: 16px;
}
.points-count {
font-size: 14px;
font-weight: 600;
color: #1F2937;
}
.membership-section {
display: flex;
align-items: center;
}
.membership-btn {
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
.count-text {
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
background-color: #6B46C1;
color: white;
border: none;
}
.membership-btn:hover {
background-color: #5B21B6;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
}
.membership-btn.member {
background-color: #A78BFA;
color: white;
}
.membership-btn.member:hover {
background-color: #9333EA;
color: #1F2937;
white-space: nowrap;
}
/* 指南按钮样式 */
@ -521,31 +470,15 @@ html.dark .project-title {
color: #A78BFA;
}
html.dark .points-display {
html.dark .image-count,
html.dark .model-count {
background-color: #374151;
}
html.dark .points-count {
html.dark .count-text {
color: #E5E7EB;
}
html.dark .membership-btn {
background-color: #8B5CF6;
}
html.dark .membership-btn:hover {
background-color: #7C3AED;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
html.dark .membership-btn.member {
background-color: #6D28D9;
}
html.dark .membership-btn.member:hover {
background-color: #5B21B6;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.header-component {
@ -575,21 +508,17 @@ html.dark .membership-btn.member:hover {
gap: 8px;
}
.points-display {
.image-count,
.model-count {
padding: 6px 10px;
}
.points-icon {
font-size: 16px;
.count-icon {
font-size: 14px;
}
.points-count {
font-size: 13px;
}
.membership-btn {
padding: 6px 12px;
font-size: 13px;
.count-text {
font-size: 11px;
}
}
@ -607,9 +536,6 @@ html.dark .membership-btn.member:hover {
gap: 6px;
}
.membership-btn {
padding: 4px 8px;
font-size: 12px;
}
}
</style>

View File

@ -1,20 +1,5 @@
<template>
<div class="ip-card-container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 左侧附件卡片区域 -->
<!-- <div class="left-attachment-card" v-if="internalImageUrl||generateFourView" :class="{ 'show': isHovered }" :style="leftCardStyle">
<img
v-if="leftCardImageUrl"
:src="leftCardImageUrl"
:alt="'白模附件图片'"
class="left-card-image"
@error="handleLeftImageError"
@load="handleLeftImageLoad"
/>
<div v-else class="left-card-loading">
<div class="white-model-spinner"></div>
<div class="white-model-loading-text">正在生成白模...</div>
</div>
</div> -->
<!-- 主卡片区域 -->
<div class="ip-card-wrapper">
<!-- 文本输入框弹出层 -->
@ -49,29 +34,13 @@
</div>
<div class="ip-card" :style="cardStyle">
<img
v-if="props.generateFourView && fourViewImageUrl"
:src="fourViewImageUrl"
:alt="altText || 'IP四视图'"
v-if="formData.internalImageUrl"
:src="formData.internalImageUrl"
alt="IP"
class="ip-card-image"
@error="handleImageError"
@load="handleImageLoad"
/>
<img
v-else-if="internalImageUrl && !props.generateFourView"
:src="internalImageUrl"
:alt="altText || 'IP展示图片'"
class="ip-card-image"
@error="handleImageError"
@load="handleImageLoad"
/>
<div v-else-if="generateSmoothWhiteModelStatus" class="generating-placeholder">
<div class="generating-spinner"></div>
<div class="generating-text">正在生成白膜...</div>
</div>
<div v-else-if="isGeneratingFourView" class="generating-placeholder">
<div class="generating-spinner"></div>
<div class="generating-text">正在生成四视图...</div>
</div>
<div v-else class="generating-placeholder">
<div class="generating-spinner"></div>
<div class="generating-text">正在生成图片...</div>
@ -130,29 +99,24 @@
<script setup>
import demoImage from '@/assets/demo.png'
import demo4 from '@/assets/demo4.png'
import demosst from '@/assets/demosst.png'
import emails from '../../assets/sketches/emails.png'
import { API_BASE_URL } from '../../../ipconfig';
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';
import humanTypeImg from '@/assets/sketches/tcww.png'
import anTypeImg from '@/assets/sketches/dwww.png';
// Element Plus
import { Cpu, View, Picture, ChatDotRound, MagicStick, CloseBold } from '@element-plus/icons-vue'
import { ElIcon } from 'element-plus'
import { Cpu, ChatDotRound, CloseBold } from '@element-plus/icons-vue'
import { ElIcon,ElMessage } from 'element-plus'
const formData = ref({
internalImageUrl: '',//URL
status:'loading',//
})
const giminiServer = new GiminiServer();
//
const showRightControls = ref(false);
//
const isHovered = ref(false);
// URL
const leftCardImageUrl = ref('');
//
const imageAspectRatio = ref(16 / 9); //
//
const leftCardAspectRatio = ref(16 / 9); //
//
const showTextInput = ref(false);
//
@ -170,12 +134,6 @@ const handleMouseLeave = () => {
//
showRightControls.value = false;
};
// /
const toggleActions = () => {
showRightControls.value = !showRightControls.value;
};
// /
const toggleTextInput = () => {
showTextInput.value = !showTextInput.value;
@ -196,31 +154,20 @@ const handleTextInputConfirm = () => {
//
if (textInputValue.value.trim()) {
emit('create-prompt-card', {
img: props.generateFourView?fourViewImageUrl.value:internalImageUrl.value,
prompt:textInputValue.value,
img: formData.value.internalImageUrl,
diyPromptText:textInputValue.value,
generateFourView:props.generateFourView
});
showTextInput.value = false;
textInputValue.value = '';
}
};
//
const handleTextInputCancel = () => {
showTextInput.value = false;
textInputValue.value = '';
};
//
const handleHairDetach = () => {
// URL
emit('create-prompt-card', {
img: props.generateFourView?fourViewImageUrl.value:internalImageUrl.value,
prompt:promptConfig.Hairseparation,
generateFourView:props.generateFourView
});
};
//
const props = defineProps({
// URL
@ -228,11 +175,6 @@ const props = defineProps({
type: String,
default: ''
},
//
altText: {
type: String,
default: ''
},
// 200px
cardWidth: {
type: Number,
@ -248,16 +190,6 @@ const props = defineProps({
type: Number,
default: 8
},
//
cardName: {
type: String,
default: 'Chrono Warden 801'
},
//
cardSeries: {
type: String,
default: 'Series: Quantum Nights'
},
// 使9:16false使
useFixedRatio: {
type: Boolean,
@ -268,122 +200,51 @@ const props = defineProps({
type: Object,
default: () => ({})
},
//
isGenerating: {
type: Boolean,
default: false
},
//
generateFourView: {
type: Boolean,
default: false
},
//
selectedMaterial: {
type: Object,
default: ''
},
//
selectedColor: {
type: Object,
default: ''
},
//
generateSmoothWhiteModelStatus: {
type: Boolean,
default: false
},
//使
diyPromptText: {
type: String,
default: ''
}
});
//
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card']);
// URL
const internalImageUrl = ref(props.imageUrl);
//
const fourViewImageUrl = ref('');
const isGeneratingFourView = ref(false);
//
const emitGenerateSmoothWhiteModel = () => {
emit('generate-smooth-white-model', internalImageUrl.value);
}
//
const handleGenerateFourView = async () => {
try {
isGeneratingFourView.value = true;
console.log('开始生成四视图...');
// 使
const fourViewUrl = await generateFourViewSheet(
internalImageUrl.value,
);
//
fourViewImageUrl.value = fourViewUrl;
// generateSmoothWhiteModel(fourViewUrl)
console.log('四视图生成成功!');
} catch (error) {
console.error('生成四视图失败:', error);
} finally {
isGeneratingFourView.value = false;
}
};
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete']);
//
const handleGenerateImage = async () => {
// cardData
// if (!props.cardData || !props.cardData.profile || !props.cardData.style) {
// console.warn('');
// return;
// }
try {
//
let imageUrl;
//
// if (props.cardData.sketch) {
// 使
const referenceImages = [];
//
if (props?.cardData?.sketch) {
referenceImages.push(props.cardData.sketch.imageUrl);
}
//
if (props?.cardData?.inspirationImage) {
referenceImages.push(props.cardData.inspirationImage);
}
//
if (props?.cardData?.selectedMaterial) {
referenceImages.push(props.cardData.selectedMaterial.imageUrl);
}
// IP
if (props?.cardData?.ipTypeImg) {
referenceImages.push(props.cardData.ipTypeImg);
// if (props?.cardData?.ipTypeImg) {
// }
if(props.cardData.diyPromptText){
referenceImages.push(props.cardData.imageUrl);
}else{
referenceImages.push(humanTypeImg);
}
// 使
if(props?.cardData?.diyPromptText){
referenceImages.push(props.cardData.imageUrl);
}
if(props?.cardData?.selectedExpression){
referenceImages.push(props.cardData.selectedExpression.imageUrl);
}
// DataURLAI
// 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}.
// Cache ID: ${modifiedProfile.cacheBuster || 'default'}.
// of ${modifiedProfile.name}.
const prompt = props.diyPromptText|| `A full-body character portrait
// referenceImages.push(humanTypeImg);
// if(props?.cardData?.selectedExpression){
// referenceImages.push(props.cardData.selectedExpression.imageUrl);
// }
//
const prompt = props.cardData.diyPromptText|| `
如果有两张参考图并且第一张参考图是动物则使用第一张图参考疯狂动物城的人物设计风格特征进行设计动作参考第二张参考图并且忽略一下写的所有规则
如果有两张参考图并且第一张参考图是人的话则忽略上一个规则按照以下规则
设计动作一定要参考第二张参考图让人物动作与第二张参考图中的人物动作完全一致
A full-body character portrait
Ensure the output image has a portrait aspect ratio of 9:16.
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
角色特征Q 版萌系人物造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感色彩搭配和谐且富有层次眼睛位置与参考图一致材质表现逼真
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
${props?.cardData?.selectedHairColor?
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
${props?.cardData?.selectedSkinColor?
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
${props?.cardData?.sketch?
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
${props?.cardData?.selectedMaterial?
@ -394,15 +255,15 @@ const handleGenerateImage = async () => {
`Material Color:${props?.cardData?.selectedColor?.hex}`:``}
${props?.cardData?.selectedExpression?
`The facial expression is completely based on the picture I gave you`:``}
Note: The image should not have white borders.
The hairstyle must be suitable for 3D printing:
发型需要与参考图任务完全一致严格按照参考图中的头发形态方向长度体积进行完整还原.
${props?.cardData?.selectedHairColor?
`Hair Color:${props?.cardData?.selectedHairColor?.hex}`:``}
${props?.cardData?.selectedSkinColor?
`Skin Color:${props?.cardData?.selectedSkinColor?.hex}`:``}
${props?.cardData?.inspirationImage?`
如果参考图是人形并且有发型有的动物参考图可能没有发型可以忽略发型需要与第一张参考图完全一致严格按照参考图中的头发形态方向长度体积进行完整还原.
发型必须适合3d打印
请严格按照参考图中的角色服装进行完整还原
服装结构确保上衣下装外套鞋履配饰等所有服装元素与参考图一致保持准确的比例形状与层次
服装结构确保上衣下装外套鞋履配饰等所有服装元素与参考图一致保持准确的比例形状与层次
眼睛位置与第一张参考图一致材质表现逼真保留参考图中的细节比如还原参考图中的头发
`:``}
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感色彩搭配和谐且富有层次.
Note: The image should not have white borders.
完整度不要简化修改或重新设计服装需忠于原设计
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性
排除项禁止添加额外装饰图案文字徽章或修改风格
@ -419,130 +280,56 @@ const handleGenerateImage = async () => {
保证生成的任务图片一定要有眼睛一定要有嘴巴
`
;
// imageUrl = await generateImageFromMultipleImages(convertedImages, prompt);
imageUrl = await giminiServer.handleGenerateImage(referenceImages, prompt);
// }
// else {
// // 使
// imageUrl = await generateCharacterImage(
// modifiedProfile,
// props.cardData.style,
// props.cardData.inspirationImage || null
// );
// }
console.log(imageUrl,'imageUrlimageUrl',props.generateFourView);
imageUrl = await giminiServer.handleGenerateImage(referenceImages, prompt,{
project_id: props.cardData.project_id,
});
//
if(props.generateFourView){
fourViewImageUrl.value = imageUrl;
}else{
internalImageUrl.value = imageUrl;
}
// generateSmoothWhiteModel(imageUrl);
console.log('角色图片生成成功!');
formData.value.internalImageUrl = imageUrl;
formData.value.status = 'success';
saveProject();
} catch (error) {
console.error('生成角色图片失败:', error);
ElMessage.error('生成图片失败,请稍后重试');
emit('delete');
}
};
const init = ()=>{
let status = props.cardData.status;
switch (status) {
case 'loading':
handleGenerateImage();
break;
default:
formData.value.internalImageUrl = props.cardData.imageUrl;
break;
}
}
//
const saveProject = ()=>{
emit('save-project', {
imageUrl:formData.value.internalImageUrl,
// status:formData.value.status,
status:'success',
});
}
//
onMounted(async () => {
//
if (props.generateFourView&&!props.diyPromptText) {
// fourViewImageUrl.value = demosst;
// return
internalImageUrl.value = props.cardData.imageUrl;
handleGenerateFourView();
}else if(props.generateSmoothWhiteModelStatus){//
internalImageUrl.value = demo4;
return
// generateSmoothWhiteModel(props.cardData.imageUrl);
}
// cardDatainternalImageUrl
else {
// if(!props.diyPromptText){
// internalImageUrl.value = demoImage;
// formData.value.internalImageUrl = demoImage;
// return
// }
// internalImageUrl.value = demoImage;
// return
handleGenerateImage();
}
init();
});
// props.imageUrlinternalImageUrl
watch(() => props.imageUrl, (newValue) => {
if (newValue) {
internalImageUrl.value = newValue;
}
});
//
const leftCardStyle = computed(() => {
const width = props.cardWidth * 0.8; //
const height = width / leftCardAspectRatio.value;
return {
width: `${width}px`,
height: `${height}px`,
borderRadius: `${props.borderRadius}px`,
border: props.showBorder ? '1px solid #e0e0e0' : 'none'
};
});
//
const handleLeftImageError = (event) => {
console.warn('左侧卡片图片加载失败:', event.target.src);
};
//
const handleLeftImageLoad = (event) => {
const img = event.target;
const width = img.naturalWidth;
const height = img.naturalHeight;
if (width > 0 && height > 0) {
leftCardAspectRatio.value = width / height;
console.log(`左侧卡片图片比例: ${width}:${height} (${leftCardAspectRatio.value.toFixed(2)})`);
}
};
//
const handleGenerateModel = async () => {
// if(!leftCardImageUrl.value){
// return
// }
try {
emit('generate-model-requested', {
cardId: props.cardData.id,
imageUrl: props.generateFourView ? fourViewImageUrl.value : internalImageUrl.value,
// imageUrl: fourViewImageUrl.value,
imageUrl:formData.value.internalImageUrl,
generateFourView: props.generateFourView,
});
} catch (error) {
console.error('生成3D模型失败:', error);
}
};
//
const handleCreateNewCard = async () => {
// if (!internalImageUrl.value || !props.cardData?.profile) {
// console.warn('');
// return;
// }
try {
// AIDataURL
const convertedBaseImage = await convertImageToDataURL(internalImageUrl.value);
//
emit('create-new-card', {
baseImage: convertedBaseImage,
// profile: props.cardData.profile,
// sourceCardId: props.cardData.id
});
console.log('已请求创建新卡片');
} catch (error) {
console.error('创建新卡片请求失败:', error);
}
};
//
const cardStyle = computed(() => {
const width = props.cardWidth;

View File

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

View File

@ -52,9 +52,7 @@
@acknowledge="handleAcknowledge" />
<PurchaseModal
:show="showPurchaseModal"
:template="purchaseInfo.template"
:id="purchaseInfo.id"
:modelUrl="purchaseInfo.modelUrl"
:modelData="modelData"
@close="showPurchaseModal=false" />
</template>
@ -103,63 +101,6 @@ const closeModal = () => {
emit('close');
};
//
const toggleRotation = () => {
isRotating.value = !isRotating.value;
};
//
const resetView = () => {
// 3D
};
// GLB
const exportAsGLB = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsGLB();
}
};
// OBJ
const exportAsOBJ = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsOBJ();
}
};
// STL
const exportAsSTL = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsSTL();
}
};
const getThemeBg = () => {
const root = document.documentElement;
const cssBg = getComputedStyle(root).getPropertyValue('--bg-color').trim();
if (cssBg) return cssBg;
return root.classList.contains('dark') ? '#111827' : '#ffffff';
};
const toggleWireframe = () => {
isWireframe.value = !isWireframe.value;
if (threeModelViewer.value) {
threeModelViewer.value.setWireframe(isWireframe.value);
}
};
const toggleWhiteMode = () => {
isWhiteMode.value = !isWhiteMode.value;
if (isWhiteMode.value && isWireframe.value) {
isWireframe.value = false;
if (threeModelViewer.value) {
threeModelViewer.value.setWireframe(false);
}
}
if (threeModelViewer.value) {
threeModelViewer.value.setWhiteMode(isWhiteMode.value);
}
};
const handleCustomize = () => {
//
@ -167,23 +108,15 @@ const handleCustomize = () => {
//
showOrderProcessModal.value = true;
};
const handleDeepFix = () => {};
const handleBuyFromCustomize = (payload) => {
purchaseInfo.value = payload || { id: '', template: '', modelUrl: '' }
const handleBuyFromCustomize = () => {
console.log('点击知晓');
purchaseInfo.value = props.modelData
showCustomizeModal.value = false
showPurchaseModal.value = true
};
const handleAcknowledge = (modelData) => {
// ""
// handleBuyFromCustomize使
const payload = {
id: modelData?.cardId || modelData?.taskId || '',
template: 'white', //
modelUrl: modelData?.modelUrl || ''
};
handleBuyFromCustomize(payload);
const handleAcknowledge = () => {
handleBuyFromCustomize();
};
watch(() => props.show, (newVal) => {
@ -492,7 +425,4 @@ onBeforeUnmount(() => {
font-size: 20px;
}
}
</style>
<CustomizationModal :show="showCustomizeModal" :modelData="modelData" @close="showCustomizeModal=false" @buy="handleBuyFromCustomize" />
<PurchaseModal :show="showPurchaseModal" :template="purchaseInfo.template" :id="purchaseInfo.id" :modelUrl="purchaseInfo.modelUrl" @close="showPurchaseModal=false" />
</style>

View File

@ -3,45 +3,24 @@
<!-- 卡片头部 -->
<div class="order-card__header">
<div class="order-card__id">
<span class="order-number">#{{ order.id }}</span>
<el-tag :type="statusType" size="small">
<el-icon v-if="order.status === 'pending'" class="status-icon"><Clock /></el-icon>
<span class="order-number">#{{ order.order_no }}</span>
</div>
<el-tag :type="statusType.status" size="small">
<el-icon v-if="true" class="status-icon"><Clock /></el-icon>
<el-icon v-else-if="order.status === 'paid'" class="status-icon"><Check /></el-icon>
<el-icon v-else-if="order.status === 'shipped'" class="status-icon"><Van /></el-icon>
<el-icon v-else-if="order.status === 'completed'" class="status-icon"><CircleCheck /></el-icon>
<el-icon v-else-if="order.status === 'expired'" class="status-icon"><Warning /></el-icon>
{{ statusLabel }}
{{ statusType.label }}
</el-tag>
</div>
<div class="order-card__amount">
<span class="amount-value">¥{{ (order.total || order.amount || 0).toLocaleString() }}</span>
<el-icon class="expand-icon" :class="{ 'expand-icon--rotated': isExpanded }" @click.stop="toggleExpand">
<ArrowDown />
</el-icon>
</div>
</div>
<div v-if="order.status === 'pending'" class="order-card__countdown">
<el-icon class="countdown-icon"><Clock /></el-icon>
<span v-if="!isExpired" class="countdown-text">{{ t('orderManagement.countdown.remaining') }}: {{ countdownText }}</span>
<el-tag v-else type="danger" size="small">{{ t('orderManagement.countdown.expired') }}</el-tag>
</div>
<!-- 卡片内容 -->
<div class="order-card__content">
<!-- 基础信息 -->
<div class="order-info">
<div class="customer-section">
<div class="customer-info">
<el-icon class="customer-icon"><User /></el-icon>
<div class="customer-details">
<span class="customer-name">{{ order.customerName }}</span>
<span class="customer-email">{{ order.customerEmail }}</span>
</div>
</div>
<div class="order-date">
<el-icon><Calendar /></el-icon>
<span>{{ formatDate(order.createdAt) }}</span>
<span>{{ formatDate(order.created_at) }}</span>
</div>
</div>
@ -50,113 +29,58 @@
<h4 class="products-title">{{ t('orderManagement.order.products') }}</h4>
<div class="products-list">
<div
v-for="product in order.products"
:key="product.id"
class="product-item"
>
<img
:src="product.image"
:alt="product.name"
:src="order?.order_info?.modelData?.imageUrl"
:alt="order?.order_info?.ipName"
class="product-image"
@error="handleImageError"
/>
<div class="product-details">
<span class="product-name">{{ product.name }}</span>
<span class="product-quantity">x{{ product.quantity }}</span>
<span class="product-name">{{ order.order_info.ipName||'rob' }}</span>
<span class="product-quantity">x{{ order.order_info.quantity }}</span>
</div>
<div class="product-price">¥{{ (product.price || 0).toLocaleString() }}</div>
<div class="product-price">¥{{order.actual_amount}}</div>
</div>
</div>
</div>
</div>
<!-- 展开的详细信息弹层 -->
<el-popover
v-model:visible="isExpanded"
virtual-triggering
:virtual-ref="cardRef"
placement="bottom-start"
:width="cardWidth"
teleported
:show-arrow="false"
trigger="manual"
popper-class="order-popover"
>
<div class="order-details">
<div class="shipping-section">
<h4 class="section-title">
{{ t('orderManagement.order.shipping') }}
</h4>
<div class="shipping-info">
<p><strong>{{ t('orderManagement.order.recipient') }}:</strong> {{ order.shipping?.recipientName || '-' }}</p>
<p><strong>{{ t('orderManagement.order.phone') }}:</strong> {{ order.shipping?.phone || '-' }}</p>
<p><strong>{{ t('orderManagement.order.address') }}:</strong> {{ order.shipping?.address || '-' }}</p>
</div>
</div>
<div class="payment-section">
<h4 class="section-title">
<el-icon><CreditCard /></el-icon>
{{ t('orderManagement.order.payment') }}
</h4>
<div class="payment-info">
<p><strong>{{ t('orderManagement.order.paymentMethod') }}:</strong> {{ getPaymentMethodLabel(order.payment?.method) }}</p>
<p><strong>{{ t('orderManagement.order.paymentStatus') }}:</strong>
<el-tag :type="order.payment?.status === 'paid' ? 'success' : 'warning'" size="small">
{{ getPaymentStatusLabel(order.payment?.status) }}
</el-tag>
</p>
<p v-if="order.payment?.paidAt">
<strong>{{ t('orderManagement.order.paidAt') }}:</strong> {{ formatDate(order.payment.paidAt) }}
</p>
</div>
</div>
<div v-if="order.tracking" class="tracking-section">
<h4 class="section-title">
<el-icon><Van /></el-icon>
{{ t('orderManagement.order.tracking') }}
</h4>
<div class="tracking-info">
<p><strong>{{ t('orderManagement.order.courier') }}:</strong> {{ order.tracking.courier }}</p>
<p><strong>{{ t('orderManagement.order.trackingNumber') }}:</strong> {{ order.tracking.number }}</p>
</div>
</div>
</div>
</el-popover>
</div>
<!-- 卡片操作 -->
<div class="order-card__actions">
<div class="action-buttons">
<el-button size="small" @click="viewDetails">
<el-icon><View /></el-icon>
<el-icon><View />&nbsp;</el-icon>
{{ t('orderManagement.actions.view') }}
</el-button>
<el-button
v-if="order.status === 'pending'"
v-if="statusType.type === 'dzf'"
type="primary"
size="small"
@click="handlePay"
:loading="isPaying"
:disabled="isExpired"
>
<el-icon><CreditCard /></el-icon>
<el-icon><CreditCard />&nbsp;</el-icon>
{{ t('orderManagement.actions.pay') }}
</el-button>
<el-button
v-if="order.status === 'pending'"
v-if="statusType.type === 'dzf'"
type="danger"
size="small"
@click="handleCancel"
:loading="isCancelling"
>
<el-icon><Close /></el-icon>
<el-icon><Close />&nbsp;</el-icon>
{{ t('orderManagement.actions.cancel') }}
</el-button>
<el-button
v-if="order.status === 'shipped'"
v-if="statusType.type === 'yfh'"
type="success"
size="small"
@click="handleConfirm"
@ -165,8 +89,7 @@
<el-icon><Check /></el-icon>
{{ t('orderManagement.actions.confirm') }}
</el-button>
<el-dropdown v-if="moreActions.length > 0" trigger="click">
<!-- <el-dropdown v-if="moreActions.length > 0" trigger="click">
<el-button size="small">
{{ t('orderManagement.actions.more') }}
<el-icon><ArrowDown /></el-icon>
@ -183,7 +106,7 @@
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown> -->
</div>
</div>
</div>
@ -255,19 +178,61 @@ export default {
}
const statusType = computed(() => {
const typeMap = {
pending: 'warning',
paid: 'success',
shipped: 'primary',
completed: 'success',
expired: 'danger'
const payment_statusMap = {
0: ['danger','orderManagement.payment.pending','dzf'],
1: ['success','orderManagement.payment.paid','yzf'],
3: ['danger','orderManagement.payment.failed','zfsb'],
4: ['danger','orderManagement.status.expired','zfgq']
}
return typeMap[props.order.status] || 'info'
})
const statusLabel = computed(() => {
return t(`orderManagement.status.${props.order.status}`)
const order_statusMap = {
0:['warning','orderManagement.status.shenhe','dsh'],
1: ['warning','orderManagement.status.unsuccess','wtg'],
2: ['info','orderManagement.status.clz','clz'],
3: ['info','orderManagement.status.dfh','dfh'],
4: ['info','orderManagement.status.delivered','yfh'],
5: ['success','orderManagement.status.success','ywc'],
6: ['info','orderManagement.status.cancelled','yqx'],
}
const refund_statusMap = {
0:['info','orderManagement.refundStatus.wtk','wtk'],
1: ['info','orderManagement.refundStatus.sqtk','sqtk'],
2: ['warning','orderManagement.refundStatus.jjtk','jjtk'],
3: ['success','orderManagement.refundStatus.tytk','tytk'],
4: ['info','orderManagement.refundStatus.ytk','ytk'],
}
// deo_order.payment_status IS ': 0 1 3 4';
// deo_order.order_status IS ': 0 1 2 3 4 5 6';
// deo_order.refund_status IS '退: 0退 1退 2退 3退 4退';
let status = '';
let label = '';
let type='';
let order_status = props.order.order_status;
let refund_status = props.order.refund_status;
let payment_status = props.order.payment_status;
if(order_status==6){
status = order_statusMap[order_status][0]
label = order_statusMap[order_status][1]
type = order_statusMap[order_status][2]
}if(payment_status!=1 && order_status!=6){
status = payment_statusMap[payment_status][0]
label = payment_statusMap[payment_status][1]
type = payment_statusMap[payment_status][2]
}else if(order_status!=1||order_status!=6){
status = order_statusMap[order_status][0]
label = order_statusMap[order_status][1]
type = order_statusMap[order_status][2]
}else{
status = refund_statusMap[refund_status][0]
label = refund_statusMap[refund_status][1]
type = refund_statusMap[refund_status][2]
}
return {
status:status,
label: t(label),
type:type
};
})
const moreActions = computed(() => {
const actions = []
@ -315,22 +280,22 @@ export default {
}
const viewDetails = () => {
emit('view-details', props.order.id)
emit('view-details', props.order)
}
const handlePay = async () => {
isPaying.value = true
try {
emit('pay-order', props.order.id)
emit('pay-order', props.order)
} finally {
isPaying.value = false
}
}
const handleCancel = async () => {
const handleCancel = async () => {//
isCancelling.value = true
try {
emit('cancel-order', props.order.id)
emit('cancel-order', props.order)
} finally {
isCancelling.value = false
}
@ -457,7 +422,6 @@ export default {
isExpired,
countdownText,
statusType,
statusLabel,
moreActions,
toggleExpand,
formatDate,

View File

@ -4,12 +4,10 @@
<button class="close-button" @click="onClose" :aria-label="$t('common.close') || '关闭'">
<el-icon class="close-icon"><CloseBold /></el-icon>
</button>
<div class="process-header">
<h2 class="title">{{ $t('orderProcess.title') || '定制到家流程' }}</h2>
<p class="subtitle">{{ $t('orderProcess.subtitle') || '了解您的模型从制作到发货的全过程' }}</p>
</div>
<div class="process-timeline">
<div class="timeline-item" v-for="(step, index) in processSteps" :key="index">
<div class="timeline-marker">
@ -22,7 +20,6 @@
<h3 class="step-title">{{ step.title }}</h3>
<p class="step-description">{{ step.description }}</p>
<div class="step-time">{{ step.time }}</div>
<!-- 缩略图显示 -->
<div v-if="step.hasThumbnail" class="step-thumbnail">
<img
@ -35,12 +32,10 @@
</div>
</div>
</div>
<div class="process-note">
<el-icon class="note-icon"><InfoFilled /></el-icon>
<p>{{ $t('orderProcess.note') || '注意以上时间为工作日计算节假日可能会顺延。如有问题请联系客服13121765685' }}</p>
</div>
<div class="actions">
<button class="acknowledge-btn" @click="handleAcknowledge">
{{ $t('orderProcess.acknowledge') || '我已知晓' }}
@ -57,31 +52,19 @@ import { ElIcon } from 'element-plus'
import {
CloseBold,
InfoFilled,
CreditCard,
Document,
Calendar,
Setting,
Picture,
Van
} from '@element-plus/icons-vue'
const { t } = useI18n()
const props = defineProps({
show: { type: Boolean, default: false },
modelData: { type: Object, default: null }
})
const emit = defineEmits(['close', 'acknowledge'])
const handleOverlayClick = () => onClose()
const onClose = () => emit('close')
const handleAcknowledge = () => {
emit('acknowledge', props.modelData)
onClose()
}
//
const processSteps = computed(() => [
{
@ -109,12 +92,12 @@ const processSteps = computed(() => [
time: t('orderProcess.steps.production.time')
},
{
icon: 'Picture',
title: t('orderProcess.steps.inspection.title'),
description: t('orderProcess.steps.inspection.description'),
time: t('orderProcess.steps.inspection.time'),
hasThumbnail: true
},
icon: 'Picture',
title: t('orderProcess.steps.inspection.title'),
description: t('orderProcess.steps.inspection.description'),
time: t('orderProcess.steps.inspection.time'),
hasThumbnail: true
},
{
icon: 'Van',
title: t('orderProcess.steps.shipping.title'),

View File

@ -8,12 +8,12 @@
<!-- Hero Section -->
<section class="hero-section">
<div class="hero-image">
<img :src="heroImage" alt="Custom Models" />
<img :src="props.modelData.imageUrl" alt="Custom Models" />
</div>
<div class="product-info">
<h1 class="product-title">{{ $t('checkout.customModel') }}</h1>
<div class="price-info">
<span class="price">{{ $t('checkout.from') }} ${{ (amountCents / 100).toFixed(2) }}</span>
<span class="price">{{ $t('checkout.from') }} ${{ (amountCents ).toFixed(2) }}</span>
</div>
</div>
</section>
@ -24,16 +24,9 @@
<!-- 产品配置区域 -->
<div class="content-block">
<h2 class="block-title">
<el-icon class="title-icon"><Setting /></el-icon>
{{ $t('checkout.configuration') }}
</h2>
<div class="config-grid">
<div class="config-item">
<div class="label">{{ $t('checkout.size') }}</div>
<div class="chip-group">
<button v-for="s in sizeOptions" :key="s" :class="['chip', { active: size===s }]" @click="size=s">{{ s }}</button>
</div>
</div>
<div class="config-item quantity">
<div class="label">{{ $t('checkout.quantity') }}</div>
<div class="qty-container">
@ -42,6 +35,14 @@
<button class="qty-btn" @click="incQty"></button>
</div>
</div>
<div class="config-item ip-name">
<div class="label">{{ $t('checkout.ipName') }}</div>
<el-input
v-model="ipName"
:placeholder="$t('checkout.ipNamePlaceholder')"
class="ip-name-input"
/>
</div>
</div>
</div>
@ -50,10 +51,9 @@
<!-- 联系信息 -->
<div class="info-column">
<h2 class="block-title">
<el-icon class="title-icon"><User /></el-icon>
{{ $t('checkout.contact') }}
</h2>
<el-form :model="contact" class="contact-form">
<el-form class="contact-form">
<el-form-item :label="$t('checkout.emailOrPhone')">
<el-input v-model="contact.emailOrPhone" :placeholder="$t('checkout.emailOrPhone')" />
</el-form-item>
@ -77,7 +77,6 @@
<!-- 配送信息 -->
<div class="info-column">
<h2 class="block-title">
<el-icon class="title-icon"><Location /></el-icon>
{{ $t('checkout.shipping') }}
</h2>
<el-form :model="shipping" label-width="auto" class="shipping-form">
@ -141,7 +140,7 @@
<!-- Action Buttons -->
<div class="action-area">
<button class="buy-btn" @click="goShopify">{{ $t('checkout.buy') }}</button>
<button class="buy-btn" :disabled="isPayButtonDisabled" @click="goShopify">{{ $t('checkout.buy') }}</button>
</div>
<!-- Stripe Payment Overlay -->
@ -167,24 +166,20 @@
import { ref, computed, onMounted, watch } from 'vue'
import { ElIcon } from 'element-plus'
import { CloseBold, Search } from '@element-plus/icons-vue'
import heroImage from '@/assets/demo4.png'
import StripePaymentForm from '@/components/StripePaymentForm.vue'
import { Country, State } from 'country-state-city'
import { useI18n } from 'vue-i18n'
import { PayServer } from '@deotaland/utils'
const payserver = new PayServer();
const props = defineProps({
show: { type: Boolean, default: false },
template: { type: String, default: '' },
id: { type: String, default: '' },
modelUrl: { type: String, default: '' }
modelData: { type: Object, default: () => ({}) },
show: { type: Boolean, default: false }
})
const emit = defineEmits(['close'])
const onClose = () => emit('close')
const sizeOptions = ['3cm','4cm','5cm','6cm','7cm','8cm','10cm']
const size = ref(sizeOptions[0])
const addons = ref({ matte:false, gloss:false, base:false })
const qty = ref(1)
const ipName = ref('')
const contact = ref({ emailOrPhone:'', subscribe:false })
const shipping = ref({ country:'US', firstName:'', lastName:'', postalCode:'', state:'', city:'', address1:'', address2:'', phone:'', saveInfo:false })
const countryOptions = ref([])
@ -194,53 +189,65 @@ const { locale } = useI18n()
//
const amountCents = computed(() => {
const sizePricing = { '3cm':1099,'4cm':1299,'5cm':1499,'6cm':1699,'7cm':1899,'8cm':2099,'10cm':2499 }
let base = sizePricing[size.value] || 1099
let base = 299 //
if (addons.value.gloss) base += 300
if (addons.value.base) base += 400
if (addons.value.matte) base += 200
return base * qty.value
})
const placedDate = computed(() => new Date().toLocaleDateString())
const shippedRange = computed(() => {
const start = new Date(); start.setDate(start.getDate()+7)
const end = new Date(); end.setDate(end.getDate()+11)
return `${start.toLocaleDateString()}${end.toLocaleDateString()}`
//
const isPayButtonDisabled = computed(() => {
return !(
shipping.value.firstName.trim() &&
shipping.value.lastName.trim() &&
shipping.value.postalCode.trim() &&
shipping.value.state.trim() &&
shipping.value.city.trim() &&
shipping.value.address1.trim() &&
shipping.value.phone.trim() &&
contact.value.emailOrPhone.trim() &&
ipName.value.trim()
)
})
const deliveredRange = computed(() => {
const start = new Date(); start.setDate(start.getDate()+14)
const end = new Date(); end.setDate(end.getDate()+25)
return `${start.toLocaleDateString()}${end.toLocaleDateString()}`
})
const incQty = () => { qty.value = Math.min(qty.value+1, 99) }
const decQty = () => { qty.value = Math.max(qty.value-1, 1) }
const goShopify = () => {
const base = 'https://tripo3d.myshopify.com/zh/products/custom-resin-3d-printed-figurine'
const params = new URLSearchParams()
if (props.template) params.set('template', props.template)
if (props.id) params.set('ref', `model-${props.id}`)
params.set('size', size.value)
params.set('qty', String(qty.value))
const addOns = Object.keys(addons.value).filter(k => addons.value[k])
if (addOns.length) params.set('addons', addOns.join(','))
if (contact.value.emailOrPhone) params.set('contact', contact.value.emailOrPhone)
if (contact.value.subscribe) params.set('subscribe', '1')
params.set('country', shipping.value.country)
if (shipping.value.lastName) params.set('lastName', shipping.value.lastName)
if (shipping.value.firstName) params.set('firstName', shipping.value.firstName)
if (shipping.value.postalCode) params.set('postal', shipping.value.postalCode)
if (shipping.value.state) params.set('state', shipping.value.state)
if (shipping.value.city) params.set('city', shipping.value.city)
if (shipping.value.address1) params.set('address1', shipping.value.address1)
if (shipping.value.address2) params.set('address2', shipping.value.address2)
if (shipping.value.phone) params.set('phone', shipping.value.phone)
if (shipping.value.saveInfo) params.set('save', '1')
const url = `${base}?${params.toString()}`
try { navigator.clipboard.writeText(String(props.id || '')) } catch (e) {}
showStripe.value = true
const goShopify = () => {//
const project_details = {
imageUrl:props.modelData.imageUrl,
modelUrl:props.modelData.modelUrl,
projectId:props.modelData.projectId,
}
//
const order_info = {
quantity: qty.value,
ipName: ipName.value,
contact: {
emailOrPhone: contact.value.emailOrPhone
},
shipping: {
country: shipping.value.country,
countryLabel: countryOptions.value.find(c => c.value === shipping.value.country)?.label || shipping.value.country,
firstName: shipping.value.firstName,
lastName: shipping.value.lastName,
postalCode: shipping.value.postalCode,
state: shipping.value.state,
stateLabel: stateOptions.value.find(s => s.value === shipping.value.state)?.label || shipping.value.state,
city: shipping.value.city,
address1: shipping.value.address1,
address2: shipping.value.address2,
phone: shipping.value.phone
},
modelData:project_details
}
let params ={
quantity:qty.value,
project_id:props.modelData.projectId,
project_details:project_details,
order_info:order_info,
}
//
payserver.createPayorOrder(params)
}
const saveLocal = () => {
@ -534,7 +541,7 @@ const updateStates = () => {
/* Configuration Grid */
.config-grid {
display: grid;
grid-template-columns: 1fr 200px;
grid-template-columns: 200px 1fr;
gap: 24px;
align-items: start;
}
@ -546,7 +553,27 @@ const updateStates = () => {
}
.config-item.quantity {
align-items: center;
align-items: flex-start;
}
.config-item.ip-name {
align-items: flex-start;
}
.ip-name-input {
width: 100%;
max-width: 300px;
}
.ip-name-input :deep(.el-input__wrapper) {
background: rgba(17,24,39,0.8);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
border-radius: 8px;
}
.ip-name-input :deep(.el-input__inner) {
color: #ffffff;
}
/* Info Row Layout */
@ -854,13 +881,13 @@ const updateStates = () => {
justify-content: center;
}
.buy-btn {
height: 48px;
padding: 0 32px;
border-radius: 12px;
border: 1px solid rgba(139,92,246,0.6);
background: linear-gradient(135deg, rgba(139,92,246,0.8), rgba(59,130,246,0.8));
color: #fff;
.buy-btn {
height: 48px;
padding: 0 32px;
border-radius: 12px;
border: 1px solid rgba(139,92,246,0.6);
background: linear-gradient(135deg, rgba(139,92,246,0.8), rgba(59,130,246,0.8));
color: #fff;
cursor: pointer;
font-size: 16px;
font-weight: 600;
@ -878,6 +905,21 @@ const updateStates = () => {
transform: translateY(0);
}
.buy-btn:disabled {
opacity: 0.6;
background: linear-gradient(135deg, rgba(75,85,99,0.8), rgba(55,65,81,0.8));
border-color: rgba(75,85,99,0.6);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.buy-btn:disabled:hover {
transform: none;
box-shadow: none;
background: linear-gradient(135deg, rgba(75,85,99,0.8), rgba(55,65,81,0.8));
}
/* Stripe Payment Overlay */
.stripe-overlay {
position: fixed; inset: 0;
@ -910,6 +952,10 @@ const updateStates = () => {
gap: 20px;
}
.ip-name-input {
max-width: 100%;
}
.info-row {
grid-template-columns: 1fr;
gap: 20px;

View File

@ -2,7 +2,6 @@
<div class="stripe-payment-form">
<div id="card-element" class="stripe-element"></div>
<div id="card-errors" class="stripe-errors" role="alert"></div>
<!-- 支付信息 -->
<div class="payment-info">
<div class="order-summary">
@ -27,28 +26,6 @@
</div>
</div>
</div>
<!-- 优惠券 -->
<div class="coupon-section">
<el-input
v-model="couponCode"
:placeholder="$t('payment.couponPlaceholder')"
size="small"
clearable
>
<template #append>
<el-button
@click="applyCoupon"
:loading="applyingCoupon"
size="small"
>
{{ $t('payment.applyCoupon') }}
</el-button>
</template>
</el-input>
<p v-if="couponError" class="coupon-error">{{ couponError }}</p>
<p v-if="couponSuccess" class="coupon-success">{{ couponSuccess }}</p>
</div>
</div>
<!-- 支付按钮 -->
@ -86,27 +63,25 @@ import { ElMessage, ElLoading } from 'element-plus'
import { CreditCard, Lock } from '@element-plus/icons-vue'
import { loadStripe } from '@stripe/stripe-js'
import { useI18n } from 'vue-i18n'
import { PayServer } from '@deotaland/utils'
// Stripe
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51SRnUTG9Oq8PDokQxhKzpPYaf5rTFR5OZ8QqTkGVtL9YUwTZFgU4ipN42Lub6NEYjXRvcIx8hvAvJGkKskDQ0pf9003uZhrC9Y'
const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const { t } = useI18n()
// Props
const props = defineProps({
amount: {
type: Number,
required: true //
},
currency: {
currency: {//
type: String,
default: 'usd'
},
orderId: {
orderId: {// ID
type: String,
required: true
},
customerEmail: {
customerEmail: {//
type: String,
default: ''
}
@ -132,76 +107,21 @@ const shippingAmount = ref(0)
// Stripe
//
const canPay = computed(() => !!stripe.value && !!cardElement.value && !processing.value)
const canPay = computed(() => !processing.value)
const finalAmount = computed(() => {
return props.amount + taxAmount.value + shippingAmount.value - discountAmount.value
})
const payServer = new PayServer()
// Stripe
const initializeStripe = async () => {
try {
stripe.value = await loadStripe(STRIPE_PUBLISHABLE_KEY)
if (!stripe.value) {
throw new Error('Failed to load Stripe')
}
//
const isDarkMode = document.documentElement.classList.contains('dark')
elements.value = stripe.value.elements({
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#6B46C1',
colorBackground: isDarkMode ? '#1f2937' : '#ffffff',
colorText: isDarkMode ? '#f9fafb' : '#1f2937',
colorDanger: '#ef4444',
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
spacingUnit: '4px',
borderRadius: '6px'
},
rules: {
'.Input': {
backgroundColor: isDarkMode ? '#374151' : '#ffffff',
borderColor: isDarkMode ? '#4b5563' : '#e5e7eb',
color: isDarkMode ? '#f9fafb' : '#1f2937'
},
'.Input:hover': {
borderColor: '#6B46C1'
},
'.Input:focus': {
borderColor: '#6B46C1',
boxShadow: '0 0 0 1px #6B46C1'
},
'.Label': {
color: isDarkMode ? '#d1d5db' : '#4b5563'
},
'.Error': {
color: '#ef4444'
}
}
}
const data = await payServer.createPaymentIntent(finalAmount.value, props.currency, {
order_id: props.orderId,
customer_email: props.customerEmail,
})
//
cardElement.value = elements.value.create('card', {
style: {
base: {
fontSize: '16px',
color: isDarkMode ? '#f9fafb' : '#424770',
'::placeholder': {
color: isDarkMode ? '#9ca3af' : '#aab7c4',
},
iconColor: isDarkMode ? '#9ca3af' : '#666ee1'
},
invalid: {
color: '#9e2146',
},
},
})
elements.value = data.element
cardElement.value = data.cardElement
//
cardElement.value.on('change', ({ error }) => {
const displayError = document.getElementById('card-errors')
@ -282,44 +202,24 @@ const applyCoupon = async () => {
//
const processPayment = async () => {
if (!stripe.value || !elements.value) {
if ( !elements.value) {
ElMessage.error(t('payment.stripeNotInitialized'))
return
}
processing.value = true
const loading = ElLoading.service({
lock: true,
text: t('payment.processing'),
background: 'rgba(0, 0, 0, 0.7)'
})
try {
let paymentMethod = null
const { error, paymentMethod: pm } = await stripe.value.createPaymentMethod({
type: 'card',
card: cardElement.value,
billing_details: { email: props.customerEmail }
})
if (error) throw new Error(error.message)
paymentMethod = pm
//
await new Promise(resolve => setTimeout(resolve, 2000))
// 90%
if (Math.random() > 0.1) {
const { paymentIntent } = await payServer.confirmPaymentIntent(elements.value, props.customerEmail)
if(paymentIntent.status === 'succeeded'){
ElMessage.success(t('payment.success'))
emit('payment-success', {
paymentMethodId: paymentMethod?.id,
orderId: props.orderId,
amount: finalAmount.value,
currency: props.currency
paymentIntent: paymentIntent,
})
} else {
throw new Error(t('payment.declined'))
}
} catch (error) {
console.error('Payment error:', error)
ElMessage.error(error.message || t('payment.failure'))
@ -342,7 +242,13 @@ onMounted(() => {
})
onUnmounted(() => {
if (cardElement.value) cardElement.value.destroy()
if (cardElement.value) {
cardElement.value.destroy()
cardElement.value = null
}
if (elements.value) {
elements.value = null
}
})
//

View File

@ -1,12 +1,5 @@
<template>
<aside class="floating-sidebar">
<!-- 基础模型选择 -->
<!-- <div class="form-section">
<label class="section-label">Base Model</label>
<select class="model-select" v-model="selectedModel">
<option>General Model v1.0</option>
</select>
</div> -->
<!-- IP类型选择 -->
<div class="form-section" v-if="false">
<label class="section-label">IP类型</label>
@ -29,15 +22,6 @@
</div>
</div>
</div>
<!-- 角色导入 -->
<!-- <div class="form-section">
<label class="section-label">角色导入</label>
<div>
<el-button class="import-btn" type="primary" @click="openCharacterImport">角色导入</el-button>
</div>
</div> -->
<!-- 表情选择 -->
<div class="form-section" v-if="false">
<!-- <label class="section-label">表情选择</label> -->
@ -62,63 +46,19 @@
</div>
</div>
</div>
<!-- 发色选择 -->
<!-- <div class="form-section" v-if="ipType === '人物'">
<label class="section-label">发色选择</label>
<div class="hair-color-info">
<span class="hair-color-description">
选择角色的发色
</span>
</div>
<div class="hair-color-grid">
<div
v-for="color in hairColors"
:key="color.id"
class="hair-color-card"
:class="{ active: selectedHairColor?.id === color.id }"
@click="handleHairColorSelect(color)"
>
<div class="hair-color-preview" :style="{ backgroundColor: color.hex }">
<div v-if="selectedHairColor?.id === color.id" class="hair-color-selected"></div>
</div>
<div class="hair-color-name">{{ color.name }}</div>
</div>
</div>
</div> -->
<!-- 肤色选择 -->
<!-- <div class="form-section" v-if="ipType === '人物'">
<label class="section-label">肤色选择</label>
<div class="skin-color-info">
<span class="skin-color-description">
选择角色的肤色
</span>
</div>
<div class="skin-color-grid">
<div
v-for="color in skinColors"
:key="color.id"
class="skin-color-card"
:class="{ active: selectedSkinColor?.id === color.id }"
@click="handleSkinColorSelect(color)"
>
<div class="skin-color-preview" :style="{ backgroundColor: color.hex }">
<div v-if="selectedSkinColor?.id === color.id" class="skin-color-selected"></div>
</div>
<div class="skin-color-name">{{ color.name }}</div>
</div>
</div>
</div> -->
<!-- 文本提示输入 -->
<div class="form-section" v-if="false">
<label class="section-label">文本提示</label>
<div class="form-section">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.textPrompt') }}
</span>
</div>
<div class="prompt-input-container">
<div class="textarea-wrapper">
<textarea
class="prompt-input custom-scrollbar"
placeholder="请描述您想要创建的角色形象..."
v-model="prompt"
:placeholder="$t('iPandCardLeft.placeholder.characterDescription')"
v-model="formData.prompt"
@input="autoResizeTextarea"
ref="textareaRef"
:disabled="isOptimizing"
@ -127,13 +67,13 @@
<div v-if="isOptimizing" class="scan-overlay">
<div class="scan-line"></div>
</div>
<button
<!-- <button
class="optimizer-btn"
@click="handleOptimizePrompt"
:disabled="isOptimizing || !prompt.trim()"
>
<span class="btn-icon">🪄</span>
</button>
</button> -->
</div>
</div>
</div>
@ -141,7 +81,7 @@
<div class="form-section">
<div class="expression-info">
<span class="expression-description">
添加参考图片
{{ $t('iPandCardLeft.addReferenceImage') }}
</span>
</div>
<div
@ -154,15 +94,15 @@
:class="{ 'drag-over': isDragOver }"
>
<!-- 图片预览 -->
<div v-if="previewImage" class="image-preview">
<img :src="previewImage" alt="Selected image">
<div v-if="formData.previewImage" class="image-preview">
<img :src="formData.previewImage" alt="Selected image">
<button class="remove-image" @click.stop="removeImage">×</button>
</div>
<!-- 上传提示 -->
<div v-else class="upload-prompt">
<div class="upload-icon">+</div>
<span class="upload-text">上传或选择一张图片</span>
<span class="drag-text">或拖拽图片到此处</span>
<span class="upload-text">{{ $t('iPandCardLeft.uploadOrSelectImage') }}</span>
<span class="drag-text">{{ $t('iPandCardLeft.dragImageHere') }}</span>
</div>
<!-- 隐藏的文件输入 -->
<input
@ -176,11 +116,11 @@
</div>
<!-- 材质选择 -->
<div class="form-section" v-if="false">
<label class="section-label">材质选择</label>
<label class="section-label">{{ $t('iPandCardLeft.material.title') }}</label>
<div class="material-selection">
<div class="material-info">
<span class="material-description">
选择材质来提升您的设计质感
{{ $t('iPandCardLeft.material.description') }}
</span>
</div>
<div class="material-grid">
@ -209,11 +149,11 @@
</div>
<!-- 颜色选择 -->
<div class="form-section" v-if="selectedMaterial">
<label class="section-label">颜色选择</label>
<label class="section-label">{{ $t('iPandCardLeft.color.title') }}</label>
<div class="color-selection">
<div class="color-info">
<span class="color-description">
为您的{{ selectedMaterial.name }}材质选择一种颜色
{{ $t('iPandCardLeft.color.description', { material: selectedMaterial.name }) }}
</span>
</div>
<div class="color-grid">
@ -237,7 +177,7 @@
</div>
<!-- 电子模块选择 -->
<div class="form-section" v-if="false">
<label class="section-label">电子模块</label>
<label class="section-label">{{ $t('iPandCardLeft.electronicModule') }}</label>
<div class="module-selection">
<div class="module-grid">
<div
@ -256,14 +196,13 @@
</div>
</div>
</div>
<!-- 草图选择系统 -->
<div class="form-section" v-if="selectedModule">
<label class="section-label">草图选择</label>
<label class="section-label">{{ $t('iPandCardLeft.sketch.title') }}</label>
<div class="sketch-selection">
<div class="sketch-info">
<span class="sketch-description">
选择与您的{{ selectedModule.name }}模块相匹配的草图
{{ $t('iPandCardLeft.sketch.description', { module: selectedModule.name }) }}
</span>
</div>
<div class="sketch-grid">
@ -286,22 +225,6 @@
</div>
</div>
</div>
<!-- 创意风格选择 -->
<!-- <div class="form-section">
<label class="section-label">Creative Style</label>
<div class="style-options">
<button
v-for="style in styles"
:key="style"
class="style-option"
:class="{ active: selectedStyle === style }"
@click="handleStyleSelect(style)"
>
{{ style }}
</button>
</div>
</div> -->
<!-- 图片数量选择 -->
<div class="form-section" v-if="false">
<div class="expression-info">
@ -333,34 +256,31 @@
<script setup>
import lts from '../../assets/sketches/lts.png'
import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance } from 'vue';
import { optimizePrompt, } from '../../services/aiService.js';
import { ElMessage, ElLoading } from 'element-plus';
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import cz1 from '../../assets/material/cz1.jpg'
import humanTypeImg from '../../assets/sketches/tcww.png'
import animalTypeImg from '../../assets/sketches/dwww.png'
import { useRouter } from 'vue-router';
//
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back']);
const router = useRouter();
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back', 'updateProjectInfo']);
const props = defineProps({
Info: {
type: Object,
default: () => ({})
}
})
// i18n
const { proxy } = getCurrentInstance();
const $t = proxy.$t;
const openCharacterImport = () => {
emit('import-character');
};
const formData = ref({
prompt: '',
previewImage:'',
})
const prompt = ref(``); //
const selectedStyle = ref('General');
const styles = ref(['通用', '动漫', '写实', '赛博朋克', '国风', '像素风']);
const textareaRef = ref(null);
const previewImage = ref(''); // URL
const referenceImage = ref(''); // 3DURL
const fileInput = ref(null); //
const generateCount = ref(4); // 1
const generateCount = ref(1); // 1
const isOptimizing = ref(false); //
const isDragOver = ref(false); //
// IP/
@ -386,74 +306,13 @@ const selectedSkinColor = ref(null); // 选中的肤色
// assets/email
const expressions = ref([]);
const loadExpressions = () => {
// Vite import.meta.glob email
const modules = import.meta.glob('../../assets/email/*.png', { eager: true });
// 使 <>
const nameMap = {
email1: '开心',
email2: '惊讶',
email3: '微笑',
email4: '大笑',
email5: '调皮',
email6: '酷',
email7: '害羞',
email8: '生气',
email9: '思考',
email10: '爱心'
};
const list = Object.entries(modules).map(([path, mod]) => {
const filenameWithExt = path.split('/').pop();
const filename = filenameWithExt ? filenameWithExt.replace(/\.[^.]+$/, '') : 'emoji';
const match = filename.match(/email(\d+)/);
const num = match ? Number(match[1]) : null;
const name = nameMap[filename] || (num ? `表情 ${num}` : filename);
const imageUrl = mod && (mod.default || mod);
return {
id: filename,
name,
imageUrl
};
}).sort((a, b) => {
const na = Number(a.id.match(/email(\d+)/)?.[1] ?? 0);
const nb = Number(b.id.match(/email(\d+)/)?.[1] ?? 0);
return na - nb;
});
expressions.value = list;
};
const init = () => {
}
onMounted(() => {
loadExpressions();
init()
// loadExpressions();
});
//
const hairColors = ref([
{ id: 'black', name: '黑色', hex: '#000000' },
{ id: 'brown', name: '棕色', hex: '#8B4513' },
{ id: 'blonde', name: '金色', hex: '#FFD700' },
{ id: 'red', name: '红色', hex: '#B22222' },
{ id: 'gray', name: '灰色', hex: '#808080' },
{ id: 'white', name: '白色', hex: '#F5F5F5' },
{ id: 'blue', name: '蓝色', hex: '#4169E1' },
{ id: 'green', name: '绿色', hex: '#228B22' },
{ id: 'purple', name: '紫色', hex: '#9370DB' },
{ id: 'pink', name: '粉色', hex: '#FFC0CB' }
]);
//
const skinColors = ref([
{ id: 'fair', name: '白皙', hex: '#FDBCB4' },
{ id: 'light', name: '浅色', hex: '#F1C27D' },
{ id: 'medium', name: '中等', hex: '#E0AC69' },
{ id: 'olive', name: '橄榄色', hex: '#C68642' },
{ id: 'tan', name: '古铜色', hex: '#8D5524' },
{ id: 'brown', name: '深棕色', hex: '#6B4423' },
{ id: 'dark', name: '深色', hex: '#4A2C17' }
]);
// -
const colorDatabase = ref({
metal_brushed: [
@ -638,11 +497,7 @@ const availableColors = computed(() => {
return colorDatabase.value[selectedMaterial.value.id] || [];
});
// - React
const internalError = ref(''); //
const characterProfile = ref(null); //
const userPrompt = ref(''); //
//
const handleMaterialSelect = (material) => {
//
@ -690,25 +545,7 @@ const handleExpressionSelect = (expression) => {
}
};
//
const handleHairColorSelect = (color) => {
//
if (selectedHairColor.value?.id === color.id) {
selectedHairColor.value = null;
} else {
selectedHairColor.value = color;
}
};
//
const handleSkinColorSelect = (color) => {
//
if (selectedSkinColor.value?.id === color.id) {
selectedSkinColor.value = null;
} else {
selectedSkinColor.value = color;
}
};
//
const handleSketchSelect = (sketch) => {
@ -725,7 +562,7 @@ const validateGenerationInputs = () => {
const errors = []
const warnings = []
// -
if (!referenceImage.value && !selectedSketch.value) {
if (!(formData.value.previewImage || formData.value.prompt)) {
errors.push($t('common.validation.referenceImageRequired'))
}
return { errors, warnings }
@ -771,89 +608,7 @@ const handleProcessingError = (error, context = '') => {
})
}
}
// - ReactPromptStep.tsx
const handleOptimizePrompt = async () => {
// - React if (!prompt.trim())
if (!validatePromptInput(prompt.value)) {
handleValidationError(['请先输入要优化的文本内容']);
return;
}
//
if (!validateImageInput(previewImage.value)) {
handleValidationError(['上传的图片格式无效']);
return;
}
// - React setLoading(true); setError(null);
isOptimizing.value = true;
internalError.value = '';
try {
// AI - React const profile = await optimizePrompt(prompt, imagePreview);
const characterProfile = await optimizePrompt(prompt.value, previewImage.value);
console.log(characterProfile,'响应格式');
// -
if (characterProfile) {
// - React setCharacterProfile(profile);
// 使CharacterProfile
const newCharacterProfile = {
name: sanitizeText(characterProfile.name || ''),
gender: sanitizeText(characterProfile.gender || ''),
appearance: sanitizeText(characterProfile.appearance || ''),
personality: sanitizeText(characterProfile.personality || ''),
backstory: sanitizeText(characterProfile.backstory || ''),
skills: Array.isArray(characterProfile.skills) ? characterProfile.skills : [],
birthday: sanitizeText(characterProfile.birthday || ''),
likes: Array.isArray(characterProfile.likes) ? characterProfile.likes : []
};
//
if (validateCharacterProfile(newCharacterProfile)) {
characterProfile.value = newCharacterProfile;
} else {
console.warn('字符档案验证失败,使用默认处理');
}
// - React setUserPrompt(prompt);
userPrompt.value = prompt.value;
// -
try {
// CharacterProfile
const formattedContent = formatCharacterProfileToText(newCharacterProfile);
if (formattedContent) {
prompt.value = formattedContent;
} else {
console.warn('格式化内容为空,保持原始输入');
}
} catch (formatError) {
handleProcessingError(formatError, '内容格式化');
//
}
//
if (textareaRef.value) {
try {
textareaRef.value.style.height = 'auto';
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px';
} catch (resizeError) {
console.warn('文本框高度调整失败:', resizeError);
}
}
} else {
handleValidationError(['优化服务返回空结果']);
}
} catch (error) {
// - React catch
handleProcessingError(error, '优化提示词');
//
resetResponseData();
} finally {
// - React finally { setLoading(false); }
isOptimizing.value = false;
}
};
//
const handleStyleSelect = (style) => {
selectedStyle.value = style;
};
//
const handleQuantitySelect = (num) => {
@ -890,118 +645,22 @@ const handleGenerateWithMultipleImages = async () => {
try {
//
const profile = {
name: characterProfile.value?.name || '角色',
appearance: characterProfile.value?.appearance || prompt.value,
personality: characterProfile.value?.personality || '独特个性',
sketch: selectedSketch.value
appearance: formData.value.prompt,
};
//
const params = {
profile: profile,
style: selectedStyle.value,
inspirationImage: previewImage.value,
inspirationImage: formData.value.previewImage,
count: generateCount.value,
sketch: selectedSketch.value,
selectedMaterial:selectedMaterial.value,
selectedColor:selectedColor.value,
selectedExpression: selectedExpression.value,
selectedHairColor: selectedHairColor.value,
selectedSkinColor: selectedSkinColor.value,
ipType:ipType.value,
ipTypeImg:ipTypeImages[ipType.value]||'',
}
emit('generate-requested', params);
} catch (error) {
console.error('创建多图像角色档案失败:', error);
handleProcessingError(error, '多图像角色档案创建');
}
};
//
const sanitizeText = (text) => {
if (typeof text !== 'string') {
return String(text || '');
}
// XSS
return text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
};
const validatePromptInput = (input) => {
if (!input || typeof input !== 'string') {
return false;
}
return input.trim().length > 0;
};
const validateImageInput = (imageData) => {
if (!imageData) {
return true; //
}
// base64URL
return typeof imageData === 'string' && imageData.length > 0;
};
const validateCharacterProfile = (profile) => {
if (!profile || typeof profile !== 'object') {
return false;
}
//
const requiredFields = ['name', 'appearance', 'personality'];
return requiredFields.some(field => profile[field] && profile[field].trim().length > 0);
};
const formatCharacterProfileToText = (profile) => {
if (!profile || typeof profile !== 'object') {
return '';
}
const sections = [];
if (profile.name) {
sections.push(`角色名称: ${profile.name}`);
}
if (profile.gender) {
sections.push(`性别: ${profile.gender}`);
}
if (profile.appearance) {
sections.push(`外观描述: ${profile.appearance}`);
}
if (profile.personality) {
sections.push(`性格特征: ${profile.personality}`);
}
if (profile.backstory) {
sections.push(`背景故事: ${profile.backstory}`);
}
if (profile.skills) {
sections.push(`技能特长: ${profile.skills}`);
}
if (profile.birthday) {
sections.push(`生日: ${profile.birthday}`);
}
if (profile.likes) {
sections.push(`喜好: ${profile.likes}`);
}
return sections.join('\n\n');
};
const resetResponseData = () => {
//
characterProfile.value = null;
userPrompt.value = '';
internalError.value = '';
};
// textarea
const autoResizeTextarea = () => {
if (textareaRef.value) {
@ -1025,12 +684,10 @@ const autoResizeTextarea = () => {
}
}
};
//
const triggerFileUpload = () => {
fileInput.value?.click();
};
//
const handleFileChange = async (event) => {
const file = event.target.files?.[0];
@ -1039,8 +696,7 @@ const handleFileChange = async (event) => {
// URL
const reader = new FileReader();
reader.onload = (e) => {
previewImage.value = e.target.result;
referenceImage.value = e.target.result; // referenceImage
formData.value.previewImage = e.target.result;
};
reader.readAsDataURL(file);
}
@ -1048,8 +704,7 @@ const handleFileChange = async (event) => {
//
const removeImage = () => {
previewImage.value = '';
referenceImage.value = '';
formData.value.previewImage = '';
if (fileInput.value) {
fileInput.value.value = ''; //
}
@ -1090,8 +745,7 @@ const handleDrop = (event) => {
// URL
const reader = new FileReader();
reader.onload = (e) => {
previewImage.value = e.target.result;
referenceImage.value = e.target.result; // referenceImage
formData.value.previewImage = e.target.result;
};
reader.readAsDataURL(file);
} else {
@ -1100,7 +754,7 @@ const handleDrop = (event) => {
};
// prompt textarea
watch(prompt, () => {
watch(() => formData.value.prompt, () => {
nextTick(() => {
autoResizeTextarea();
});

View File

@ -9,7 +9,6 @@
>
<MenuIcon :class="{ 'icon-active': sidebarVisible }" />
</button>
<!-- 品牌标识区域 -->
<div class="brand-section">
<router-link to="/" class="brand-link">
@ -21,7 +20,6 @@
</div>
</router-link>
</div>
<!-- 功能区域 -->
<div class="actions-section">
<!-- 移动端隐藏的操作按钮 -->
@ -34,7 +32,6 @@
>
<SearchIcon />
</button> -->
<!-- 通知按钮 -->
<!-- <button
class="action-button notification-button"
@ -46,7 +43,6 @@
{{ notificationCount }}
</span>
</button> -->
<!-- 用户菜单 -->
<div class="user-menu" v-if="currentUser">
<el-dropdown trigger="click" @command="handleUserCommand">

View File

@ -69,7 +69,7 @@ export default {
'agent-management': { label: 'sidebar.agentManagement', icon: null },
'add-agent': { label: 'agentManagement.createAgent', icon: null },
'device-settings': { label: 'sidebar.deviceSettings', icon: null },
'create-project': { label: 'home.createProject.title', icon: null },
'project': { label: 'home.createProject.title', icon: null },
'model-purchase': { label: 'breadcrumb.modelPurchase', icon: null },
'list': { label: 'list.title', icon: null },
'login': { label: 'breadcrumb.login', icon: null },

View File

@ -12,9 +12,9 @@
:style="cardStyle"
>
<!-- 如果没有模型URL但有图片URL显示图片 -->
<div v-if="!currentModelUrl && imageUrl" class="image-preview">
<div v-if="!currentModelUrl && props.cardData.imageUrl" class="image-preview">
<img
:src="imageUrl"
:src="props.cardData.imageUrl"
:alt="altText"
class="preview-image"
@load="handleImageLoad"
@ -34,20 +34,13 @@
<span>正在生成模型...</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }">
<span
class="progress-text"
:class="{ 'low-progress': progressPercentage < 15 }"
v-show="progressPercentage >= 8"
>
{{ Math.round(progressPercentage) }}%
</span>
</div>
<span
<div
class="progress-background-text"
v-show="progressPercentage < 8"
>
{{ Math.round(progressPercentage) }}%
</span>
</div>
</div>
</div>
</div>
@ -115,7 +108,7 @@ import { computed, ref, onMounted } from 'vue';
import ThreeModelViewer from '../ThreeModelViewer/index.vue';
import { MeshyServer } from '@deotaland/utils';
//
const emit = defineEmits(['clickModel','refineModel']);
const emit = defineEmits(['clickModel','refineModel','save-project']);
const Meshy = new MeshyServer();
//
const showRightControls = ref(false);
@ -129,29 +122,54 @@ const handleCardClick = () => {
// URL
const modelData = {
modelUrl: currentModelUrl.value,
imageUrl: props.imageUrl,
imageUrl: props.cardData.imageUrl,
projectId:props.projectId,
altText: props.altText,
cardId: props.cardId,
cardWidth: props.cardWidth
};
emit('clickModel', modelData);
};
const modelData = ref({});
//
const handleGenerateModel = async () => {
isGenerating.value = true;
progressPercentage.value = 0;
Meshy.createModelTask({
project_id: 0,
image_url: props.imageUrl,
progressPercentage.value = 10;
let result;
if(props.cardData.taskId){
result = props.cardData.taskId;
TaskStatus(result);
return
}
Meshy.createModelTask({
project_id: props.cardData.project_id,
image_url: props.cardData.imageUrl,
},(result)=>{
if(result){
Meshy.getModelTaskStatus(result,(modelUrl)=>{
emit('save-project',{
...props.cardData,
taskId:result
});
TaskStatus(result);
}
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
},{})
};
const TaskStatus = (result)=>{
Meshy.getModelTaskStatus(result,(modelUrl)=>{
if(modelUrl){
//
generatedModelUrl.value = modelUrl;
isGenerating.value = false;
progressPercentage.value = 100;
emit('save-project',{
...props.cardData,
modelUrl:modelUrl,
taskId:result,
status:'success'
});
}
},(error)=>{
console.error('模型生成失败:', error);
@ -162,19 +180,22 @@ const handleGenerateModel = async () => {
progressPercentage.value = progress;
}
})
}
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
},{})
};
}
// URLURL
onMounted(() => {
if(false){//
return
switch (props.cardData.status) {
case 'loading':
handleGenerateModel();
break;
case 'success':
generatedModelUrl.value = props.cardData.modelUrl;
isGenerating.value = false;
progressPercentage.value = 100;
break;
default:
break;
}
handleGenerateModel();
});
//
@ -204,25 +225,13 @@ const handleImageError = (event) => {
};
//
const props = defineProps({
// URL
modelUrl: {
projectId:{
type: String,
default: ''
},
// URL
imageUrl: {
type: String,
default: ''
},
// IDIPCard
cardId: {
type: [String, Number],
default: null
},
//
altText: {
type: String,
default: '3D模型'
cardData: {
type: Object,
default: () => ({})
},
//
backgroundColor: {
@ -249,21 +258,6 @@ const props = defineProps({
type: Boolean,
default: false
},
//
generateFourView: {
type: Boolean,
default: false
},
//
refineModel: {
type: Boolean,
default: false
},
// ID
refineModelTaskId: {
type: String,
default: ''
},
});
// URL
@ -273,9 +267,11 @@ const imageAspectRatio = ref(16 / 9); // 默认比例
// 使URL
const currentModelUrl = computed(() => {
return generatedModelUrl.value || props.modelUrl;
return generatedModelUrl.value || props.cardData.modelUrl;
});
// 使9:16
const cardStyle = computed(() => {
const width = props.cardWidth;
@ -486,6 +482,11 @@ const cardStyle = computed(() => {
}
.progress-background-text {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
color: rgba(255, 255, 255, 0.8);
font-size: 12px;

View File

@ -89,7 +89,7 @@ export default {
},
sort: {
name: '名称',
createdAt: '创建时间',
created_at: '创建时间',
lastActive: '最后活跃',
status: '状态'
},
@ -172,7 +172,22 @@ export default {
viewProfile: '查看个人资料',
accountSettings: '账户设置',
languageSettings: '语言设置',
themeSettings: '主题设置'
themeSettings: '主题设置',
projectName: '数字创作平台',
projectNamePlaceholder: '请输入项目名称',
editProjectName: '编辑项目名称',
saveProjectName: '保存',
imageFreeCount: '生图免费',
modelFreeCount: '模型免费',
times: '次',
guide: '使用指南',
back: '返回',
skip: '跳过',
next: '下一步',
previous: '上一步',
startCreating: '开始创作',
skipGuide: '跳过引导',
step: '步骤'
},
roles: {
creator: '创作者',
@ -285,6 +300,28 @@ export default {
backButton: '返回'
}
},
guideModal: {
step1: {
title: '参考图片',
description: '选择您喜欢的图片作为创作参考',
tips: '点击生成按钮后平台会根据您的选择生成相应的3D模型。'
},
step2: {
title: '模型生成/文字优化',
description: '根据您的参考图片平台会生成对应的3D模型。',
tips: '您也可以输入文字描述,平台会根据您的需求进行图片优化。'
},
step3: {
title: '查看详情',
description: '点击查看详情按钮您可以查看更多关于您创作的3D模型的信息。',
tips: ''
},
step4: {
title: '定制到家',
description: '根据您的需求平台会为您定制专属的3D模型机器人确保符合您的要求。',
tips: '您可以优先在智能体中配置模型角色'
}
},
list: {
title: '虚拟滚动列表示例',
},
@ -333,7 +370,7 @@ export default {
},
sort: {
name: '名称',
createdAt: '创建时间',
created_at: '创建时间',
lastActive: '最后活跃',
status: '状态'
},
@ -396,6 +433,13 @@ export default {
description: '您还没有任何订单记录',
action: '创建订单'
},
refundStatus:{
wtk:'无退款',
sqtk:'申请退款',
jjtk:'拒绝退款',
tytk:'同意退款',
ytk:'已退款'
},
status: {
all: '全部',
pending: '待处理',
@ -406,10 +450,14 @@ export default {
completed: '已完成',
cancelled: '已取消',
refunded: '已退款',
expired: '已过期'
expired: '已过期',
shenhe:'待审核',
unsuccess:'已拒绝',
clz:'处理中',
dfh:'待发货'
},
sort: {
createdAt: '创建时间',
created_at: '创建时间',
total: '订单总额',
status: '订单状态',
customer: '客户名称'
@ -536,12 +584,25 @@ export default {
saveInfo: '保存此信息以备下次使用',
size: '尺寸',
quantity: '数量',
ipName: 'IP名称',
ipNamePlaceholder: '请输入IP名称',
buy: '购买',
processTitle: '我们的流程如下',
orderConfirmation: '订单确认在下单后的1个工作日内我们会确认信息后开始处理。',
productionTime: '生产时间:生产周期为 515 个工作日,节假日可能顺延。',
logistics: '物流:发货后将提供订单与跟踪编号,物流信息会发送到您的邮箱。',
afterSales: '售后与退款请参考退款政策如有问题请联系13121765685'
afterSales: '售后与退款请参考退款政策如有问题请联系13121765685',
error: {
firstNameRequired: '名不能为空',
lastNameRequired: '姓不能为空',
postalCodeRequired: '邮政编码不能为空',
stateRequired: '省/州不能为空',
cityRequired: '城市不能为空',
address1Required: '地址不能为空',
phoneRequired: '电话不能为空',
emailOrPhoneRequired: '邮箱/手机号不能为空',
ipNameRequired: 'IP名称不能为空'
}
},
agentTemplate: {
basicInfo: '基本信息',
@ -885,6 +946,100 @@ export default {
validation: {
referenceImageRequired: '请上传参考图像或选择草图以继续生成'
}
},
iPandCardLeft: {
textPrompt: '文本提示',
placeholder: {
characterDescription: '请描述您想要创建的角色形象...'
},
addReferenceImage: '添加参考图片',
uploadOrSelectImage: '上传或选择一张图片',
dragImageHere: '或拖拽图片到此处',
ipType: 'IP类型',
character: '人物',
animal: '动物',
characterImport: '角色导入',
expression: {
title: '表情选择',
description: '选择一个表情来丰富您的角色形象',
happy: '开心',
surprised: '惊讶',
smile: '微笑',
laugh: '大笑',
naughty: '调皮',
cool: '酷',
shy: '害羞',
angry: '生气',
thinking: '思考',
love: '爱心'
},
hairColor: {
title: '发色选择',
description: '选择角色的发色',
black: '黑色',
brown: '棕色',
blonde: '金色',
red: '红色',
gray: '灰色',
white: '白色',
blue: '蓝色',
green: '绿色',
purple: '紫色',
pink: '粉色'
},
skinColor: {
title: '肤色选择',
description: '选择角色的肤色',
fair: '白皙',
light: '浅色',
medium: '中等',
olive: '橄榄色',
tan: '古铜色',
brown: '深棕色',
dark: '深色'
},
material: {
title: '材质选择',
description: '选择材质来提升您的设计质感',
metal: '白毛绒',
type: 'Metal'
},
color: {
title: '颜色选择',
description: '为您的{{material}}材质选择一种颜色'
},
electronicModule: '电子模块',
sketch: {
title: '草图选择',
description: '选择与您的{{module}}模块相匹配的草图'
},
creativeStyle: '创意风格选择',
imageQuantity: '图片数量选择',
selectQuantity: '选择图片数量',
styles: {
general: '通用',
anime: '动漫',
realistic: '写实',
cyberpunk: '赛博朋克',
chinese: '国风',
pixel: '像素风'
}
},
creationWorkspace: {
changeCover: '更换封面',
createNewProject: '创建新项目',
dropToDelete: '拖到此处删除',
dropToDeleteHint: '释放鼠标即可删除项目',
confirmDelete: '确认删除',
deleteProject: '删除项目',
cancel: '取消'
},
loading: '加载中...',
allLoaded: '已加载全部数据',
emptyProjects: {
title: '暂无项目',
description: '您还没有创建任何项目,点击下方按钮开始创建吧',
action: '创建新项目'
}
},
en: {
@ -1022,7 +1177,7 @@ export default {
},
sort: {
name: 'Name',
createdAt: 'Created Time',
created_at: 'Created Time',
lastActive: 'Last Active',
status: 'Status'
},
@ -1054,6 +1209,28 @@ export default {
},
deviceSettings: 'Settings'
},
guideModal: {
step1: {
title: 'Reference Image',
description: 'Select your preferred image as a creative reference',
tips: 'After clicking the generate button, the platform will generate the corresponding 3D model based on your selection.'
},
step2: {
title: 'Model Generation/Text Optimization',
description: 'Based on your reference image, the platform will generate the corresponding 3D model.',
tips: 'You can also input text descriptions, and the platform will optimize the image according to your needs.'
},
step3: {
title: 'View Details',
description: 'Click the view details button to see more information about your created 3D model.',
tips: ''
},
step4: {
title: 'Customize to Home',
description: 'According to your needs, the platform will customize an exclusive 3D model robot for you to ensure it meets your requirements.',
tips: 'You can configure the model character in the agent first'
}
},
modelModal: {
customizeToHome: 'Customize to Home'
},
@ -1106,7 +1283,22 @@ export default {
viewProfile: 'View Profile',
accountSettings: 'Account Settings',
languageSettings: 'Language Settings',
themeSettings: 'Theme Settings'
themeSettings: 'Theme Settings',
projectName: 'Digital Creation Platform',
projectNamePlaceholder: 'Please enter project name',
editProjectName: 'Edit Project Name',
saveProjectName: 'Save',
imageFreeCount: 'Image Free',
modelFreeCount: 'Model Free',
times: 'times',
guide: 'User Guide',
back: 'Back',
skip: 'Skip',
next: 'Next',
previous: 'Previous',
startCreating: 'Start Creating',
skipGuide: 'Skip Guide',
step: 'Step'
},
roles: {
creator: 'Creator',
@ -1237,7 +1429,7 @@ export default {
refunded: 'Refunded'
},
sort: {
createdAt: 'Created Time',
created_at: 'Created Time',
total: 'Order Total',
status: 'Order Status',
customer: 'Customer Name'
@ -1310,7 +1502,7 @@ export default {
expired: 'Expired'
},
sort: {
createdAt: 'Created Time',
created_at: 'Created Time',
total: 'Order Total',
status: 'Order Status',
customer: 'Customer Name'
@ -1463,7 +1655,23 @@ export default {
succeeded: 'Succeeded',
failed: 'Failed',
canceled: 'Canceled'
}
},
methods: {
creditCard: 'Credit Card',
alipay: 'Alipay',
wechat: 'WeChat Pay'
},
orderSummary: 'Order Summary',
couponPlaceholder: 'Enter coupon code',
applyCoupon: 'Apply Coupon',
couponApplied: 'Coupon Applied',
couponSuccess: 'Coupon applied successfully',
invalidCoupon: 'Invalid coupon',
couponError: 'Failed to process coupon',
payNow: 'Pay Now',
processing: 'Processing payment...',
stripeNotInitialized: 'Stripe not initialized',
securityNotice: 'Your payment information will be securely transmitted with encryption'
},
checkout: {
customModel: 'Custom Model',
@ -1487,6 +1695,8 @@ export default {
saveInfo: 'Save this information for next time',
size: 'Size',
quantity: 'Quantity',
ipName: 'IP Name',
ipNamePlaceholder: 'Please enter IP name',
buy: 'Buy',
processTitle: 'Our Process',
orderConfirmation: 'Order Confirmation: We will confirm the information within 1 business day after placing the order and begin processing.',
@ -1758,6 +1968,100 @@ export default {
validation: {
referenceImageRequired: 'Please upload a reference image or select a sketch to continue generation'
}
},
iPandCardLeft: {
textPrompt: 'Text Prompt',
placeholder: {
characterDescription: 'Please describe the character you want to create...'
},
addReferenceImage: 'Add Reference Image',
uploadOrSelectImage: 'Upload or select an image',
dragImageHere: 'Or drag image here',
ipType: 'IP Type',
character: 'Character',
animal: 'Animal',
characterImport: 'Character Import',
expression: {
title: 'Expression Selection',
description: 'Choose an expression to enrich your character',
happy: 'Happy',
surprised: 'Surprised',
smile: 'Smile',
laugh: 'Laugh',
naughty: 'Naughty',
cool: 'Cool',
shy: 'Shy',
angry: 'Angry',
thinking: 'Thinking',
love: 'Love'
},
hairColor: {
title: 'Hair Color Selection',
description: 'Choose character hair color',
black: 'Black',
brown: 'Brown',
blonde: 'Blonde',
red: 'Red',
gray: 'Gray',
white: 'White',
blue: 'Blue',
green: 'Green',
purple: 'Purple',
pink: 'Pink'
},
skinColor: {
title: 'Skin Color Selection',
description: 'Choose character skin color',
fair: 'Fair',
light: 'Light',
medium: 'Medium',
olive: 'Olive',
tan: 'Tan',
brown: 'Brown',
dark: 'Dark'
},
material: {
title: 'Material Selection',
description: 'Choose material to enhance your design texture',
metal: 'White Plush',
type: 'Metal'
},
color: {
title: 'Color Selection',
description: 'Choose a color for your {{material}} material'
},
electronicModule: 'Electronic Module',
sketch: {
title: 'Sketch Selection',
description: 'Choose a sketch that matches your {{module}} module'
},
creativeStyle: 'Creative Style Selection',
imageQuantity: 'Image Quantity Selection',
selectQuantity: 'Select Image Quantity',
styles: {
general: 'General',
anime: 'Anime',
realistic: 'Realistic',
cyberpunk: 'Cyberpunk',
chinese: 'Chinese Style',
pixel: 'Pixel Art'
}
},
creationWorkspace: {
changeCover: 'Change Cover',
createNewProject: 'Create New Project',
dropToDelete: 'Drop to Delete',
dropToDeleteHint: 'Release to delete project',
confirmDelete: 'Confirm Delete',
deleteProject: 'Delete Project',
cancel: 'Cancel'
},
loading: 'Loading...',
allLoaded: 'All data loaded',
emptyProjects: {
title: 'No Projects Yet',
description: 'You haven\'t created any projects yet. Click the button below to get started.',
action: 'Create New Project'
}
},
},

View File

@ -16,7 +16,7 @@ import i18nConfig from './locales/index.js'
import router from './router'
import { createPinia } from 'pinia'
import VueLazyload from 'vue3-lazyload'
import 'element-plus/dist/index.css'
const app = createApp(App)
// Pinia
@ -92,6 +92,6 @@ document.head.appendChild(scrollbarStyle)
// Import message-fix styles last to ensure project dark overrides don't break
// Element Plus ElMessage appearance.
import './styles/element-fix.css'
// import './styles/element-fix.css'
app.mount('#app')

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory} from 'vue-router'
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const ModernHome = () => import('../views/ModernHome.vue')
@ -8,18 +8,25 @@ const Register = () => import('../views/Register.vue')
const ForgotPassword = () => import('../views/ForgotPassword.vue')
const CreationWorkspace = () => import('../views/CreationWorkspace.vue')
const ProjectGallery = () => import('../views/ProjectGallery.vue')
const OrderManagement = () => import('../views/OrderManagement.vue')
const OrderManagement = () => import('../views/OrderManagement/OrderManagement.vue')
const OrderDetail = () => import('../views/OrderDetail.vue')
const DeviceSettings = () => import('../views/DeviceSettings.vue')
const AgentManagement = () => import('../views/AgentManagement.vue')
const AddAgent = () => import('../views/AddAgent.vue')
const UiTest = () => import('../views/UiTest.vue')
const home = () => import('../views/home/index.vue')
// 路由配置
const routes = [
{
path: '/',
name: 'home',
component: home,
meta: { fullScreen: true }
},
{
path: '/czhome',
name: 'czhome',
component: ModernHome,
meta: { requiresAuth: false, keepAlive: false }
},
@ -72,9 +79,9 @@ const routes = [
meta: { requiresAuth: true, keepAlive: false }
},
{
path: '/create-project',
name: 'create-project',
component: () => import('../views/CreateProject.vue'),
path: '/project/:id',
name: 'project',
component: () => import('../views/Project/CreateProject.vue'),
meta: { requiresAuth: true, fullScreen: true }
},
{
@ -114,7 +121,8 @@ const routes = [
]
const router = createRouter({
history: createWebHistory(),
// history: createWebHistory(),
history:createWebHashHistory(),
routes,
})

View File

@ -219,7 +219,7 @@ const mockAgents = [
},
status: 'active',
lastActive: '2024-12-06T08:30:00Z',
createdAt: '2024-01-10T09:00:00Z',
created_at: '2024-01-10T09:00:00Z',
updatedAt: '2024-12-06T08:30:00Z'
},
{
@ -239,7 +239,7 @@ const mockAgents = [
},
status: 'active',
lastActive: '2024-12-05T16:20:00Z',
createdAt: '2024-01-12T14:30:00Z',
created_at: '2024-01-12T14:30:00Z',
updatedAt: '2024-12-05T16:20:00Z'
},
{
@ -262,7 +262,7 @@ const mockAgents = [
},
status: 'inactive',
lastActive: '2024-12-04T14:10:00Z',
createdAt: '2024-02-01T10:00:00Z',
created_at: '2024-02-01T10:00:00Z',
updatedAt: '2024-12-04T14:10:00Z'
}
];
@ -422,7 +422,7 @@ export const createAgent = async (agentData) => {
},
status: 'active',
lastActive: null,
createdAt: now,
created_at: now,
updatedAt: now
};

View File

@ -103,7 +103,7 @@ export const useAgentStore = defineStore('agents', () => {
let bVal = b[sortBy.value]
// 处理日期排序
if (sortBy.value === 'createdAt' || sortBy.value === 'updatedAt') {
if (sortBy.value === 'created_at' || sortBy.value === 'updatedAt') {
aVal = new Date(aVal)
bVal = new Date(bVal)
}

View File

@ -42,7 +42,7 @@ export const useOrderStore = defineStore('orders', () => {
const filters = ref({
status: null,
search: '',
sortBy: 'createdAt',
sortBy: 'created_at',
sortOrder: 'desc',
dateRange: null
})
@ -78,7 +78,7 @@ export const useOrderStore = defineStore('orders', () => {
const start = new Date(filters.value.dateRange.start)
const end = new Date(filters.value.dateRange.end)
result = result.filter(order => {
const orderDate = new Date(order.createdAt)
const orderDate = new Date(order.created_at)
return orderDate >= start && orderDate <= end
})
}
@ -90,7 +90,7 @@ export const useOrderStore = defineStore('orders', () => {
let bVal = b[sortBy]
// 处理日期排序
if (sortBy === 'createdAt' || sortBy === 'updatedAt') {
if (sortBy === 'created_at' || sortBy === 'updatedAt') {
aVal = new Date(aVal).getTime()
bVal = new Date(bVal).getTime()
}
@ -154,7 +154,7 @@ export const useOrderStore = defineStore('orders', () => {
const setOrders = (orderList) => {
orders.value = orderList.map(order => ({
...order,
createdAt: order.createdAt || new Date().toISOString(),
created_at: order.created_at || new Date().toISOString(),
updatedAt: order.updatedAt || new Date().toISOString()
}))
pagination.value.total = orders.value.length
@ -165,7 +165,7 @@ export const useOrderStore = defineStore('orders', () => {
const newOrder = {
...order,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
created_at: new Date().toISOString(),
updatedAt: new Date().toISOString(),
status: order.status || ORDER_STATUS.PENDING
}
@ -239,7 +239,7 @@ export const useOrderStore = defineStore('orders', () => {
filters.value = {
status: null,
search: '',
sortBy: 'createdAt',
sortBy: 'created_at',
sortOrder: 'desc',
dateRange: null
}
@ -267,7 +267,7 @@ export const useOrderStore = defineStore('orders', () => {
order.customerEmail || '',
STATUS_LABELS[order.status] || '',
order.total || 0,
order.createdAt,
order.created_at,
order.updatedAt
].join(','))
]

View File

@ -60,11 +60,11 @@ button:focus-visible {
#app {
width: 100%;
height: 100vh;
min-height: 100vh;
margin: 0;
padding: 0;
text-align: left;
overflow: hidden;
/* 移除 overflow: hidden 以允许页面滚动 */
}
@media (prefers-color-scheme: light) {

View File

@ -1,124 +0,0 @@
/* Restore Element Plus ElMessage default-like styles and neutralize dark overrides
This file is intentionally loaded last to ensure message styles match Element's
appearance rather than project-specific dark-mode overrides. */
.el-message {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
align-items: center;
display: flex;
gap: 8px;
box-sizing: border-box;
max-width: calc(100% - 32px);
padding: 11px 15px !important;
border-radius: 4px !important;
border-style: solid !important;
border-width: 1px !important;
background-color: #fff !important;
border-color: #ebeef5 !important;
color: #303133 !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
transition: all 0.3s ease !important;
opacity: 1 !important;
}
/* ElMessage transition animations */
.el-message-fade-enter-active,
.el-message-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.el-message-fade-enter-from,
.el-message-fade-leave-to {
opacity: 0 !important;
transform: translateX(-50%) translateY(-100%) !important;
}
.el-message-fade-leave-from {
opacity: 1 !important;
transform: translateX(-50%) translateY(0%) !important;
}
/* ElMessage types */
.el-message--success {
background-color: #f0f9ff !important;
border-color: #b3e5fc !important;
color: #1e88e5 !important;
}
.el-message--warning {
background-color: #fff8e1 !important;
border-color: #ffcc02 !important;
color: #f57c00 !important;
}
.el-message--error {
background-color: #ffebee !important;
border-color: #ffcdd2 !important;
color: #d32f2f !important;
}
.el-message--info {
background-color: #f0f9ff !important;
border-color: #b3e5fc !important;
color: #1e88e5 !important;
}
.el-message p,
.el-message__content {
margin: 0;
color: inherit !important;
overflow-wrap: break-word;
font-size: 14px !important;
line-height: 1.5 !important;
}
.el-message__icon {
color: inherit !important;
font-size: 16px !important;
}
.el-message__closeBtn {
color: var(--el-message-close-icon-color, inherit) !important;
}
/* Neutralize overly-specific dark-mode overrides that use html.dark and !important
by re-declaring rules without relying on html.dark. Loaded last, these will win. */
html.dark .el-message {
background-color: #1f2937 !important;
border-color: #374151 !important;
color: #f9fafb !important;
}
html.dark .el-message--success {
background-color: #065f46 !important;
border-color: #10b981 !important;
color: #d1fae5 !important;
}
html.dark .el-message--warning {
background-color: #78350f !important;
border-color: #f59e0b !important;
color: #fef3c7 !important;
}
html.dark .el-message--error {
background-color: #7f1d1d !important;
border-color: #ef4444 !important;
color: #fecaca !important;
}
html.dark .el-message--info {
background-color: #1e3a8a !important;
border-color: #3b82f6 !important;
color: #dbeafe !important;
}
html.dark .el-message__icon,
html.dark .el-message__content,
html.dark .el-message__closeBtn {
color: inherit !important;
}

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
type="primary"
size="large"
class="action-btn primary-btn create-btn-large"
@click="navigateToFeature({ path: '/create-project' })">
@click="navigateToFeature({ path: '/project' })">
{{ t('home.welcome.startCreating') }}
</el-button>
<div v-else class="guest-actions">

View File

@ -0,0 +1,29 @@
import { requestUtils, clientApi } from '@deotaland/utils';
export class OrderManagement {
constructor() {
}
//获取订单列表
getOrderList(params){
return requestUtils.common(clientApi.default.getOrderList, params);
}
//获取订单详情
getOrderDetail(params){
return requestUtils.common(clientApi.default.getOrderDetail, params);
}
//取消订单支付
orderCancel(params){
return requestUtils.common(clientApi.default.orderCancel, params);
}
//确认收货
receiveAddress(params){
return requestUtils.common(clientApi.default.receiveAddress, params);
}
//退款订单
refundOrder(params){
return requestUtils.common(clientApi.default.refundOrder, params);
}
//订单状态统计
orderStatistics(params){
return requestUtils.common(clientApi.default.orderStatistics, params);
}
}

View File

@ -22,7 +22,6 @@
</button>
</div>
</div>
<div class="filter-group">
<label class="filter-label">{{ t('orderManagement.filters.search') }}</label>
<el-input
@ -36,18 +35,18 @@
</template>
</el-input>
</div>
<div class="filter-group">
<label class="filter-label">{{ t('orderManagement.filters.sort') }}</label>
<el-select v-model="sortBy" class="sort-select">
<el-option v-for="opt in sortOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</div>
</div>
</div>
<div class="orders-grid">
<OrderCard
v-for="order in ordersToShow"
v-for="order in order_list"
:key="order.id"
:order="order"
@view-details="viewOrderDetails"
@ -57,30 +56,15 @@
/>
</div>
<div v-if="ordersToShow.length === 0" class="empty-state">
<div v-if="order_list.length === 0" class="empty-state">
<div class="empty-icon">
<el-icon><DocumentDelete /></el-icon>
</div>
<h3 class="empty-title">{{ t('orderManagement.empty.title') }}</h3>
<p class="empty-description">{{ t('orderManagement.empty.description') }}</p>
<el-button type="primary">{{ t('orderManagement.empty.action') }}</el-button>
</div>
</div>
<el-dialog v-model="paymentDialogVisible" :title="t('payment.title')" class="pay-dialog" destroy-on-close>
<StripePaymentForm
v-if="currentPaymentOrder"
:amount="toCents(currentPaymentOrder.amount || currentPaymentOrder.total || 0)"
:currency="'usd'"
:orderId="currentPaymentOrder.id"
:customerEmail="currentPaymentOrder.customerEmail || ''"
@payment-success="onPaymentSuccess"
@payment-error="onPaymentError"
@cancel="onPaymentCancel"
/>
</el-dialog>
</div>
</template>
<script>
@ -91,7 +75,8 @@ import { DocumentDelete, Search } from '@element-plus/icons-vue'
import OrderCard from '@/components/OrderCard.vue'
import StripePaymentForm from '@/components/StripePaymentForm.vue'
import { useOrderStore } from '@/stores/orders'
import {OrderManagement} from './OrderManagement';
const orderPlug = new OrderManagement()
export default {
name: 'OrderManagement',
components: {
@ -111,7 +96,7 @@ export default {
const ordersToShow = computed(() => orderStore.paginatedOrders || [])
const selectedStatus = ref('all')
const searchQuery = ref('')
const sortBy = ref('createdAt')
const sortBy = ref('created_at')
const statusFilters = ref([
{ key: 'all', label: t('orderManagement.status.all'), count: 0 },
{ key: 'pending', label: t('orderManagement.status.pending'), count: 0 },
@ -125,21 +110,17 @@ export default {
{ key: 'expired', label: t('orderManagement.status.expired'), count: 0 }
])
const sortOptions = ref([
{ label: t('orderManagement.sort.createdAt'), value: 'createdAt' },
{ label: t('orderManagement.sort.total'), value: 'total' },
{ label: t('orderManagement.sort.status'), value: 'status' },
{ label: t('orderManagement.sort.customer'), value: 'customerName' }
{ label: t('orderManagement.sort.created_at'), value: 'created_at' },
{ label: t('orderManagement.sort.total'), value: 'amount' },
// { label: t('orderManagement.sort.status'), value: 'status' },
// { label: t('orderManagement.sort.customer'), value: 'customerName' }
])
const viewOrderDetails = (orderId) => {
router.push({ name: 'order-detail', params: { orderId } })
const viewOrderDetails = (orderData) => {
router.push({ name: 'order-detail', params: { orderId:orderData.id } })
}
const handlePayOrder = (orderId) => {
const order = orderStore.getOrder(orderId)
if (!order) return
currentPaymentOrder.value = order
paymentDialogVisible.value = true
const handlePayOrder = (orderData) => {
window.location.href = orderData.stripe_url
}
const onPaymentSuccess = ({ orderId }) => {
@ -166,8 +147,29 @@ export default {
currentPaymentOrder.value = null
}
const handleCancelOrder = (orderId) => {
orderStore.updateOrder(orderId, { status: 'cancelled' })
const handleCancelOrder = (orderData) => {
ElMessageBox.confirm(
t('orderManagement.cancelConfirm.message'),
t('orderManagement.cancelConfirm.title'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
).then(() => {
orderPlug.orderCancel({
id: orderData.id
}).then(res => {
if (res.code === 0) {
ElMessage.success(t('orderManagement.cancelSuccess'))
init()
} else {
ElMessage.error(res.message || t('orderManagement.cancelFail'))
}
})
}).catch(() => {
//
})
}
const handleExpiredOrder = (orderId) => {
@ -175,10 +177,36 @@ export default {
}
const toCents = (amount) => Math.round((amount || 0) * 100)
const page = ref(1);
const page_size = ref(10);
//
const order_no = ref('');
//
const sort_by = ref('')
const sort_order = ref('asc');//asc/desc
const order_list = ref([]);
//
const getOrderList = ()=>{
orderPlug.getOrderList({
page: page.value,
page_size: page_size.value,
order_no: order_no.value,
sort_by: sort_by.value,
sort_order: sort_order.value
}).then(res=>{
if(res.code==0){
let data = res.data;
order_list.value = data.items||[];
}
})
}
const init = ()=>{
getOrderList();
}
onMounted(() => {
orderStore.initSampleData()
updateStatusCounts()
// orderStore.initSampleData()
// updateStatusCounts()
init();
})
const selectStatus = (status) => {
@ -222,7 +250,8 @@ export default {
onPaymentCancel,
toCents,
selectStatus,
updateStatusCounts
updateStatusCounts,
order_list,
}
}
}

View File

@ -2,12 +2,13 @@
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent @openGuideModal="showGuideModal = true" />
<HeaderComponent :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
</div>
<!-- 导入的侧边栏组件 -->
<div class="sidebar-container">
<iPandCardLeft
:Info="projectInfo.details"
@generate-requested="handleGenerateRequested"
@model-generated="handleModelGenerated"
@import-character="openImportModal"
@ -57,33 +58,28 @@
<!-- 根据卡片类型显示不同组件 -->
<IPCard
@generate-smooth-white-model="handleGenerateSmoothWhiteModel"
@create-new-card="handleCreateFourViewCard"
@create-prompt-card="handleCreatePromptCard"
@generate-smooth-white-model="(imageUrl)=>handleGenerateSmoothWhiteModel(index,imageUrl)"
@create-new-card="(data)=>handleCreateFourViewCard(index,data)"
@delete="handleDeleteCard(index)"
:projectId="projectId"
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
v-if="card.type === 'image'"
:cardData="card"
:generateSmoothWhiteModelStatus="card.generateSmoothWhiteModelStatus || false"
:generateFourView="card.generateFourView || false"
:diyPromptText="card.diyPromptText || ''"
@generate-model-requested="handleGenerateModelRequested"
/>
<ModelCard
v-else-if="card.type === 'model'"
:imageUrl="card.imageUrl"
:cardId="card.cardId"
:altText="card.altText"
:cardWidth="card.cardWidth"
:cardData="card"
:clickDisabled="isElementDragging"
:refineModel="card.refineModel || false"
:refineModelTaskId="card.refineModelTaskId || null"
:generateFourView="card.generateFourView || false"
:projectId="projectId"
@save-project="(item)=>{handleSaveProject(index,item,'model')}"
@clickModel="handleModelClick"
@refineModel="handleRefineModel"
/>
</div>
</div>
</main>
<!-- 模型展示弹窗 -->
<ModelModal
:show="showModelModal"
@ -100,7 +96,6 @@
<!-- 引导弹窗 -->
<GuideModal
:show="showGuideModal"
@close="closeGuideModal"
@complete="completeGuide"
@ -109,27 +104,81 @@
</template>
<script setup>
import demoImage from '../assets/demo.png'
//
import { ref, computed, onMounted, onUnmounted } from 'vue';
import iPandCardLeft from '../components/iPandCardLeft/index.vue';
import IPCard from '../components/IPCard/index.vue';
import ModelCard from '../components/modelCard/index.vue';
import ModelModal from '../components/ModelModal/index.vue';
import CharacterImportModal from '../components/CharacterImportModal/index.vue';
import HeaderComponent from '../components/HeaderComponent/HeaderComponent.vue';
import GuideModal from '../components/GuideModal/index.vue';
import { ref, computed, onMounted, onUnmounted,watch } from 'vue';
import iPandCardLeft from '../../components/iPandCardLeft/index.vue';
import IPCard from '../../components/IPCard/index.vue';
import ModelCard from '../../components/modelCard/index.vue';
import ModelModal from '../../components/ModelModal/index.vue';
import CharacterImportModal from '../../components/CharacterImportModal/index.vue';
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
import GuideModal from '../../components/GuideModal/index.vue';
import {useRoute,useRouter} from 'vue-router';
import {MeshyServer} from '@deotaland/utils';
import { ElMessage } from 'element-plus';
import {Project} from './index';
const router = useRouter();
const PluginProject = new Project();
//
const showModelModal = ref(false);
const selectedModel = ref(null);
const showImportModal = ref(false);
const importUrl = ref('https://xiaozhi.me/console/agents');
const showGuideModal = ref(false);
//
const cleanupFunctions = ref({});
const projectId = ref(null);
//
const projectInfo = ref({});
//
const cards = ref([
]);
watch(()=>[projectInfo.value,cards.value], () => {
let newProjectInfo = {...projectInfo.value};
newProjectInfo.details.node_card = cards.value;
updateProjectInfo(newProjectInfo)
}, {deep: true});
//
const handleSaveProject = (index,item,type='image')=>{
let cardItem = cards.value[index];
switch(type){
case 'image':
cardItem.imageUrl = item.imageUrl;
break;
case 'model':
cardItem.modelUrl = item.modelUrl;
cardItem.taskId = item.taskId;
break;
}
cardItem.status = item.status;
console.log(cards.value,'保存项目');
}
const createProject = async ()=>{
const {id} = await PluginProject.createProject();
// project/id
await router.replace(`/project/${id}`);
projectId.value = id;
getProjectInfo(id);
// projectId.value = 8;
// getProjectInfo(8)
}
//
const getProjectInfo = async (id)=>{
const data = await PluginProject.getProject(id);
projectInfo.value = {...data};
// idid
cards.value = [...projectInfo.value.details.node_card].map(card => ({
...card,
id: card.id || Date.now() + Math.random().toString(36).substr(2, 9)
}));
}
//
const updateProjectInfo = async (newProjectInfo)=>{
PluginProject.updateProject(projectId.value,newProjectInfo);
}
//
const handleModelClick = (modelData) => {
//
@ -168,71 +217,6 @@ const completeGuide = () => {
showGuideModal.value = false;
};
// ==================== ====================
/**
* 卡片层级管理系统
* 负责管理所有卡片的z-index层级确保新创建的卡片总是显示在最上层
*/
class ZIndexManager {
constructor() {
this.zIndexCounter = 100;
this.zIndexMap = new Map(); // ID -> z-index
}
/**
* 获取当前最高层级值
*/
getCurrentzIndexCounter() {
return this.zIndexCounter;
}
/**
* 为新卡片分配最高层级
* @param {string|number} cardId - 卡片ID
* @returns {number} 分配的z-index值
*/
allocateHighestZIndex(cardId) {
this.zIndexCounter += 1;
this.zIndexMap.set(cardId, this.zIndexCounter);
return this.zIndexCounter;
}
/**
* 释放卡片的层级
* @param {string|number} cardId - 卡片ID
*/
releaseZIndex(cardId) {
const zIndex = this.zIndexMap.get(cardId);
if (zIndex === this.zIndexCounter) {
//
this.recalculatezIndexCounter();
}
this.zIndexMap.delete(cardId);
}
/**
* 重新计算最高层级值
*/
recalculatezIndexCounter() {
if (this.zIndexMap.size === 0) {
this.zIndexCounter = 100;
return;
}
const maxValue = Math.max(...Array.from(this.zIndexMap.values()));
this.zIndexCounter = maxValue;
}
/**
* 获取卡片的z-index值
* @param {string|number} cardId - 卡片ID
* @returns {number} z-index值
*/
getZIndex(cardId) {
return this.zIndexMap.get(cardId) || 100;
}
}
// ==================== ====================
/**
* 卡片定位计算系统
@ -323,51 +307,29 @@ class CardPositionCalculator {
}
}
//
const zIndexManager = new ZIndexManager();
const positionCalculator = new CardPositionCalculator();
//
const handleDeleteCard = (index) => {
//
if (index >= 0 && index < cards.value.length) {
const cardToDelete = cards.value[index];
//
zIndexManager.releaseZIndex(cardToDelete.id);
//
if (draggedElementIndex.value === index) {
stopElementDrag();
}
//
cards.value.splice(index, 1);
//
if (draggedElementIndex.value !== null && draggedElementIndex.value > index) {
draggedElementIndex.value--;
}
console.log(`已删除卡片,当前卡片数量: ${cards.value.length}`);
}
};
/**
* 创建带智能定位和层级管理的新卡片
* @param {Object} cardConfig - 卡片配置对象
* @returns {Object} 配置完整的新卡片
*/
const createSmartCard = (cardConfig) => {
const createSmartCard = (cardConfig,index=null) => {
// ID
const newCardId = Date.now() + Math.random();
const uniqueId = Date.now() + Math.random().toString(36).substr(2, 9);
//
// const highestCard;
// zIndex
const highestCard = cards.value.length > 0 ? cards.value.reduce((max, card) => {
return (card.zIndex || 0) > (max.zIndex || 0) ? card : max;
}, cards.value[0]) : null;
let highestCard;
if(index){
highestCard = cards.value[index]
}else{
highestCard = cards.value[cards.value.length-1]
}
//
const position = highestCard ?
positionCalculator.calculateRightSidePosition(highestCard, scale.value) :
@ -376,26 +338,19 @@ const createSmartCard = (cardConfig) => {
// 使ID
const newCard = {
...cardConfig,
id: newCardId, // 使ID
id: uniqueId,
offsetX: `${position.x}px`,
offsetY: `${position.y}px`,
timestamp: new Date().toISOString(),
project_id: projectId.value,
//
zIndex: zIndexManager.allocateHighestZIndex(newCardId)
zIndex: cards.value.length+1,
};
//
console.log(`创建新卡片 [ID: ${newCardId}],层级: ${newCard.zIndex},位置: (${position.x}, ${position.y})`);
if (highestCard) {
console.log(`相对于最高层级卡片 [ID: ${highestCard.id}],层级: ${zIndexManager.getZIndex(highestCard.id)}`);
}
console.log(newCard);
return newCard;
};
const handleRefineModel = (taskId)=>{
console.log('点击了模型卡片', taskId);
// 使
const newModelCard = createSmartCard({
cardWidth: "250", //
@ -407,18 +362,16 @@ const handleRefineModel = (taskId)=>{
// cards
cards.value.push(newModelCard);
console.log('已创建精确定位模型卡片:', newModelCard);
}
const handleGenerateSmoothWhiteModel = (imageUrl) => {
//
const handleGenerateSmoothWhiteModel = (index,imageUrl) => {
console.log(imageUrl, 'imageUrlimageUrlimageUrl');
// 使
const newCard = createSmartCard({
imageUrl: imageUrl, //
type: 'image', //
generateSmoothWhiteModelStatus: true
});
},index);
//
cards.value.push(newCard);
@ -426,7 +379,7 @@ const handleGenerateSmoothWhiteModel = (imageUrl) => {
console.log('已创建精确定位白膜卡片:', newCard);
}
//
const handleCreateFourViewCard = (params) => {
const handleCreateFourViewCard = (index,params) => {
const { baseImage } = params;
//
@ -439,7 +392,7 @@ const handleCreateFourViewCard = (params) => {
sketch: null,
isGenerating: true, //
generateFourView: true //
});
},index);
//
cards.value.push(newCard);
@ -447,75 +400,53 @@ const handleCreateFourViewCard = (params) => {
console.log('已创建精确定位四视图卡片:', newCard);
}
//
const handleCreatePromptCard = (params) => {
const { img, prompt, generateFourView } = params;
const handleCreatePromptCard = (index,params) => {
const { img, diyPromptText } = params;
//
const newCard = createSmartCard({
imageUrl: img, //
diyPromptText: prompt, //
type: 'image', //
isGenerating: true, //
generateFourView: generateFourView
});
const newCard = createSmartCard({
imageUrl: img,
diyPromptText:diyPromptText,
status:'loading',
type:'image',
},index);
//
cards.value.push(newCard);
console.log('已创建精确定位提示词卡片:', newCard);
}
//
const handleGenerateRequested = (params) => {
const handleGenerateRequested = async (params) => {
// IPCard
const { profile,
style,
inspirationImage,
count, module,
selectedHairColor,
selectedSkinColor,
sketch,selectedMaterial,selectedColor,ipType,ipTypeImg,selectedExpression } = params;
const {count,profile,inspirationImage,ipType,ipTypeImg} = params
for (let i = 0; i < count; i++) {
// 使
const newCard = createSmartCard({
imageUrl: null, // IPCard
prompt: `${profile.name} - ${profile.appearance}`,
style: style,
type: 'image', //
profile: profile,
prompt:profile.appearance,
inspirationImage: inspirationImage,
ipType:ipType,
ipTypeImg:ipTypeImg,
selectedExpression:selectedExpression || null,
//
module: module || null,
sketch: sketch || null,
selectedMaterial:selectedMaterial || null,
selectedHairColor:selectedHairColor || null,
selectedSkinColor:selectedSkinColor || null,
selectedColor:selectedColor || null,
isGenerating: true, //
index: i + 1
status:'loading',
type:'image',
});
//
cards.value.push(newCard);
}
};
//
const handleGenerateModelRequested = (params) => {
const handleGenerateModelRequested = (index,params) => {
// IPCardmodelCard
const { cardId, imageUrl } = params;
const {imageUrl } = params;
// 使
const newModelCard = createSmartCard({
imageUrl: imageUrl, // IPCardURL
cardWidth: "250", //
type: 'model', //
modelUrl: null, // modelCard
cardId: cardId, // IPCardID
generateFourView: params.generateFourView || false //
});
generateFourView: params.generateFourView || false, //
status:'loading',
taskId:'',
project_id: projectId.value,
},index);
// cards
cards.value.push(newModelCard);
@ -567,27 +498,6 @@ const lastMouseX = ref(0);
const lastMouseY = ref(0);
const lastTimestamp = ref(0);
const animationFrameId = ref(null);
//
const cards = ref([
// {
// id: 1,
// imageUrl: demoImage,
// altText: "IP1",
// cardWidth: "250",
// offsetX: '100px', //
// offsetY: '50px',
// type: 'image' //
// },
// {
// id: 2,
// imageUrl: "https://liblibai-online.liblib.cloud/agent_images/afdbbde1-c334-4dae-9680-c50be1503177.png?x-oss-process=image/resize,w_560,m_lfit/format,webp",
// altText: "IP2",
// cardWidth: "250",
// offsetX: '-100px', //
// offsetY: '-50px',
// type: 'image' //
// }
]);
//
const sceneContainerStyle = computed(() => ({
@ -631,34 +541,12 @@ const getElementStyle = (index) => {
//
const bringToFront = (index) => {
if (index >= 0 && index < cards.value.length) {
// z-index
zIndexCounter.value++;
// z-index
if (!originalZIndexes.value.has(index)) {
const currentZIndex = cards.value[index].zIndex || 'auto';
originalZIndexes.value.set(index, currentZIndex);
}
// z-index
cards.value[index].zIndex = zIndexCounter.value;
console.log(`将元素 ${index} 提升到最前面z-index: ${zIndexCounter.value}`);
}
cards.value[index].zIndex =cards.value.length+1;
};
//
const sendToBack = (index) => {
return
if (index >= 0 && index < cards.value.length) {
const originalZIndex = originalZIndexes.value.get(index);
if (originalZIndex !== undefined) {
// z-index
cards.value[index].zIndex = originalZIndex;
console.log(`将元素 ${index} 恢复到原始层级z-index: ${originalZIndex}`);
}
}
cards.value[index].zIndex =1;
};
//
@ -679,22 +567,18 @@ const startElementDrag = (e, index) => {
isElementDragging.value = true;
isSceneDragging.value = false;
draggedElementIndex.value = index;
//
zIndexCounter.value++;
cards.value[index].zIndex = zIndexCounter.value;
cards.value[index].zIndex =cards.value.length+1;
//
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
//
const currentOffsetX = parseFloat(cards.value[index].offsetX) || 0;
const currentOffsetY = parseFloat(cards.value[index].offsetY) || 0;
//
elementStartX.value = clientX - (currentOffsetX * scale.value);
elementStartY.value = clientY - (currentOffsetY * scale.value);
cards.value[index].offsetX = `${currentOffsetX}px`;
cards.value[index].offsetY = `${currentOffsetY}px`;
};
//
@ -872,17 +756,25 @@ const preventPinchZoom = (e) => {
//
import { addPassiveEventListener } from '@/utils/passiveEventListeners'
const init = ()=>{
const route = useRoute();
projectId.value = route.params.id;
if(projectId.value === 'new'){
createProject();
return
}
getProjectInfo(projectId.value);
}
//
onMounted(() => {
MeshyServer.pollingEnabled = true;
//
showGuideModal.value = true;
// showGuideModal.value = true;
init();
// 使
const removeWheelListener = addPassiveEventListener(document, 'wheel', preventZoom);
const removeTouchStartListener = addPassiveEventListener(document, 'touchstart', preventPinchZoom);
const removeTouchMoveListener = addPassiveEventListener(document, 'touchmove', preventPinchZoom);
// 便使
cleanupFunctions.value = {
wheel: removeWheelListener,
@ -892,7 +784,8 @@ onMounted(() => {
});
//
onUnmounted(() => {
onUnmounted(() => {//
MeshyServer.pollingEnabled = false;
if (cleanupFunctions.value) {
Object.values(cleanupFunctions.value).forEach(cleanup => {
if (typeof cleanup === 'function') {

View File

@ -0,0 +1,719 @@
import { clientApi, requestUtils } from '@deotaland/utils';
export class Project{
constructor() {
// 用于防抖处理的定时器
this.updateTimer = null;
// 存储最新的请求参数
this.latestUpdateParams = null;
}
//创建项目
async createProject(name='project',) {
return new Promise(async (resolve, reject) => {
let params = {
"thumbnail": "",//项目缩略图
"title": name,//项目标题
"description": "init",//项目描述
"details": {
"node_card": [],
"prompt":'',
"thumbnail": "", // 缩略图
},
"tags": [
"doll"
],
"duration_seconds": 0,//项目时长(秒)
}
let res = await requestUtils.common(clientApi.default.PROJECT_CREATE,params)
if(res.code !== 0){
reject(res)
}else{
resolve(res.data)
}
})
}
//更新项目(带防抖处理,三秒内只执行最后一次请求)
async updateProject(projectId,projectConfig) {
return new Promise(async (resolve, reject) => {
// 存储最新的请求参数
const params = {
"id": projectId,//项目ID
...projectConfig
};
this.latestUpdateParams = params;
// 清除之前的定时器
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
// 设置新的3秒定时器
this.updateTimer = setTimeout(async () => {
try {
const res = await requestUtils.common(clientApi.default.PROJECT_UPDATE, this.latestUpdateParams);
if(res.code !== 0){
reject(res)
}else{
resolve(res.data)
}
} catch (error) {
reject(error);
} finally {
// 清除定时器引用
this.updateTimer = null;
}
}, 1000);
})
}
//删除项目
async deleteProject(projectId) {
return new Promise(async (resolve, reject) => {
let params = {
"id": projectId,//项目ID
}
let res = await requestUtils.common(clientApi.default.PROJECT_DELETE,params)
if(res.code !== 0){
reject(res)
}else{
resolve(res.data)
}
})
}
//获取项目详情
async getProject(projectId) {
return new Promise(async (resolve, reject) => {
let params = {
"id": projectId,//项目ID
}
let res = await requestUtils.common(clientApi.default.PROJECT_GET,params)
if(res.code !== 0){
reject(res)
}else{
resolve(res.data)
}
})
}
//获取项目列表
async getProjectList(params) {
return new Promise(async (resolve, reject) => {
// resolve({
// "items": [
// {
// "id": 20,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T05:36:19.758116+00:00",
// "updated_at": "2025-11-28T06:05:04.982516+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 19,
// "user_id": 1,
// "thumbnail": "",
// "title": "蛇系",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "蛇系美女",
// "status": "success",
// "zIndex": 112,
// "offsetX": "-191px",
// "offsetY": "-230px",
// "imageUrl": "https://api.deotaland.ai/upload/932512adb172459db6cb1d02c12c842e.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:34:17.545Z",
// "project_id": "19",
// "inspirationImage": ""
// },
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "蛇系美女",
// "status": "success",
// "zIndex": 117,
// "offsetX": "96px",
// "offsetY": "-247px",
// "imageUrl": "https://api.deotaland.ai/upload/2e58e1dfc1d8459eb2b094e7c1ce459e.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:34:17.546Z",
// "project_id": "19",
// "inspirationImage": ""
// },
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "蛇系美女",
// "status": "success",
// "zIndex": 121,
// "offsetX": "81px",
// "offsetY": "212px",
// "imageUrl": "https://api.deotaland.ai/upload/9d9d70a5ecbe4e1ab4298d3ad14a8402.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:34:17.546Z",
// "project_id": "19",
// "inspirationImage": ""
// },
// {
// "type": "model",
// "status": "success",
// "taskId": "019ac8f5-c5b2-775e-a36d-19a4651275dd",
// "zIndex": 117,
// "offsetX": "-223px",
// "offsetY": "210px",
// "imageUrl": "https://api.deotaland.ai/upload/2e58e1dfc1d8459eb2b094e7c1ce459e.png",
// "modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8f5-c5b2-775e-a36d-19a4651275dd/output/model.glb?Expires=4917908316&Signature=Vo3dEReKA3MeFRoxQxs6xf0Ypj4lkf8sJiOk43~2M95Mw7qv5WvjPmmKRktj6XIoAlZ4wvtB-zjmhR8rxK9CMR0uRJC~v06e8jDJBViDpjaB5TC6cyvb5EQK8gEPq-lr4p8wfWoNDtO0sX6Zmby~4Z-vhp2wg9Vc4GuzqJ6Uj89qBJgAx-~i2QaGDdRaJ~qqIdCp82zZnNi8Rl-2JEfiMeMWDAUzDbqTG4Hy4yKLsZ2TBQh2YZEJZcwOYdwbwVokFPuAHn-EyIDcxHtUsLqUp8JDB~yALWmuidw1ETQUUPl914T~FfmkWfb0SqVcdIDDkNl-sLwKaxdW8hyCOCYpXA__&Key-Pair-Id=KL5I0C8H7HX83",
// "cardWidth": "250",
// "timestamp": "2025-11-28T05:35:41.218Z",
// "project_id": "19",
// "generateFourView": false
// }
// ],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T05:33:35.161488+00:00",
// "updated_at": "2025-11-28T06:05:01.156032+00:00",
// "is_delete": 0,
// "gemini_success_count": 4,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 1
// },
// {
// "id": 18,
// "user_id": 1,
// "thumbnail": "",
// "title": "蛇系",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "蛇系女孩",
// "status": "success",
// "zIndex": 102,
// "offsetX": "-446px",
// "offsetY": "-210px",
// "imageUrl": "https://api.deotaland.ai/upload/2ada0aaffd0945b49f67a0da85afc20c.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:29:06.741Z",
// "project_id": 18,
// "inspirationImage": ""
// },
// {
// "type": "model",
// "status": "success",
// "taskId": "019ac8f0-1ff1-78a9-885c-9258854baedb",
// "zIndex": 102,
// "offsetX": "8px",
// "offsetY": "-236px",
// "imageUrl": "https://api.deotaland.ai/upload/2ada0aaffd0945b49f67a0da85afc20c.png",
// "modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8f0-1ff1-78a9-885c-9258854baedb/output/model.glb?Expires=4917907940&Signature=XcZPFzqb3JSAN1hYpm2SU~NXxn40ytlfJv~WkULZGw5ZBnNijdBxAPLYLde-FZaoX8OPHZTexnP6odftS1MXtt9NAv2Q-goLd9ey~iHrl25ihXCdTENYilhXSCi5jQS54I6x-GerPpbfKi-61hxRLO4flLAwqxTMXqueDNVMZBq7sQwJ5CsA~9wft503w2qQZBajBBTykFxUYAGFkSFYPcb5uO7Xw1JTrj3BQ~r7QCzSOAyZmK2hnDKKi160gt425h28351DOW3jP2eiV5TsLaTkJnHyUT7bVX1~JFEuO53R9d~rGwdrToLYb6NW~bkiII218VOb6ZN51oU~SVkKPA__&Key-Pair-Id=KL5I0C8H7HX83",
// "cardWidth": "250",
// "timestamp": "2025-11-28T05:29:31.065Z",
// "project_id": 18,
// "generateFourView": false
// }
// ],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T05:28:39.047049+00:00",
// "updated_at": "2025-11-28T05:32:35.485785+00:00",
// "is_delete": 0,
// "gemini_success_count": 1,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 1
// },
// {
// "id": 17,
// "user_id": 1,
// "thumbnail": "",
// "title": "恐龙妹系列",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "恐龙妹妹",
// "status": "success",
// "zIndex": 106,
// "offsetX": "-409px",
// "offsetY": "-336px",
// "imageUrl": "https://api.deotaland.ai/upload/580886031063469781f18d6ef7f0b09c.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:24:06.918Z",
// "project_id": 17,
// "inspirationImage": ""
// }
// ],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T05:23:50.666448+00:00",
// "updated_at": "2025-11-28T05:24:38.270082+00:00",
// "is_delete": 0,
// "gemini_success_count": 1,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 1
// },
// {
// "id": 16,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T05:23:20.613163+00:00",
// "updated_at": "2025-11-28T05:23:23.442095+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 15,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T04:16:02.800209+00:00",
// "updated_at": "2025-11-28T04:16:02.800209+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 14,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T04:14:37.402123+00:00",
// "updated_at": "2025-11-28T04:14:41.210826+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 13,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T04:14:21.190171+00:00",
// "updated_at": "2025-11-28T04:14:23.700132+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 12,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T04:14:16.233977+00:00",
// "updated_at": "2025-11-28T04:14:18.747615+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 11,
// "user_id": 1,
// "thumbnail": "",
// "title": "猫系玩偶系列",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "猫女",
// "status": "success",
// "zIndex": 101,
// "offsetX": "92px",
// "offsetY": "-395px",
// "imageUrl": "https://api.deotaland.ai/upload/526afeec880e46688d6e2edd1893e778.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T04:11:03.655Z",
// "project_id": "11",
// "inspirationImage": ""
// },
// {
// "type": "model",
// "status": "success",
// "taskId": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroo1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
// "zIndex": 101,
// "offsetX": "-263px",
// "offsetY": "-388px",
// "imageUrl": "https://api.deotaland.ai/upload/526afeec880e46688d6e2edd1893e778.png",
// "modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroo1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
// "cardWidth": "250",
// "timestamp": "2025-11-28T04:51:05.620Z",
// "project_id": "11",
// "generateFourView": false
// },
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "猫男",
// "status": "success",
// "zIndex": 102,
// "offsetX": "-232px",
// "offsetY": "66px",
// "imageUrl": "https://api.deotaland.ai/upload/eacac612e5d34e23bbf26491a00fef20.png",
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T05:19:26.236Z",
// "project_id": "11",
// "inspirationImage": ""
// },
// {
// "type": "model",
// "status": "success",
// "taskId": "019ac8e7-3cf0-7aa8-b441-f94d8d445b88",
// "zIndex": 102,
// "offsetX": "466px",
// "offsetY": "-380px",
// "imageUrl": "https://api.deotaland.ai/upload/eacac612e5d34e23bbf26491a00fef20.png",
// "modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8e7-3cf0-7aa8-b441-f94d8d445b88/output/model.glb?Expires=4917907360&Signature=HTG71MVGMBj-inbrX4fjHwf3B7xBE~WX~cB--F6cMR-~03fdqyzYKAi-UDt9rnW3md03mJbTiMa2z9z6B~3NP8Exmzp783nfrAQaWfxarDX8iaH9GXPuEePhveXXYmcg2QEBTifPqkU~fwinI6kY9dfNGRTvrp8RHglr3Q8kuhUZS2Mo79OSCg5X-oZ17uEd-uU7hae8IBAPUSiIsbS1dQRjI32WOhDP6NfSZrf304Fn6HMfpyUxbauuPmb7MJ3dQAWUA1g-iBISThp3degZyV4YhhPkxuaBX5t2GlRuibjqmRlUXB2dEasTmKwq-L5zLYbbaVPXg1HfW9zdnuZJ8A__&Key-Pair-Id=KL5I0C8H7HX83",
// "cardWidth": "250",
// "timestamp": "2025-11-28T05:19:48.668Z",
// "project_id": "11",
// "generateFourView": false
// }
// ],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T03:40:04.226536+00:00",
// "updated_at": "2025-11-28T05:22:47.719360+00:00",
// "is_delete": 0,
// "gemini_success_count": 4,
// "gemini_failure_count": 2,
// "meshy_success_count": 0,
// "meshy_failure_count": 3
// },
// {
// "id": 10,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T03:39:51.024726+00:00",
// "updated_at": "2025-11-28T04:15:11.880275+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 9,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T03:39:18.867572+00:00",
// "updated_at": "2025-11-28T03:39:18.867572+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 8,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": "",
// "node_card": [
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "111",
// "status": "success",
// "zIndex": 106,
// "offsetX": "-462px",
// "offsetY": "-105px",
// "imageUrl": 1,
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T03:17:49.448Z",
// "inspirationImage": ""
// },
// {
// "type": "image",
// "ipType": "人物",
// "prompt": "111",
// "status": "success",
// "zIndex": 102,
// "offsetX": "55px",
// "offsetY": "78px",
// "imageUrl": 2,
// "ipTypeImg": "/src/assets/sketches/tcww.png",
// "timestamp": "2025-11-28T03:17:49.448Z",
// "inspirationImage": ""
// }
// ],
// "thumbnail": ""
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T03:05:53.897607+00:00",
// "updated_at": "2025-11-28T03:39:15.975164+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 7,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": [],
// "ip_card": [],
// "thumbnail": "",
// "model_card": []
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-28T01:27:47.283082+00:00",
// "updated_at": "2025-11-28T01:27:47.283082+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 6,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": [],
// "ip_card": [],
// "thumbnail": "",
// "model_card": []
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-27T09:38:13.654692+00:00",
// "updated_at": "2025-11-27T09:38:13.654692+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 5,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": [],
// "ip_card": [],
// "thumbnail": "",
// "model_card": []
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-27T09:29:49.987926+00:00",
// "updated_at": "2025-11-27T09:29:49.987926+00:00",
// "is_delete": 0,
// "gemini_success_count": 27,
// "gemini_failure_count": 20,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 4,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": [],
// "ip_card": [],
// "thumbnail": "",
// "model_card": []
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-27T09:29:23.927850+00:00",
// "updated_at": "2025-11-27T09:29:23.927850+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// },
// {
// "id": 3,
// "user_id": 1,
// "thumbnail": "",
// "title": "project",
// "description": "init",
// "details": {
// "prompt": [],
// "ip_card": [],
// "thumbnail": "",
// "model_card": []
// },
// "tags": [
// "doll"
// ],
// "duration_seconds": 0,
// "created_at": "2025-11-27T09:21:24.768031+00:00",
// "updated_at": "2025-11-27T09:21:24.768031+00:00",
// "is_delete": 0,
// "gemini_success_count": 0,
// "gemini_failure_count": 0,
// "meshy_success_count": 0,
// "meshy_failure_count": 0
// }
// ],
// "page": 1,
// "page_size": 20,
// "total": 18
// })
// return
let res = await requestUtils.common(clientApi.default.PROJECT_LIST,params)
if(res.code !== 0){
// reject(res)
}else{
resolve(res.data)
}
})
}
}

View File

@ -0,0 +1,554 @@
<template>
<div class="bg-black min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
<!-- Navbar -->
<header
:class="[
'fixed top-0 left-0 right-0 z-50 transition-all duration-300',
isScrolled ? 'bg-black/90 backdrop-blur-md py-4' : 'bg-transparent py-6'
]"
>
<div class="w-full px-6 flex items-center justify-between">
<!-- Logo -->
<a href="#" class="text-2xl font-bold tracking-tighter text-white">
Deotaland
</a>
<!-- Desktop Nav -->
<nav class="hidden md:flex items-center gap-8">
<a
v-for="link in navLinks"
:key="link.name"
:href="link.href"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
{{ link.name }}
</a>
</nav>
<!-- Right Action & Mobile Toggle -->
<div class="flex items-center gap-4">
<a
href="#start"
class="hidden md:inline-flex items-center justify-center px-5 py-2 text-sm font-semibold text-black bg-white rounded-full hover:bg-gray-200 transition-colors"
>
Start Now
</a>
<button
class="md:hidden text-white"
@click="isMobileMenuOpen = !isMobileMenuOpen"
>
<svg v-if="isMobileMenuOpen" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div v-if="isMobileMenuOpen" class="absolute top-full left-0 right-0 bg-black border-t border-gray-800 p-6 flex flex-col gap-4 md:hidden">
<a
v-for="link in navLinks"
:key="link.name"
:href="link.href"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ link.name }}
</a>
<a
href="#start"
class="w-full text-center py-3 text-black bg-white rounded-full font-bold"
>
Start Now
</a>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Hero Section -->
<div ref="containerRef" class="relative h-[300vh] w-full bg-black">
<!-- Sticky Container -->
<div class="sticky top-0 h-[100dvh] w-full overflow-hidden">
<!-- Layer 1: Background Animation (Grid) -->
<div class="absolute inset-0 flex items-center justify-center overflow-hidden">
<div
:style="{ scale }"
class="origin-center flex items-center justify-center transition-transform duration-300"
>
<!-- Grid Layout -->
<div class="grid grid-cols-3 md:grid-cols-5 gap-4 md:gap-6 w-[200vw] md:w-[140vw] h-auto p-4">
<!-- Row 1 -->
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[0]" class="w-full h-full object-cover opacity-60" alt="Robot Companion" /></div>
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[1]" class="w-full h-full object-cover opacity-60" alt="Electronics" /></div>
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[2]" class="w-full h-full object-cover opacity-60" alt="Retro Bot" /></div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[3]" class="w-full h-full object-cover opacity-60" alt="Toy Bot" /></div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[4]" class="w-full h-full object-cover opacity-60" alt="Cyberpunk" /></div>
<!-- Row 2 (Middle) -->
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[5]" class="w-full h-full object-cover opacity-60" alt="Interactive" /></div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[6]" class="w-full h-full object-cover opacity-60" alt="Small Bot" /></div>
<!-- CENTER HERO IMAGE (Always Visible) -->
<div class="col-span-1 row-span-1 aspect-[9/16] rounded-xl overflow-hidden shadow-2xl relative z-10 bg-gray-800 border border-gray-700">
<img :src="heroImage" class="w-full h-full object-cover" alt="Main Hero Robot" />
</div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[7]" class="w-full h-full object-cover opacity-60" alt="3D Print" /></div>
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[8]" class="w-full h-full object-cover opacity-60" alt="Glowing Eye" /></div>
<!-- Row 3 -->
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[9]" class="w-full h-full object-cover opacity-60" alt="Tech Texture" /></div>
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[10]" class="w-full h-full object-cover opacity-60" alt="Robot Hand" /></div>
<div class="aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[11]" class="w-full h-full object-cover opacity-60" alt="Circuit" /></div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[12]" class="w-full h-full object-cover opacity-60" alt="Display" /></div>
<div class="hidden md:block aspect-[9/16] rounded-xl overflow-hidden bg-gray-900/50"><img :src="gridImages[13]" class="w-full h-full object-cover opacity-60" alt="Robotics" /></div>
</div>
</div>
</div>
<!-- Layer 2: Static Dark Overlay -->
<div class="absolute inset-0 bg-black/60 z-10 pointer-events-none" />
<!-- Layer 3: Static Content Layer -->
<div class="absolute inset-0 z-20 flex flex-col items-center justify-center pointer-events-none">
<div class="pointer-events-auto flex flex-col items-center justify-center text-center px-4 w-full max-w-5xl mx-auto">
<h1 class="text-5xl md:text-7xl lg:text-8xl font-bold text-white mb-6 tracking-tighter drop-shadow-2xl">
Create with Deotaland
</h1>
<p class="text-xl md:text-2xl text-gray-200 mb-10 drop-shadow-lg max-w-2xl font-light">
Bring your own AI robot companion to life
</p>
<div class="flex flex-col sm:flex-row gap-5">
<a
href="https://deotaland.com"
class="px-9 py-4 rounded-full border border-white/50 bg-black/20 backdrop-blur-sm text-white font-semibold hover:bg-white hover:text-black transition-all text-lg"
>
Explore More
</a>
<a
href="#login"
class="px-9 py-4 rounded-full bg-white text-black font-semibold hover:bg-gray-200 transition-all text-lg shadow-[0_0_20px_rgba(255,255,255,0.3)]"
>
Start Now
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Creation Canvas Section -->
<section class="py-24 bg-black relative overflow-hidden">
<!-- Background gradient hint -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-gray-900/50 blur-[120px] rounded-full pointer-events-none" />
<div class="w-full px-6 relative z-10">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-bold mb-4">
Your Creation canvas DeotaBoard
</h2>
<p class="text-gray-400 text-lg md:text-xl">
Build your robot in an infinite canvas, together with AI.
</p>
</div>
<!-- Workflow Visualization Container -->
<div class="relative w-full max-w-6xl mx-auto bg-gray-900/40 border border-gray-800 rounded-3xl p-8 md:p-12 backdrop-blur-sm">
<!-- Responsive Flex Layout: Stack on mobile, Row on desktop -->
<div class="flex flex-col md:flex-row items-center justify-between gap-8 md:gap-4">
<!-- LEVEL 1: INPUTS (Stacked Vertically) -->
<div class="flex flex-col gap-6 w-full md:w-auto">
<!-- Input A: Prompt -->
<div class="flex items-center gap-4 bg-black/80 p-4 rounded-xl border border-gray-800 hover:border-gray-600 transition-colors w-full md:w-64 shadow-lg">
<div class="w-12 h-12 rounded-full bg-gray-800 flex items-center justify-center text-purple-400 shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg>
</div>
<div>
<span class="block text-sm font-bold text-gray-200">Prompt / Idea</span>
<span class="text-xs text-gray-500">"A cute pumpkin dog"</span>
</div>
</div>
<!-- Input B: Reference -->
<div class="flex items-center gap-4 bg-black/80 p-4 rounded-xl border border-gray-800 hover:border-gray-600 transition-colors w-full md:w-64 shadow-lg">
<div class="w-12 h-12 rounded-full bg-gray-800 flex items-center justify-center text-blue-400 shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
</div>
<div class="flex-1">
<span class="block text-sm font-bold text-gray-200 mb-1">Reference</span>
<div class="w-full h-24 rounded-lg overflow-hidden border border-gray-700 relative group">
<img :src="refImage" alt="Dog Reference" class="w-full h-full object-cover opacity-80 group-hover:scale-110 transition-transform duration-500" />
<div class="absolute bottom-1 right-1 bg-black/60 px-1 rounded text-[10px] text-white">Reference</div>
</div>
</div>
</div>
</div>
<!-- Connector Arrow 1 -->
<div class="flex flex-col items-center justify-center text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotate-90 md:rotate-0"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>
</div>
<!-- LEVEL 2: 3D MODEL -->
<div class="flex flex-col gap-4 bg-black/80 p-5 rounded-2xl border border-gray-800 w-full md:w-64 shadow-lg hover:border-gray-600 transition-colors">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-green-400">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
</div>
<span class="text-sm font-bold text-gray-200">3D Model</span>
</div>
<div class="w-full aspect-square rounded-xl overflow-hidden border border-gray-700 relative bg-gray-900 group">
<img :src="model3dImage" alt="3D Pumpkin Dog" class="w-full h-full object-cover opacity-90 group-hover:scale-110 transition-transform duration-500" />
<div class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/grid-me.png')] opacity-30 pointer-events-none"></div>
<div class="absolute bottom-2 left-2 bg-green-900/80 px-2 py-0.5 rounded text-[10px] text-green-100 font-mono">.OBJ Generated</div>
</div>
<p class="text-xs text-gray-500 text-center">Auto-rigged mesh ready for core</p>
</div>
<!-- Connector Arrow 2 -->
<div class="flex flex-col items-center justify-center text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotate-90 md:rotate-0"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>
</div>
<!-- LEVEL 3: REAL ROBOT -->
<div class="flex flex-col gap-4 bg-gradient-to-b from-purple-900/20 to-blue-900/20 p-5 rounded-2xl border border-purple-500/40 w-full md:w-72 shadow-[0_0_30px_rgba(147,51,234,0.1)]">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white shadow-lg shadow-purple-600/50">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="12" cy="12" r="2"></circle><path d="M7 7h10v10"></path></svg>
</div>
<span class="text-sm font-bold text-white">Real Robot</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400 fill-yellow-400 ml-auto animate-pulse"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
</div>
<div class="w-full aspect-square rounded-xl overflow-hidden border border-purple-500/30 relative group bg-gray-900">
<img :src="realRobotImage" alt="Real Robot Toy" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" />
<div class="absolute inset-0 bg-gradient-to-t from-purple-900/40 to-transparent"></div>
</div>
<p class="text-xs text-purple-200 text-center font-medium">Alive on your desktop</p>
</div>
</div>
</div>
</div>
</section>
<!-- Companionship Section -->
<section class="py-24 bg-black border-t border-gray-900">
<div class="w-full px-6 flex flex-col items-center text-center">
<h2 class="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
Born for Personal Companionship
</h2>
<h3 class="text-2xl md:text-3xl text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400 font-semibold mb-8">
Powered by AI
</h3>
<p class="text-xl text-gray-400 max-w-2xl mb-12 leading-relaxed">
Cute companions, emotional partners, desktop friends... All in one Deotaland DIY robot.
</p>
<a
href="https://deotaland.com"
class="inline-flex items-center justify-center px-8 py-4 text-base font-bold text-black bg-white rounded-full hover:scale-105 transition-transform duration-200"
>
See Robot Examples
</a>
<!-- Optional Visual Element below -->
<div class="mt-16 w-full max-w-4xl h-64 md:h-96 rounded-3xl overflow-hidden relative">
<img
src="https://picsum.photos/1200/600?grayscale"
alt="Robot Companion Context"
class="w-full h-full object-cover opacity-40 hover:opacity-60 transition-opacity duration-500"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent"></div>
</div>
</div>
</section>
<!-- Robot Cards Section -->
<section class="py-20 bg-black overflow-hidden">
<div class="w-full px-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
<div
v-for="card in cards"
:key="card.id"
class="group relative rounded-3xl overflow-hidden aspect-[3/4] cursor-pointer"
>
<img
:src="card.img"
:alt="card.title"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-90" />
<div class="absolute bottom-0 left-0 p-6">
<h3 class="text-xl font-bold text-white mb-1">{{ card.title }}</h3>
<p class="text-sm font-medium text-gray-300">{{ card.user }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-24 bg-gray-900/30">
<div class="w-full px-6">
<div class="mb-16 text-center md:text-left">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-4">
From Zero to Your Own Robot Friend
</h2>
<p class="text-xl text-gray-400">
Simple enough for beginners, powerful enough for makers.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-16">
<div
v-for="(item, idx) in featureList"
:key="idx"
class="flex flex-col items-start gap-4"
>
<div class="p-3 bg-gray-800 rounded-xl mb-2">
<svg v-if="idx === 0" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-purple-400"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path><path d="M2 12h20"></path></svg>
<svg v-else-if="idx === 1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-400"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
<svg v-else-if="idx === 2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-red-400"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</div>
<h3 class="text-2xl font-bold text-white">{{ item.title }}</h3>
<p class="text-gray-400 leading-relaxed text-lg">
{{ item.desc }}
</p>
</div>
</div>
</div>
</section>
<!-- Strong Engine Section -->
<section class="py-24 bg-black">
<div class="container mx-auto px-6 text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Strong Engine Behind
</h2>
<p class="text-lg text-gray-500 mb-16 max-w-2xl mx-auto">
We combine leading AI and 3D technologies to power your DIY robots.
</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
<div
v-for="(name, i) in logos"
:key="i"
class="h-24 bg-gray-900 rounded-lg flex items-center justify-center border border-gray-800 hover:border-gray-600 transition-colors"
>
<span class="text-gray-500 font-semibold text-sm">{{ name }}</span>
</div>
</div>
</div>
</section>
<!-- Community Call Section -->
<section class="py-32 bg-gradient-to-b from-black to-gray-900 border-b border-gray-800">
<div class="container mx-auto px-6 text-center">
<h2 class="text-5xl md:text-7xl font-bold text-white mb-12 tracking-tighter">
Deotaland is for All Creators
</h2>
<a
href="#"
class="inline-block px-10 py-4 text-lg font-bold text-black bg-white rounded-full hover:bg-gray-200 transition-colors"
>
Join the Creator Community
</a>
</div>
</section>
</main>
<!-- Footer -->
<footer class="bg-black text-white py-16 border-t border-gray-900">
<div class="container mx-auto px-6">
<div class="flex flex-col md:flex-row justify-between items-start gap-12">
<!-- Logo -->
<div class="mb-4 md:mb-0">
<span class="text-2xl font-bold tracking-tighter">Deotaland</span>
</div>
<!-- Links Columns -->
<div class="flex flex-wrap gap-12 md:gap-24">
<!-- Socials -->
<div class="flex flex-col gap-4">
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">Socials</h4>
<div class="flex gap-4">
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
<a href="#" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
</div>
</div>
<!-- Language -->
<div class="flex flex-col gap-4">
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">Language</h4>
<div class="flex flex-col gap-2">
<a href="#" class="text-gray-300 hover:text-white text-sm">English</a>
<a href="#" class="text-gray-300 hover:text-white text-sm">中文</a>
</div>
</div>
<!-- Company -->
<div class="flex flex-col gap-4">
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">Company</h4>
<div class="flex flex-col gap-2">
<a href="#" class="text-gray-300 hover:text-white text-sm">About Us</a>
</div>
</div>
<!-- Action -->
<div class="flex flex-col gap-4">
<button
@click="() => window.scrollTo({ top: 0, behavior: 'smooth' })"
class="text-gray-300 hover:text-white text-sm text-left"
>
Back to top
</button>
</div>
</div>
</div>
<div class="mt-16 pt-8 border-t border-gray-900 text-center md:text-left text-sm text-gray-600">
©2025 Deotaland limited. All rights reserved.
</div>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
// Navbar state
const isScrolled = ref(false);
const isMobileMenuOpen = ref(false);
// Hero section state
const containerRef = ref<HTMLElement | null>(null);
const scrollYProgress = ref(0);
// Scroll event handler
const handleScroll = () => {
isScrolled.value = window.scrollY > 50;
// Calculate scroll progress for hero section
if (containerRef.value) {
const containerRect = containerRef.value.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const scrollPosition = window.scrollY;
const containerTop = containerRef.value.offsetTop;
const containerHeight = containerRef.value.offsetHeight;
// Calculate progress from 0 to 1 as we scroll through the container
// The original React code uses offset: ["start start", "end end"] which means:
// - start start: when container starts entering viewport
// - end end: when container finishes exiting viewport
const progress = Math.max(0, Math.min(1,
(scrollPosition - containerTop + viewportHeight) /
(containerHeight + viewportHeight)
));
scrollYProgress.value = progress;
}
};
// Scale transformation based on scroll progress
// Original React code: useTransform(scrollYProgress, [0, 0.8], [3.5, 1])
const scale = computed(() => {
// Zoom out from scale 3.5 to 1 based on scroll (0 to 0.8 progress)
// When progress reaches 0.8, we've completed the zoom out
const mappedProgress = Math.min(scrollYProgress.value / 0.8, 1);
return 3.5 - (mappedProgress * 2.5);
});
// Nav links
const navLinks = [
{ name: 'Creator', href: '#' },
{ name: 'Land', href: '#' },
{ name: 'Pricing', href: '#' },
];
// Theme: Desktop Robots, Emotional Companions, DIY Electronics
const heroImage = "https://images.unsplash.com/photo-1546776310-eef45dd6d63c?auto=format&fit=crop&q=80&w=1200";
// Grid images
const gridImages = [
// Row 1
"https://images.unsplash.com/photo-1535378437327-b7149b379c2e?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1581092160562-40aa08e78837?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1534723328310-e82dad3af43f?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1593085512500-bfd11932f80c?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1555680202-c86f0e12f086?auto=format&fit=crop&q=80&w=600",
// Row 2
"https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&q=80&w=600",
// Row 3 (Center Hero is here in grid)
"https://images.unsplash.com/photo-1601132359864-c974e7989094?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1580835239846-5bb9ce03c8c3?auto=format&fit=crop&q=80&w=600",
// Row 4
"https://images.unsplash.com/photo-1484557985045-edf25e08da73?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1517430816045-df4b7de8db98?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1563770095-39d468f9583d?auto=format&fit=crop&q=80&w=600",
"https://images.unsplash.com/photo-1580835239846-5bb9ce03c8c3?auto=format&fit=crop&q=80&w=600"
];
// Creation Canvas Images
const refImage = "https://images.unsplash.com/photo-1541364983171-a8ba01e95cfc?auto=format&fit=crop&q=80&w=600";
const model3dImage = ""; // Empty for now
const realRobotImage = "https://s1.aigei.com/prevfiles/723506e4d8a84b838dbdb237a79cfee5.png?e=2051020800&token=P7S2Xpzfz11vAkASLTkfHN7Fw-oOZBecqeJaxypL:GzjIZgVXP8PT14vf14QLBQfgHGw=";
// Robot Cards
const cards = [
{ id: 1, title: 'Custom Robot', user: '@Wownny wolf', img: 'https://picsum.photos/300/400?random=30' },
{ id: 2, title: 'Custom Robot', user: '@Lil Moods', img: 'https://picsum.photos/300/400?random=31' },
{ id: 3, title: 'Custom Robot', user: '@Deo Monkey', img: 'https://picsum.photos/300/400?random=32' },
];
// Features List
const featureList = [
{ title: "Stunning Custom Looks", desc: "Generate unique robot designs from your ideas or images." },
{ title: "From Idea to 3D Model", desc: "Turn concepts into printable 3D shells ready for your robot core." },
{ title: "One-Click Expression Packs", desc: "Create and apply DIY eye styles and animated expressions." },
{ title: "Always-On AI Companion", desc: "Long-term memory, emotional responses, and daily interactions." }
];
// Strong Engine Logos
const logos = [
"AI Engine 1", "3D Engine 2", "Voice Engine 3", "Memory Engine 4",
"Physics Core", "Render Tech", "Motion API", "Vision OS"
];
// Mount and unmount scroll event
onMounted(() => {
window.addEventListener('scroll', handleScroll);
// Initial call to set initial state
handleScroll();
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
</script>
<style scoped>
/* Additional custom styles can be added here */
</style>

View File

@ -40,6 +40,16 @@ export default defineConfig({
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
format: {
comments: false
}
},
rollupOptions: {
output: {
manualChunks: {

View File

@ -0,0 +1,260 @@
# Stripe支付数据流程详解
## 1. 价格传递机制
### 1.1 基本价格传递
StripePaymentForm组件通过props接收基本价格信息
```javascript
const props = defineProps({
amount: {
type: Number,
required: true // 金额(分)
},
currency: {
type: String,
default: 'usd' // 货币类型
},
// 其他props...
})
```
**关键说明**
- `amount` 以**分**为单位例如100表示1美元或100日元
- `currency` 默认为'usd'支持Stripe支持的所有货币
### 1.2 费用计算逻辑
组件内部会根据基础金额计算税费和运费:
```javascript
// 计算税费和运费
const calculateFees = () => {
// 模拟税费计算8%
taxAmount.value = Math.round(props.amount * 0.08)
// 模拟运费计算满99免费
shippingAmount.value = props.amount >= 9900 ? 0 : 1000
}
```
### 1.3 优惠券处理
组件支持优惠券折扣,会从最终金额中扣除:
```javascript
// 优惠券应用逻辑
if (discount < 1) {
// 百分比折扣
discountAmount.value = Math.round(props.amount * discount)
} else {
// 固定金额折扣
discountAmount.value = Math.min(discount, props.amount)
}
```
### 1.4 最终金额计算
通过计算属性`finalAmount`得出最终支付金额:
```javascript
const finalAmount = computed(() => {
return props.amount + taxAmount.value + shippingAmount.value - discountAmount.value
})
```
## 2. Stripe订单数据格式
### 2.1 支付方式创建数据
在`processPayment`函数中,创建支付方式时使用的数据格式:
```javascript
const { error, paymentMethod: pm } = await stripe.value.createPaymentMethod({
type: 'card',
card: cardElement.value,
billing_details: { email: props.customerEmail }
})
```
**参数说明**
- `type`: 支付方式类型,这里固定为'card'
- `card`: Stripe卡片元素实例
- `billing_details`: 账单详情,包含客户邮箱
### 2.2 支付方式返回数据结构
Stripe返回的支付方式数据结构示例
```javascript
{
id: 'pm_1234567890',
object: 'payment_method',
billing_details: {
address: {
city: null,
country: null,
line1: null,
line2: null,
postal_code: null,
state: null
},
email: 'customer@example.com',
name: null,
phone: null
},
card: {
brand: 'visa',
checks: {
address_line1_check: null,
address_postal_code_check: null,
cvc_check: 'pass'
},
country: 'US',
exp_month: 12,
exp_year: 2025,
fingerprint: 'abcdef1234567890',
funding: 'credit',
last4: '4242',
networks: {
available: ['visa'],
preferred: null
},
three_d_secure_usage: {
supported: true
},
wallet: null
},
created: 1678901234,
customer: null,
livemode: false,
type: 'card'
}
```
### 2.3 支付成功事件数据
支付成功后,组件通过`payment-success`事件返回的数据格式:
```javascript
emit('payment-success', {
paymentMethodId: paymentMethod?.id,
orderId: props.orderId,
amount: finalAmount.value,
currency: props.currency
})
```
**返回数据说明**
- `paymentMethodId`: Stripe支付方式ID
- `orderId`: 订单ID从props接收
- `amount`: 最终支付金额(分)
- `currency`: 货币类型
### 2.4 与后端交互的数据格式
在实际项目中前端需要将支付信息发送到后端后端再与Stripe API交互。典型的数据格式
```javascript
// 前端发送给后端的数据
const paymentData = {
orderId: props.orderId,
paymentMethodId: paymentMethod.id,
amount: finalAmount.value,
currency: props.currency,
customerEmail: props.customerEmail,
// 其他订单相关信息
}
// 后端返回的数据
const response = {
success: true,
paymentIntentId: 'pi_1234567890',
chargeId: 'ch_1234567890',
orderStatus: 'paid'
}
```
## 3. 数据流向图
```
父组件
├─── 传递基本价格信息 ───► StripePaymentForm组件
│ │
│ ├─── 计算税费和运费
│ │
│ ├─── 应用优惠券折扣
│ │
│ ├─── 创建支付方式 ───► Stripe API
│ │ │
│ │ └─── 返回支付方式数据
│ │
│ ├─── 发送支付数据 ───► 后端API
│ │ │
│ │ └─── 返回支付结果
│ │
│ └─── 触发支付事件 ───► 父组件
└─── 处理支付结果
```
## 4. 实际使用示例
### 4.1 父组件传递价格
```vue
<template>
<StripePaymentForm
:amount="10000" <!-- 100.00 元 -->
:currency="'cny'"
:order-id="'ORDER-20250101-001'"
:customer-email="'user@example.com'"
@payment-success="handlePaymentSuccess"
@payment-error="handlePaymentError"
/>
</template>
```
### 4.2 支付成功处理
```javascript
const handlePaymentSuccess = (paymentResult) => {
console.log('Payment successful:', paymentResult)
// 输出示例:
// {
// paymentMethodId: 'pm_1234567890',
// orderId: 'ORDER-20250101-001',
// amount: 10800, // 108.00 元含8%税费)
// currency: 'cny'
// }
// 跳转到支付成功页面或更新订单状态
}
```
## 5. 注意事项
1. **金额单位**:始终使用分作为金额单位,避免浮点数精度问题
2. **货币一致性**:确保前端和后端使用相同的货币类型
3. **税费计算**:实际项目中应根据地区和法规调整税费计算逻辑
4. **运费规则**:根据实际业务需求调整运费计算规则
5. **优惠券验证**生产环境中应通过后端API验证优惠券有效性
6. **支付结果处理**:必须处理支付成功和失败的情况
7. **幂等性设计**:确保重复支付请求不会导致多次扣款
## 6. 扩展建议
1. **支持多种货币**:根据用户地区自动切换货币
2. **动态税费计算**:根据不同地区和商品类型计算税费
3. **灵活运费规则**:支持多种运费模板和免运费条件
4. **优惠券系统集成**:与后端优惠券系统深度集成
5. **支付方式扩展**支持Apple Pay、Google Pay等多种支付方式
6. **支付意图创建**在后端创建Payment Intent提高支付安全性
## 7. 总结
StripePaymentForm组件通过props接收基本价格信息内部计算税费、运费和优惠券折扣最终生成支付金额。支付过程中组件与Stripe API交互创建支付方式然后将支付信息发送到后端处理。支付结果通过事件通知父组件完成整个支付流程。
了解数据流向和格式对于集成和扩展Stripe支付功能至关重要可以帮助开发人员更好地理解和调试支付流程确保支付系统的安全性和可靠性。

View File

@ -0,0 +1,233 @@
# Stripe支付集成文档
## 1. 概述
本文档介绍了如何在DeotalandAi项目中集成Stripe支付功能包括组件结构、支付流程、配置方法和测试步骤。
## 2. 项目结构
Stripe支付功能主要通过`StripePaymentForm.vue`组件实现,该组件位于`apps/frontend/src/components/`目录下。
## 3. 技术栈
- Vue 3 (Composition API)
- Stripe.js v3
- Element Plus (UI组件库)
- Vue I18n (国际化支持)
## 4. 组件功能详解
### 4.1 核心功能
- 信用卡支付处理
- 订单金额计算
- 优惠券应用
- 支付状态管理
- 响应式设计
- 暗色主题支持
- 中英文切换
### 4.2 组件结构
```
StripePaymentForm.vue
├── template
│ ├── 卡片输入区域
│ ├── 订单摘要
│ ├── 优惠券输入
│ ├── 支付按钮
│ └── 安全提示
├── script
│ ├── 初始化Stripe
│ ├── 计算税费和运费
│ ├── 应用优惠券
│ ├── 处理支付
│ └── 生命周期管理
└── style
├── 基础样式
├── 暗色主题适配
└── 响应式设计
```
## 5. 支付流程详解
### 5.1 初始化流程
1. 组件挂载时调用`initializeStripe()`函数
2. 加载Stripe.js库
3. 创建Stripe元素实例
4. 配置卡片输入样式
5. 挂载卡片输入元素到DOM
6. 计算初始税费和运费
### 5.2 支付处理流程
1. 用户输入信用卡信息
2. 系统验证卡片信息
3. 用户点击"立即支付"按钮
4. 调用`processPayment()`函数
5. 创建支付方式(Payment Method)
6. 发送支付请求到后端
7. 处理支付结果
8. 触发相应的事件回调
### 5.3 关键代码解析
```javascript
// 初始化Stripe
const initializeStripe = async () => {
try {
stripe.value = await loadStripe(STRIPE_PUBLISHABLE_KEY)
// ... 配置Stripe元素
} catch (error) {
console.error('Error initializing Stripe:', error)
}
}
// 处理支付
const processPayment = async () => {
// ... 验证Stripe实例
// 创建支付方式
const { error, paymentMethod: pm } = await stripe.value.createPaymentMethod({
type: 'card',
card: cardElement.value,
billing_details: { email: props.customerEmail }
})
// ... 发送支付请求到后端
// ... 处理支付结果
}
```
## 6. 配置和使用
### 6.1 Stripe API密钥配置
在组件中Stripe公钥直接定义在代码中
```javascript
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51SRnUTG9Oq8PDokQxhKzpPYaf5rTFR5OZ8QqTkGVtL9YUwTZFgU4ipN42Lub6NEYjXRvcIx8hvAvJGkKskDQ0pf9003uZhrC9Y'
```
**生产环境建议**将API密钥存储在环境变量中通过`.env`文件加载。
### 6.2 组件使用
在父组件中引入并使用StripePaymentForm组件
```vue
<template>
<StripePaymentForm
:amount="orderAmount"
:order-id="orderId"
:customer-email="userEmail"
@payment-success="handlePaymentSuccess"
@payment-error="handlePaymentError"
@cancel="handleCancel"
/>
</template>
<script setup>
import StripePaymentForm from './components/StripePaymentForm.vue'
// ... 其他代码
</script>
```
### 6.3 Props参数
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
|--------|------|------|--------|------|
| amount | Number | 是 | - | 订单金额(分) |
| currency | String | 否 | 'usd' | 货币类型 |
| orderId | String | 是 | - | 订单ID |
| customerEmail | String | 否 | '' | 客户邮箱 |
### 6.4 事件
| 事件名 | 描述 | 回调参数 |
|--------|------|----------|
| payment-success | 支付成功时触发 | paymentResult对象 |
| payment-error | 支付失败时触发 | 错误对象 |
| cancel | 用户取消支付时触发 | - |
## 7. 测试和调试
### 7.1 测试卡号
Stripe提供了测试卡号用于开发和测试
- 成功支付4242 4242 4242 4242
- 过期卡片4000 0000 0000 0069
- 资金不足4000 0000 0000 9995
### 7.2 测试流程
1. 确保前端项目正常运行
2. 进入支付页面
3. 输入测试卡号
4. 输入任意过期日期(未来日期)
5. 输入任意CVC码
6. 点击"立即支付"按钮
7. 观察支付结果
### 7.3 调试技巧
- 打开浏览器控制台查看日志
- 使用Stripe Dashboard查看支付记录
- 检查网络请求是否正常
- 验证后端API是否正确处理支付请求
## 8. 常见问题和解决方案
### 8.1 问题Stripe初始化失败
**解决方案**
- 检查Stripe公钥是否正确
- 确保网络连接正常
- 检查浏览器控制台是否有错误信息
### 8.2 问题:支付被拒绝
**解决方案**
- 检查测试卡号是否正确
- 确保订单金额大于0
- 检查Stripe账户是否有足够的余额
### 8.3 问题:优惠券不生效
**解决方案**
- 检查优惠券代码是否正确
- 验证优惠券是否在有效期内
- 检查优惠券使用条件是否满足
## 9. 支付流程优化建议
1. **添加支付方式选择**支持更多支付方式如Apple Pay、Google Pay等
2. **增强错误处理**:提供更详细的错误信息和解决方案
3. **添加支付进度提示**:提升用户体验
4. **实现支付结果页面**:统一处理支付成功和失败的情况
5. **添加支付记录查询**:方便用户查看历史支付记录
## 10. 安全注意事项
1. 不要在前端代码中暴露Stripe密钥
2. 所有支付请求必须经过后端验证
3. 定期更新Stripe库版本
4. 遵循PCI DSS安全标准
5. 加密敏感支付信息
## 11. 部署建议
1. 生产环境使用真实的Stripe API密钥
2. 配置Webhook接收Stripe事件通知
3. 实现幂等性处理,防止重复支付
4. 定期备份支付数据
5. 监控支付系统性能和错误率
## 12. 总结
Stripe支付集成是DeotalandAi项目中的重要功能通过`StripePaymentForm.vue`组件实现了完整的支付流程。本文档详细介绍了组件结构、支付流程、配置方法和测试步骤希望能帮助开发人员更好地理解和使用Stripe支付功能。
如需进一步了解Stripe支付API请参考[Stripe官方文档](https://stripe.com/docs/api)。

View File

@ -432,7 +432,7 @@ export interface Agent {
avatar: string
category: string
tags: string[]
createdAt: string
created_at: string
updatedAt: string
}
@ -443,7 +443,7 @@ export interface Order {
status: 'pending' | 'processing' | 'completed' | 'failed'
amount: number
currency: string
createdAt: string
created_at: string
updatedAt: string
}
```

71
package-lock.json generated
View File

@ -52,6 +52,7 @@
"devDependencies": {
"@iconify-json/feather": "^1.2.1",
"@vitejs/plugin-vue": "^6.0.1",
"terser": "^5.44.1",
"unplugin-auto-import": "^20.2.0",
"unplugin-icons": "^22.5.0",
"unplugin-vue-components": "^30.0.0",
@ -63,6 +64,7 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@types/three": "^0.180.0",
"element-plus": "^2.11.7",
"pinia": "^2.2.6",
@ -76,6 +78,7 @@
"eslint": "^9.15.0",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.3.3",
"terser": "^5.44.1",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.2"
@ -1279,6 +1282,17 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -2208,6 +2222,13 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -2362,6 +2383,13 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@ -4537,6 +4565,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@ -4546,6 +4584,17 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/spawn-command/-/spawn-command-0.0.2.tgz",
@ -4705,6 +4754,25 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/terser": {
"version": "5.44.1",
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
@ -5676,7 +5744,8 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"rimraf": "^5.0.0"
"rimraf": "^5.0.0",
"terser": "^5.44.1"
},
"peerDependencies": {
"element-plus": "^2.0.0",

View File

@ -27,11 +27,12 @@
"clean:plugins": "rimraf plugins/*/dist"
},
"peerDependencies": {
"vue": "^3.0.0",
"element-plus": "^2.0.0"
"element-plus": "^2.0.0",
"vue": "^3.0.0"
},
"devDependencies": {
"rimraf": "^5.0.0"
"rimraf": "^5.0.0",
"terser": "^5.44.1"
},
"keywords": [
"vue3",
@ -40,4 +41,4 @@
],
"author": "Deotaland AI Team",
"license": "MIT"
}
}

View File

@ -5,6 +5,16 @@ import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
format: {
comments: false
}
},
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'DeotalandUI',

View File

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

View File

@ -0,0 +1,9 @@
const order = {
getOrderList:{url:'/api-core/front/order/list',method:'GET'},// 获取订单列表
getOrderDetail:{url:'/api-core/front/order/get',method:'GET'},// 获取订单详情
orderCancel:{url:'/api-core/front/order/cancel',method:'POST'},// 取消订单支付
receiveAddress:{url:'/api-core/front/order/receive',method:'POST'},// 确认收货
refundOrder:{url:'/api-core/front/order/refund',method:'POST'},// 退款订单
orderStatistics:{url:'/api-core/front/order/statistics',method:'GET'},// 订单状态统计
}
export default order;

View File

@ -0,0 +1,6 @@
const pay = {
createPaymentintention:{url:'/createPaymentintention',method:'POST'},// 创建支付意图
createCheckoutSession:{url:'/createCheckoutSession',method:'POST'},// 创建会话支付(购物车)
createPayorOrder:{url:'/api-core/front/stripe/create-and-checkout',method:'POST'}//创建订单并且返回支付链接
}
export default pay;

View File

@ -0,0 +1,29 @@
const login = {
/**
* 创建项目接口
* 引用示例: PROJECT_CREATE
*/
PROJECT_CREATE:{url:'/api-core/front/project/create',method:'POST'},
/**
* 更新项目接口
* 引用示例: login.UPDATE
*/
PROJECT_UPDATE:{url:'/api-core/front/project/update',method:'POST'},
/**
* 删除项目接口
* 引用示例: PROJECT_DELETE
*/
PROJECT_DELETE:{url:'/api-core/front/project/delete',method:'POST'},
/**
* 项目详情接口
* 引用示例: PROJECT_GET
*/
PROJECT_GET:{url:'/api-core/front/project/get',method:'GET'},
/**
* 项目列表接口
* 引用示例: PROJECT_LIST
*/
PROJECT_LIST:{url:'/api-core/front/project/list',method:'POST'},
}
export default login;

View File

@ -16,7 +16,8 @@ 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'
import prompt from './servers/prompt.js';
import { PayServer } from './servers/payserver.js';
// 合并所有工具函数
const deotalandUtils = {
string: stringUtils,
@ -32,6 +33,7 @@ const deotalandUtils = {
MeshyServer,
GiminiServer,
prompt,
PayServer,
// 全局常用方法
debounce: stringUtils.debounce || createDebounce(),
throttle: stringUtils.throttle || createThrottle(),
@ -59,6 +61,7 @@ export {
MeshyServer,
prompt,
GiminiServer,
PayServer,
}
/**

View File

@ -9,6 +9,129 @@ export class FileServer {
concatUrl(url) {
return urlRule.replace('IMGURL',url)
}
//文件压缩
/**
* 压缩文件 - 支持多种格式
* @param {File|string} fileInput - 文件对象本地路径线上URL或base64字符串
* @param {number} quality - 压缩质量 (0-1)默认0.5
* @param {number} maxWidth - 最大宽度默认原图宽度
* @param {number} maxHeight - 最大高度默认原图高度
* @returns {Promise<string>} 压缩后的base64字符串
*/
async compressFile(fileInput, quality = 0.5, maxWidth = null, maxHeight = null) {
try {
let base64String;
// 根据输入类型获取base64字符串
if (fileInput instanceof File) {
// 直接是File对象
base64String = await this.fileToBase64(URL.createObjectURL(fileInput));
} else if (typeof fileInput === 'string') {
if (fileInput.startsWith('data:')) {
// 已经是base64格式
base64String = fileInput;
} else if (fileInput.startsWith('http://') || fileInput.startsWith('https://')) {
// 线上URL
base64String = await this.fileToBase64(fileInput);
} else {
// 本地路径尝试转换为base64
try {
base64String = await this.fileToBase64(fileInput);
} catch (error) {
throw new Error(`无法处理本地路径: ${fileInput}`);
}
}
} else {
throw new Error('不支持的文件输入类型');
}
// 压缩图片
return await this.compressImageFromBase64(base64String, quality, maxWidth, maxHeight);
} catch (error) {
console.error('文件压缩失败:', error);
throw error;
}
}
/**
* 从base64字符串压缩图片
* @param {string} base64String - base64图片字符串
* @param {number} quality - 压缩质量
* @param {number} maxWidth - 最大宽度
* @param {number} maxHeight - 最大高度
* @returns {Promise<string>} 压缩后的base64字符串
*/
compressImageFromBase64(base64String, quality = 0.5, maxWidth = null, maxHeight = null) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
try {
// 计算新的尺寸
let { width, height } = img;
if (maxWidth && width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (maxHeight && height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// 使用高质量的图像缩放
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, width, height);
const compressed = canvas.toDataURL('image/jpeg', quality);
resolve(compressed);
} catch (error) {
reject(error);
}
};
img.onerror = () => {
reject(new Error('图片加载失败'));
};
img.src = base64String;
});
}
/**
* 批量压缩文件
* @param {Array} fileInputs - 文件输入数组
* @param {Object} options - 压缩选项
* @returns {Promise<Array>} 压缩后的base64数组
*/
async compressFilesBatch(fileInputs, options = {}) {
const { quality = 0.5, maxWidth = null, maxHeight = null } = options;
const results = [];
for (const fileInput of fileInputs) {
try {
const compressed = await this.compressFile(fileInput, quality, maxWidth, maxHeight);
results.push({
success: true,
original: fileInput,
compressed: compressed
});
} catch (error) {
results.push({
success: false,
original: fileInput,
error: error.message
});
}
}
return results;
}
/**
* 从URL中提取有效的文件名
* @param {*} url 文件URL

View File

@ -84,10 +84,10 @@ export class GiminiServer extends FileServer {
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
try {
if (images.length > maxImages) {
reject(`参考图片数量不能超过 ${maxImages}`, ERROR_TYPES.VALIDATION, 'low');
reject(`参考图片数量不能超过 ${maxImages}`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
@ -128,9 +128,8 @@ export class GiminiServer extends FileServer {
})
};
//线上生成模型
async generateImageFromMultipleImagesOnline(baseImages, prompt, options = {}){
async generateImageFromMultipleImagesOnline(baseImages, prompt,config){
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) => {
@ -140,19 +139,34 @@ export class GiminiServer extends FileServer {
return await this.uploadFile(image);
}));
try {
if (images.length > maxImages) {
reject(`参考图片数量不能超过 ${maxImages}`, ERROR_TYPES.VALIDATION, 'low');
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image,'url');
} ));
// 构建请求的 parts 数组
const params = {
aspect_ratio:'9:16',
// const params = {
// "aspect_ratio": "9:16",
// "model": "gemini-2.5-flash-image",
// "location": "global",
// "vertexai": true,
// ...config,
// inputs: [
// ...imageParts,
// { text: prompt }
// ]
// }
const params = {
"aspect_ratio": "9:16",
"model": "gemini-2.5-flash-image",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: prompt }
@ -171,9 +185,13 @@ export class GiminiServer extends FileServer {
// }
// ]
// }
// }
// }
if(response.data.error){
reject(response.data.error.message);
return;
}
if(response.code!=0){
reject(response.msg, ERROR_TYPES.VALIDATION, 'low');
reject(response.msg);
return;
}
let data = response.data;
@ -187,11 +205,15 @@ export class GiminiServer extends FileServer {
})
}
//模型生图功能
async handleGenerateImage(referenceImages = [], prompt = '') {
return new Promise(async (resolve) => {
async handleGenerateImage(referenceImages = [], prompt = '',config) {
return new Promise(async (resolve,reject) => {
// let result = await this.generateImageFromMultipleImages(referenceImages, prompt);
let result = await this.generateImageFromMultipleImagesOnline(referenceImages, prompt);
resolve(result);
try {
let result = await this.generateImageFromMultipleImagesOnline(referenceImages, prompt,config);
resolve(result);
} catch (error) {
reject(error);
}
})
}
}

View File

@ -1,6 +1,7 @@
import { requestUtils,clientApi } from "../index";
import { FileServer } from './fileserver.js';
export class MeshyServer extends FileServer {
static pollingEnabled = true;
constructor() {
super();
}
@ -8,18 +9,20 @@ export class MeshyServer extends FileServer {
async createModelTask(item={},callback,errorCallback,config={}) {
try {
let params = {
project_id: item.project_id||0,
project_id: item.project_id,
"payload": {
image_url:'',
ai_model: 'latest',
enable_pbr: true,
should_remesh: true,
should_remesh: false,
should_texture: true,
save_pre_remeshed_model: true,
...config
}
}
let imgurl = await this.uploadFile(item.image_url);
let imgurl = item.image_url.indexOf('https://api.deotaland.ai') !== -1
? item.image_url
: 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);
@ -28,7 +31,7 @@ export class MeshyServer extends FileServer {
// "message": "",
// "success": true,
// "data": {
// "result": "019abf5d-450f-74f3-bc9c-a6a7e8995fd1"
// "result": "019ac8d3-959d-7432-ac15-b5bab5c75b74"
// }
// };
if(response.code==0){
@ -62,6 +65,9 @@ export class MeshyServer extends FileServer {
errorCallback&&errorCallback();
break;
default:
if(!MeshyServer.pollingEnabled){//如果禁用轮询,直接返回
return
}
// 等待三秒
await new Promise(resolve => setTimeout(resolve, 3000));
progressCallback&&progressCallback(data.progress);

View File

@ -0,0 +1,146 @@
import { loadStripe } from '@stripe/stripe-js';
import { requestUtils, clientApi } from '../index';
//获取Stripe公钥
export function getStripePublishableKey() {
if (typeof window !== 'undefined') {
// Vite 环境变量
if (import.meta?.env?.VITE_STRIPE_PUBLISHABLE_KEY) {
return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
}
// 其他环境变量
return window.VITE_STRIPE_PUBLISHABLE_KEY || '';
}
// Node.js 环境
if (typeof process !== 'undefined') {
return process.env.VITE_STRIPE_PUBLISHABLE_KEY || '';
}
}
export class PayServer {
static stripe = null// Stripe实例
constructor() {
}
//初始化
async init() {
return new Promise(async (resolve) => {
if (!PayServer.stripe) {
await loadStripe(getStripePublishableKey()).then((stripe) => {
PayServer.stripe = stripe;
resolve(PayServer.stripe);
});
} else {
resolve(PayServer.stripe);
}
})
}
/**
* 创建支付意图
* @param {number} amount - 支付金额单位为分
* @param {string} currency - 支付币种默认值为 'usd'
* @param {object} skuinfo - 商品信息
* @returns {Promise<object>} - 包含 client_secret element Promise 对象
*/
async createPaymentIntent(amount, currency = 'usd', skuinfo = {}) {
await this.init();
return new Promise(async (resolve, reject) => {
let pamras = {
amount: amount,
currency: currency,
payment_method_types: ['card', 'alipay', 'wechat_pay', 'link'],
sku_info: skuinfo,
}
// let res = await requestUtils.common(clientApi.default.createPaymentintention,pamras)
let res = {
code: 0,
data: {
client_secret: "pi_3SZS2UG9Oq8PDokQ1mYY4ntx_secret_pAE1H7ySo6EN1XO1lRsj5SWYn",
}
}
if (res.code === 0) {
const Element = PayServer.stripe.elements({
clientSecret: res.data.client_secret,
appearance: { theme: 'stripe' },
});
resolve({
client_secret: res.data.client_secret,
element: Element,
cardElement: Element.create('payment'),
})
} else {
reject(res.msg)
}
})
}
//确认支付
async confirmPaymentIntent(Element, email) {
await this.init();
return new Promise(async (resolve, reject) => {
// 确认支付
const { error, paymentIntent } = await PayServer.stripe.confirmPayment({
elements: Element,
confirmParams: {
// return_url: `${window.location.origin}/success`,
receipt_email: email,// 可选,用于接收收据的邮箱
},
redirect: 'if_required'// 如果需要重定向Stripe会自动处理
});
if (error) {
reject(error.message)
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
resolve({
paymentIntent: paymentIntent
})
}
});
}
//创建会话支付(购物车)
async createCheckoutSession(arrSkus) {
await this.init();
return new Promise(async (resolve, reject) => {
let pamras = {
payment_method_types: ['card'],
line_items: arrSkus,
success_url: '',
cancel_url: '',
mode: 'payment',
}
let res = await requestUtils.common(clientApi.default.createCheckoutSession, pamras);
if (res.code === 0) {
let { id } = res.data;
const { error } = await PayServer.stripe.redirectToCheckout({ sessionId: id });// 重定向到Stripe Checkout页面
if (error) {
reject(error.message)
} else {
resolve(id)
}
} else {
reject(res.msg)
}
})
}
//创建订单并且支付
async createPayorOrder(orderInfo) {
await this.init();
return new Promise(async (resolve, reject) => {
let pamras = {
"methods": [
"card"
],
"success_url":"https://www.deotaland.ai/#/order-management",
"cancel_url":"https://www.deotaland.ai/#/order-management",
"quantity":orderInfo.quantity,
"project_id": orderInfo.project_id,
"project_details": orderInfo.project_details,
"order_info": orderInfo.order_info
}
let res = await requestUtils.common(clientApi.default.createPayorOrder, pamras);
if (res.code == 0) {
let data = res.data
window.location.href = data.url
resolve(data);
} else {
reject(res.msg)
}
})
}
}

View File

@ -36,6 +36,7 @@ service.interceptors.request.use(
// config.headers['token'] = `${token}`;
config.headers['Authorization'] = `123`;
config.headers['token'] = `123`;
// config.headers['accept-language'] = 'en';
}
return config;
},

611
requestData.md Normal file
View File

@ -0,0 +1,611 @@
"data": {
"items": [
{
"id": 20,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T05:36:19.758116+00:00",
"updated_at": "2025-11-28T06:05:04.982516+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 19,
"user_id": 1,
"thumbnail": "",
"title": "蛇系",
"description": "init",
"details": {
"prompt": "",
"node_card": [
{
"type": "image",
"ipType": "人物",
"prompt": "蛇系美女",
"status": "success",
"zIndex": 112,
"offsetX": "-191px",
"offsetY": "-230px",
"imageUrl": "https://api.deotaland.ai/upload/932512adb172459db6cb1d02c12c842e.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:34:17.545Z",
"project_id": "19",
"inspirationImage": ""
},
{
"type": "image",
"ipType": "人物",
"prompt": "蛇系美女",
"status": "success",
"zIndex": 117,
"offsetX": "96px",
"offsetY": "-247px",
"imageUrl": "https://api.deotaland.ai/upload/2e58e1dfc1d8459eb2b094e7c1ce459e.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:34:17.546Z",
"project_id": "19",
"inspirationImage": ""
},
{
"type": "image",
"ipType": "人物",
"prompt": "蛇系美女",
"status": "success",
"zIndex": 121,
"offsetX": "81px",
"offsetY": "212px",
"imageUrl": "https://api.deotaland.ai/upload/9d9d70a5ecbe4e1ab4298d3ad14a8402.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:34:17.546Z",
"project_id": "19",
"inspirationImage": ""
},
{
"type": "model",
"status": "success",
"taskId": "019ac8f5-c5b2-775e-a36d-19a4651275dd",
"zIndex": 117,
"offsetX": "-223px",
"offsetY": "210px",
"imageUrl": "https://api.deotaland.ai/upload/2e58e1dfc1d8459eb2b094e7c1ce459e.png",
"modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8f5-c5b2-775e-a36d-19a4651275dd/output/model.glb?Expires=4917908316&Signature=Vo3dEReKA3MeFRoxQxs6xf0Ypj4lkf8sJiOk43~2M95Mw7qv5WvjPmmKRktj6XIoAlZ4wvtB-zjmhR8rxK9CMR0uRJC~v06e8jDJBViDpjaB5TC6cyvb5EQK8gEPq-lr4p8wfWoNDtO0sX6Zmby~4Z-vhp2wg9Vc4GuzqJ6Uj89qBJgAx-~i2QaGDdRaJ~qqIdCp82zZnNi8Rl-2JEfiMeMWDAUzDbqTG4Hy4yKLsZ2TBQh2YZEJZcwOYdwbwVokFPuAHn-EyIDcxHtUsLqUp8JDB~yALWmuidw1ETQUUPl914T~FfmkWfb0SqVcdIDDkNl-sLwKaxdW8hyCOCYpXA__&Key-Pair-Id=KL5I0C8H7HX83",
"cardWidth": "250",
"timestamp": "2025-11-28T05:35:41.218Z",
"project_id": "19",
"generateFourView": false
}
],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T05:33:35.161488+00:00",
"updated_at": "2025-11-28T06:05:01.156032+00:00",
"is_delete": 0,
"gemini_success_count": 4,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 1
},
{
"id": 18,
"user_id": 1,
"thumbnail": "",
"title": "蛇系",
"description": "init",
"details": {
"prompt": "",
"node_card": [
{
"type": "image",
"ipType": "人物",
"prompt": "蛇系女孩",
"status": "success",
"zIndex": 102,
"offsetX": "-446px",
"offsetY": "-210px",
"imageUrl": "https://api.deotaland.ai/upload/2ada0aaffd0945b49f67a0da85afc20c.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:29:06.741Z",
"project_id": 18,
"inspirationImage": ""
},
{
"type": "model",
"status": "success",
"taskId": "019ac8f0-1ff1-78a9-885c-9258854baedb",
"zIndex": 102,
"offsetX": "8px",
"offsetY": "-236px",
"imageUrl": "https://api.deotaland.ai/upload/2ada0aaffd0945b49f67a0da85afc20c.png",
"modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8f0-1ff1-78a9-885c-9258854baedb/output/model.glb?Expires=4917907940&Signature=XcZPFzqb3JSAN1hYpm2SU~NXxn40ytlfJv~WkULZGw5ZBnNijdBxAPLYLde-FZaoX8OPHZTexnP6odftS1MXtt9NAv2Q-goLd9ey~iHrl25ihXCdTENYilhXSCi5jQS54I6x-GerPpbfKi-61hxRLO4flLAwqxTMXqueDNVMZBq7sQwJ5CsA~9wft503w2qQZBajBBTykFxUYAGFkSFYPcb5uO7Xw1JTrj3BQ~r7QCzSOAyZmK2hnDKKi160gt425h28351DOW3jP2eiV5TsLaTkJnHyUT7bVX1~JFEuO53R9d~rGwdrToLYb6NW~bkiII218VOb6ZN51oU~SVkKPA__&Key-Pair-Id=KL5I0C8H7HX83",
"cardWidth": "250",
"timestamp": "2025-11-28T05:29:31.065Z",
"project_id": 18,
"generateFourView": false
}
],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T05:28:39.047049+00:00",
"updated_at": "2025-11-28T05:32:35.485785+00:00",
"is_delete": 0,
"gemini_success_count": 1,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 1
},
{
"id": 17,
"user_id": 1,
"thumbnail": "",
"title": "恐龙妹系列",
"description": "init",
"details": {
"prompt": "",
"node_card": [
{
"type": "image",
"ipType": "人物",
"prompt": "恐龙妹妹",
"status": "success",
"zIndex": 106,
"offsetX": "-409px",
"offsetY": "-336px",
"imageUrl": "https://api.deotaland.ai/upload/580886031063469781f18d6ef7f0b09c.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:24:06.918Z",
"project_id": 17,
"inspirationImage": ""
}
],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T05:23:50.666448+00:00",
"updated_at": "2025-11-28T05:24:38.270082+00:00",
"is_delete": 0,
"gemini_success_count": 1,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 1
},
{
"id": 16,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T05:23:20.613163+00:00",
"updated_at": "2025-11-28T05:23:23.442095+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 15,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T04:16:02.800209+00:00",
"updated_at": "2025-11-28T04:16:02.800209+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 14,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T04:14:37.402123+00:00",
"updated_at": "2025-11-28T04:14:41.210826+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 13,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T04:14:21.190171+00:00",
"updated_at": "2025-11-28T04:14:23.700132+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 12,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T04:14:16.233977+00:00",
"updated_at": "2025-11-28T04:14:18.747615+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 11,
"user_id": 1,
"thumbnail": "",
"title": "猫系玩偶系列",
"description": "init",
"details": {
"prompt": "",
"node_card": [
{
"type": "image",
"ipType": "人物",
"prompt": "猫女",
"status": "success",
"zIndex": 101,
"offsetX": "92px",
"offsetY": "-395px",
"imageUrl": "https://api.deotaland.ai/upload/526afeec880e46688d6e2edd1893e778.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T04:11:03.655Z",
"project_id": "11",
"inspirationImage": ""
},
{
"type": "model",
"status": "success",
"taskId": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroo1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
"zIndex": 101,
"offsetX": "-263px",
"offsetY": "-388px",
"imageUrl": "https://api.deotaland.ai/upload/526afeec880e46688d6e2edd1893e778.png",
"modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroo1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
"cardWidth": "250",
"timestamp": "2025-11-28T04:51:05.620Z",
"project_id": "11",
"generateFourView": false
},
{
"type": "image",
"ipType": "人物",
"prompt": "猫男",
"status": "success",
"zIndex": 102,
"offsetX": "-232px",
"offsetY": "66px",
"imageUrl": "https://api.deotaland.ai/upload/eacac612e5d34e23bbf26491a00fef20.png",
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T05:19:26.236Z",
"project_id": "11",
"inspirationImage": ""
},
{
"type": "model",
"status": "success",
"taskId": "019ac8e7-3cf0-7aa8-b441-f94d8d445b88",
"zIndex": 102,
"offsetX": "466px",
"offsetY": "-380px",
"imageUrl": "https://api.deotaland.ai/upload/eacac612e5d34e23bbf26491a00fef20.png",
"modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8e7-3cf0-7aa8-b441-f94d8d445b88/output/model.glb?Expires=4917907360&Signature=HTG71MVGMBj-inbrX4fjHwf3B7xBE~WX~cB--F6cMR-~03fdqyzYKAi-UDt9rnW3md03mJbTiMa2z9z6B~3NP8Exmzp783nfrAQaWfxarDX8iaH9GXPuEePhveXXYmcg2QEBTifPqkU~fwinI6kY9dfNGRTvrp8RHglr3Q8kuhUZS2Mo79OSCg5X-oZ17uEd-uU7hae8IBAPUSiIsbS1dQRjI32WOhDP6NfSZrf304Fn6HMfpyUxbauuPmb7MJ3dQAWUA1g-iBISThp3degZyV4YhhPkxuaBX5t2GlRuibjqmRlUXB2dEasTmKwq-L5zLYbbaVPXg1HfW9zdnuZJ8A__&Key-Pair-Id=KL5I0C8H7HX83",
"cardWidth": "250",
"timestamp": "2025-11-28T05:19:48.668Z",
"project_id": "11",
"generateFourView": false
}
],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T03:40:04.226536+00:00",
"updated_at": "2025-11-28T05:22:47.719360+00:00",
"is_delete": 0,
"gemini_success_count": 4,
"gemini_failure_count": 2,
"meshy_success_count": 0,
"meshy_failure_count": 3
},
{
"id": 10,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T03:39:51.024726+00:00",
"updated_at": "2025-11-28T04:15:11.880275+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 9,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T03:39:18.867572+00:00",
"updated_at": "2025-11-28T03:39:18.867572+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 8,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": "",
"node_card": [
{
"type": "image",
"ipType": "人物",
"prompt": "111",
"status": "success",
"zIndex": 106,
"offsetX": "-462px",
"offsetY": "-105px",
"imageUrl": 1,
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T03:17:49.448Z",
"inspirationImage": ""
},
{
"type": "image",
"ipType": "人物",
"prompt": "111",
"status": "success",
"zIndex": 102,
"offsetX": "55px",
"offsetY": "78px",
"imageUrl": 2,
"ipTypeImg": "/src/assets/sketches/tcww.png",
"timestamp": "2025-11-28T03:17:49.448Z",
"inspirationImage": ""
}
],
"thumbnail": ""
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T03:05:53.897607+00:00",
"updated_at": "2025-11-28T03:39:15.975164+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 7,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": [],
"ip_card": [],
"thumbnail": "",
"model_card": []
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-28T01:27:47.283082+00:00",
"updated_at": "2025-11-28T01:27:47.283082+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 6,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": [],
"ip_card": [],
"thumbnail": "",
"model_card": []
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-27T09:38:13.654692+00:00",
"updated_at": "2025-11-27T09:38:13.654692+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 5,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": [],
"ip_card": [],
"thumbnail": "",
"model_card": []
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-27T09:29:49.987926+00:00",
"updated_at": "2025-11-27T09:29:49.987926+00:00",
"is_delete": 0,
"gemini_success_count": 27,
"gemini_failure_count": 20,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 4,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": [],
"ip_card": [],
"thumbnail": "",
"model_card": []
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-27T09:29:23.927850+00:00",
"updated_at": "2025-11-27T09:29:23.927850+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
},
{
"id": 3,
"user_id": 1,
"thumbnail": "",
"title": "project",
"description": "init",
"details": {
"prompt": [],
"ip_card": [],
"thumbnail": "",
"model_card": []
},
"tags": [
"doll"
],
"duration_seconds": 0,
"created_at": "2025-11-27T09:21:24.768031+00:00",
"updated_at": "2025-11-27T09:21:24.768031+00:00",
"is_delete": 0,
"gemini_success_count": 0,
"gemini_failure_count": 0,
"meshy_success_count": 0,
"meshy_failure_count": 0
}
],
"page": 1,
"page_size": 20,
"total": 18
}