222
This commit is contained in:
parent
555fbbe1af
commit
eaa8f5de1f
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -470,6 +470,9 @@ const handleDisassembly = () => {
|
|||
disassembledImages.value.push(result)
|
||||
currentStep.value = 2
|
||||
disassemblyLoading.value = false
|
||||
},(error) => {
|
||||
disassemblyLoading.value = false
|
||||
ElMessage.error('拆件失败,请稍后重试')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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')
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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:16),默认为false,使用图片实际比例
|
||||
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);
|
||||
}
|
||||
// 转换所有图片为DataURL格式,确保AI模型能够读取
|
||||
// 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);
|
||||
}
|
||||
// 否则,如果有cardData且没有internalImageUrl,则自动生成普通图片
|
||||
else {
|
||||
// if(!props.diyPromptText){
|
||||
// internalImageUrl.value = demoImage;
|
||||
// formData.value.internalImageUrl = demoImage;
|
||||
// return
|
||||
// }
|
||||
// internalImageUrl.value = demoImage;
|
||||
// return
|
||||
handleGenerateImage();
|
||||
}
|
||||
init();
|
||||
});
|
||||
|
||||
// 监听props.imageUrl的变化,更新internalImageUrl
|
||||
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 {
|
||||
// 将图片转换为AI模型可读取的DataURL格式
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 character’s hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturing—with smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
|
||||
调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
|
||||
图片不要有任何水印,
|
||||
保证生成的任务图片一定要有眼睛,一定要有嘴巴
|
||||
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打印
|
||||
请严格按照参考图中的角色服装进行完整还原。
|
||||
服装结构:确保上衣、下装、外套、鞋履、配饰等所有服装元素与参考图一致,保持准确的比例、形状与层次。
|
||||
眼睛位置与第一张参考图一致,材质表现逼真,保留参考图中的细节,比如还原参考图中的头发。
|
||||
`:``}
|
||||
|
||||
`
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 /> </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 /> </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 /> </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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
// 监听金额变化,重新计算费用
|
||||
|
|
|
|||
|
|
@ -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(''); // 存储3D模型生成的参考图片URL
|
||||
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 = '') => {
|
|||
})
|
||||
}
|
||||
}
|
||||
// 处理文本优化 - 完全参照React组件PromptStep.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; // 图片是可选的
|
||||
}
|
||||
// 验证是否为有效的base64或URL格式
|
||||
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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},{})
|
||||
};
|
||||
}
|
||||
// 组件挂载时,如果有图片URL但没有模型URL,自动生成模型
|
||||
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: ''
|
||||
},
|
||||
// 卡片ID(用于标识来源IPCard)
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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: '生产时间:生产周期为 5–15 个工作日,节假日可能顺延。',
|
||||
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'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(','))
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
// 为没有id的卡片添加唯一id
|
||||
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) => {
|
||||
// 接收IPCard组件传递的参数,创建新的modelCard组件实例
|
||||
const { cardId, imageUrl } = params;
|
||||
|
||||
const {imageUrl } = params;
|
||||
// 使用统一的智能定位和层级管理系统创建模型卡片
|
||||
const newModelCard = createSmartCard({
|
||||
imageUrl: imageUrl, // 从IPCard接收的图片URL
|
||||
cardWidth: "250", // 默认宽度
|
||||
type: 'model', // 标记为模型类型
|
||||
modelUrl: null, // 初始为空,将由modelCard组件自己生成
|
||||
cardId: cardId, // 保留原始IPCard的ID
|
||||
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: "示例IP图片1",
|
||||
// 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: "示例IP图片2",
|
||||
// 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') {
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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支付功能至关重要,可以帮助开发人员更好地理解和调试支付流程,确保支付系统的安全性和可靠性。
|
||||
|
|
@ -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)。
|
||||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue