This commit is contained in:
13121765685 2026-01-05 09:51:07 +08:00
parent 324b670ed5
commit 4c59a1349d
40 changed files with 1700 additions and 4100 deletions

View File

@ -128,6 +128,7 @@ window.setElLoading = (qp=false)=>{
}else{ }else{
loading.value = true loading.value = true
} }
window.closeMethods = closeMethods;
return closeMethods return closeMethods
} }
window.closeMethods = closeMethods; window.closeMethods = closeMethods;

View File

@ -797,7 +797,7 @@ export default {
animal: 'Animal', animal: 'Animal',
person: 'Person', person: 'Person',
general: 'General', general: 'General',
O1: 'O1', E1: 'E1',
title: 'Title', title: 'Title',
enterTitle: 'Please enter prompt title', enterTitle: 'Please enter prompt title',
content: 'Content', content: 'Content',

View File

@ -791,7 +791,7 @@ orderManagement: {
animal: '动物', animal: '动物',
person: '人物', person: '人物',
general: '通用', general: '通用',
O1: 'O1', E1: 'E1',
title: '标题', title: '标题',
enterTitle: '请输入提示词标题', enterTitle: '请输入提示词标题',
content: '内容', content: '内容',

View File

@ -320,7 +320,7 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 画布编辑器对话框 --> <!-- 画布编辑器对话框 -->
<CanvasEditor <DtCanvasEditor
v-model:visible="canvasEditorVisible" v-model:visible="canvasEditorVisible"
:image-url="canvasEditorImageUrl" :image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave" @add-prompt-card="handleCanvasSave"
@ -354,7 +354,6 @@ import {
import ModelViewer from '@/components/common/ModelViewer.vue' import ModelViewer from '@/components/common/ModelViewer.vue'
import ModelCom from '@/components/modelCom/index.vue' import ModelCom from '@/components/modelCom/index.vue'
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue' import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
import CanvasEditor from '@/components/common/CanvasEditor.vue'
import ModelUploadModal from '@/components/ModelUploadModal/index.vue' import ModelUploadModal from '@/components/ModelUploadModal/index.vue'
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js'; import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
import { MeshyServer,GiminiServer,FileServer } from '@deotaland/utils'; import { MeshyServer,GiminiServer,FileServer } from '@deotaland/utils';
@ -412,7 +411,6 @@ const handlePartialEdit = (imageUrl, index) => {
currentEditImageIndex.value = index; currentEditImageIndex.value = index;
canvasEditorVisible.value = true; canvasEditorVisible.value = true;
} }
const handleCanvasSave = (editedImageUrl,editContent) => { const handleCanvasSave = (editedImageUrl,editContent) => {
fileServer.uploadFile(editedImageUrl).then((url) => { fileServer.uploadFile(editedImageUrl).then((url) => {
const newItem = { const newItem = {

View File

@ -82,7 +82,7 @@
<el-option :label="t('admin.promptManagement.animal')" value="animal" /> <el-option :label="t('admin.promptManagement.animal')" value="animal" />
<el-option :label="t('admin.promptManagement.person')" value="person" /> <el-option :label="t('admin.promptManagement.person')" value="person" />
<el-option :label="t('admin.promptManagement.general')" value="general" /> <el-option :label="t('admin.promptManagement.general')" value="general" />
<el-option :label="t('admin.promptManagement.O1')" value="O1" /> <el-option :label="t('admin.promptManagement.E1')" value="E1" />
</el-select> </el-select>
</el-form-item> </el-form-item>

View File

@ -522,7 +522,6 @@ const formatDate = (dateString) => {
const beforeImageUpload = (file) => { const beforeImageUpload = (file) => {
const isImage = file.type.startsWith('image/') const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5 const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) { if (!isImage) {
ElMessage.error(t('admin.productManagement.imageTypeError')) ElMessage.error(t('admin.productManagement.imageTypeError'))
return false return false
@ -539,12 +538,12 @@ const handleImageChange = (file) => {
if (!beforeImageUpload(file.raw)) { if (!beforeImageUpload(file.raw)) {
return return
} }
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
const imageUrl = e.target.result const imageUrl = e.target.result
fileServer.uploadFile(imageUrl).then((url) => { fileServer.uploadFile(imageUrl).then((url) => {
formData.value.image = url formData.value.image = url
window?.closeMethods?.close();
ElMessage.success(t('admin.productManagement.imageUploadSuccess')) ElMessage.success(t('admin.productManagement.imageUploadSuccess'))
}).catch((error) => { }).catch((error) => {
console.error('图片上传失败:', error) console.error('图片上传失败:', error)

View File

@ -34,8 +34,8 @@ export default defineConfig({
// 配置代理解决CORS问题 // 配置代理解决CORS问题
proxy: { proxy: {
'/api': { '/api': {
target: 'https://api.deotaland.ai', // target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local', target: 'http://api.deotaland.local',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }

View File

@ -15,6 +15,7 @@
"@deotaland/utils": "workspace:*", "@deotaland/utils": "workspace:*",
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0", "@google/genai": "^1.27.0",
"@splinetool/runtime": "^1.12.29",
"@twind/core": "^1.1.3", "@twind/core": "^1.1.3",
"@twind/preset-autoprefix": "^1.0.7", "@twind/preset-autoprefix": "^1.0.7",
"@twind/preset-tailwind": "^1.1.4", "@twind/preset-tailwind": "^1.1.4",

View File

@ -47,6 +47,7 @@ window.setElLoading = (qp=false)=>{
}else{ }else{
loading.value = true loading.value = true
} }
window.closeMethods = closeMethods;
return closeMethods return closeMethods
} }
window.closeMethods = closeMethods; window.closeMethods = closeMethods;

View File

@ -1,559 +0,0 @@
<template>
<el-dialog
v-model="dialogVisible"
title="局部修改"
width="90%"
:fullscreen="isFullscreen"
append-to-body
destroy-on-close
@close="handleClose"
>
<div class="canvas-editor-container">
<div class="canvas-toolbar">
<div class="toolbar-section">
<span class="toolbar-label">画笔颜色:</span>
<div class="color-picker">
<div
v-for="color in colors"
:key="color"
:class="['color-option', { active: currentColor === color }]"
:style="{ backgroundColor: color }"
@click="selectColor(color)"
></div>
<el-color-picker
v-model="customColor"
size="small"
@change="selectCustomColor"
class="custom-color-picker"
></el-color-picker>
</div>
</div>
<div class="toolbar-section">
<span class="toolbar-label">画笔大小:</span>
<el-slider
v-model="brushSize"
:min="1"
:max="50"
style="width: 120px"
/>
</div>
<div class="toolbar-section">
<el-button
:type="isEraser ? 'primary' : 'default'"
size="small"
@click="toggleEraser"
>
<el-icon><Delete /></el-icon>
橡皮擦
</el-button>
<el-button
type="warning"
size="small"
@click="clearCanvas"
>
<el-icon><RefreshLeft /></el-icon>
清空
</el-button>
<el-button
type="success"
size="small"
@click="handleSave"
>
<el-icon><Check /></el-icon>
确定
</el-button>
</div>
</div>
<div class="canvas-wrapper" ref="canvasWrapper">
<div v-if="imageLoading" class="loading-overlay">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载图片中...</span>
</div>
<div v-if="imageLoadError" class="error-overlay">
<el-icon><Warning /></el-icon>
<p>图片加载失败</p>
<el-button type="primary" size="small" @click="retryLoadImage">重试</el-button>
<el-button size="small" @click="dialogVisible = false">关闭</el-button>
</div>
<canvas
v-show="!imageLoading && !imageLoadError"
ref="canvas"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</div>
<div class="edit-textarea-container">
<el-input
v-model="editContent"
type="textarea"
:rows="3"
placeholder="请输入要修改的内容"
class="edit-textarea"
></el-input>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { Delete, RefreshLeft, Check, Loading, Warning } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
imageUrl: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:visible', 'add-prompt-card', 'close'])
const dialogVisible = ref(false)
const isFullscreen = ref(false)
const canvas = ref(null)
const canvasWrapper = ref(null)
const ctx = ref(null)
const isDrawing = ref(false)
const isEraser = ref(false)
const currentColor = ref('#6B46C1')
const customColor = ref('#6B46C1')
const brushSize = ref(5)
const imageLoading = ref(false)
const imageLoadError = ref(false)
const loadRetryCount = ref(0)
const maxRetryCount = 3
const editContent = ref('')
const colors = [
'#6B46C1',
'#EF4444',
'#F59E0B',
'#10B981',
'#3B82F6',
'#8B5CF6',
'#EC4899',
'#1F2937'
]
let lastX = 0
let lastY = 0
let backgroundImage = null
watch(() => props.visible, (val) => {
dialogVisible.value = val
if (val) {
nextTick(() => {
initCanvas()
})
}
})
watch(dialogVisible, (val) => {
emit('update:visible', val)
})
const initCanvas = async () => {
if (!canvas.value || !canvasWrapper.value) return
console.log('记载');
imageLoading.value = true
imageLoadError.value = false
try {
const img = new Image()
img.crossOrigin = 'anonymous'
const objectURL = await fetchImage(props.imageUrl)
await new Promise((resolve, reject) => {
img.onload = () => resolve()
img.onerror = () => reject(new Error('Failed to load image'))
img.src = objectURL
})
const canvasEl = canvas.value
canvasEl.width = img.width
canvasEl.height = img.height
ctx.value = canvasEl.getContext('2d')
backgroundImage = img
drawImage()
imageLoading.value = false
loadRetryCount.value = 0
} catch (error) {
console.error('Failed to load image:', props.imageUrl, error)
imageLoading.value = false
imageLoadError.value = true
ElMessage.error('图片加载失败,请重试')
}
}
const retryLoadImage = () => {
if (loadRetryCount.value < maxRetryCount) {
loadRetryCount.value++
ElMessage.info(`正在重试加载图片 (${loadRetryCount.value}/${maxRetryCount})`)
initCanvas()
} else {
ElMessage.warning('已达到最大重试次数,请稍后再试')
}
}
const drawImage = () => {
if (!ctx.value || !backgroundImage || !canvas.value) return
const canvasEl = canvas.value
const img = backgroundImage
ctx.value.fillStyle = '#FFFFFF'
ctx.value.fillRect(0, 0, canvasEl.width, canvasEl.height)
ctx.value.drawImage(img, 0, 0, img.width, img.height)
}
const selectColor = (color) => {
currentColor.value = color
isEraser.value = false
}
const selectCustomColor = (color) => {
if (color) {
currentColor.value = color
isEraser.value = false
}
}
const toggleEraser = () => {
isEraser.value = !isEraser.value
}
const clearCanvas = () => {
if (!ctx.value || !canvas.value) return
ctx.value.fillStyle = '#FFFFFF'
ctx.value.fillRect(0, 0, canvas.value.width, canvas.value.height)
if (backgroundImage) {
ctx.value.drawImage(backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height)
}
}
const getCanvasCoordinates = (clientX, clientY) => {
const rect = canvas.value.getBoundingClientRect()
const scaleX = canvas.value.width / rect.width
const scaleY = canvas.value.height / rect.height
return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
}
}
const startDrawing = (e) => {
isDrawing.value = true
const coords = getCanvasCoordinates(e.clientX, e.clientY)
lastX = coords.x
lastY = coords.y
}
const draw = (e) => {
if (!isDrawing.value || !ctx.value) return
const coords = getCanvasCoordinates(e.clientX, e.clientY)
ctx.value.beginPath()
ctx.value.moveTo(lastX, lastY)
ctx.value.lineTo(coords.x, coords.y)
ctx.value.strokeStyle = isEraser.value ? '#FFFFFF' : currentColor.value
ctx.value.lineWidth = brushSize.value
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.stroke()
lastX = coords.x
lastY = coords.y
}
const stopDrawing = () => {
isDrawing.value = false
}
const handleTouchStart = (e) => {
e.preventDefault()
const touch = e.touches[0]
const coords = getCanvasCoordinates(touch.clientX, touch.clientY)
lastX = coords.x
lastY = coords.y
isDrawing.value = true
}
async function fetchImage(url) {
const cacheBusterUrl = url + '?v=1.0.0';
const response = await fetch(cacheBusterUrl, {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'no-cache'
});
const blob = await response.blob();
const imgurl = URL.createObjectURL(blob);
console.log(imgurl,'imgurlimgurlimgurl');
return imgurl;
}
const handleTouchMove = (e) => {
e.preventDefault()
if (!isDrawing.value || !ctx.value) return
const touch = e.touches[0]
const coords = getCanvasCoordinates(touch.clientX, touch.clientY)
ctx.value.beginPath()
ctx.value.moveTo(lastX, lastY)
ctx.value.lineTo(coords.x, coords.y)
ctx.value.strokeStyle = isEraser.value ? '#FFFFFF' : currentColor.value
ctx.value.lineWidth = brushSize.value
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.stroke()
lastX = coords.x
lastY = coords.y
}
const handleTouchEnd = (e) => {
e.preventDefault()
isDrawing.value = false
}
const handleSave = () => {
const dataUrl = canvas.value.toDataURL('image/png')
emit('add-prompt-card', dataUrl,editContent.value)
dialogVisible.value = false
}
const handleClose = () => {
emit('close')
}
const handleResize = () => {
if (dialogVisible.value) {
nextTick(() => {
initCanvas()
})
}
}
onMounted(() => {
window.addEventListener('resize', handleResize)
if (window.innerWidth < 768) {
isFullscreen.value = true
}
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.canvas-editor-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.canvas-toolbar {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.toolbar-section {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-label {
font-size: 14px;
font-weight: 500;
color: #1F2937;
}
.color-picker {
display: flex;
gap: 8px;
}
.color-option {
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: #1F2937;
box-shadow: 0 0 0 2px rgba(107, 70, 193, 0.3);
}
.custom-color-picker {
width: 24px;
height: 24px;
}
.custom-color-picker :deep(.el-color-picker__trigger) {
width: 24px;
height: 24px;
padding: 0;
border: none;
}
.custom-color-picker :deep(.el-color-picker__color) {
border-radius: 50%;
border: 2px solid transparent;
}
.canvas-wrapper {
width: 100%;
height: 500px;
background: #f3f4f6;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e2e8f0;
position: relative;
}
.loading-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: #f3f4f6;
z-index: 10;
}
.loading-overlay .el-icon {
font-size: 32px;
color: #6B46C1;
}
.loading-overlay span {
font-size: 14px;
color: #6b7280;
}
.error-overlay .el-icon {
font-size: 48px;
color: #EF4444;
}
.error-overlay p {
font-size: 16px;
color: #1F2937;
margin: 0;
}
canvas {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
cursor: crosshair;
display: block;
}
.edit-textarea-container {
width: 100%;
margin-top: 12px;
}
.edit-textarea {
width: 100%;
}
@media (max-width: 768px) {
.canvas-toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-section {
flex-wrap: wrap;
justify-content: center;
}
.canvas-wrapper {
height: 400px;
}
.color-picker {
flex-wrap: wrap;
justify-content: center;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.canvas-wrapper {
height: 450px;
}
}
[data-theme="dark"] .canvas-toolbar {
background: #1f1f1f;
border-color: #303030;
}
[data-theme="dark"] .toolbar-label {
color: #e5e7eb;
}
[data-theme="dark"] .canvas-wrapper {
background: #141414;
border-color: #303030;
}
[data-theme="dark"] .loading-overlay,
[data-theme="dark"] .error-overlay {
background: #141414;
}
[data-theme="dark"] .loading-overlay span,
[data-theme="dark"] .error-overlay p {
color: #e5e7eb;
}
[data-theme="dark"] .color-option.active {
border-color: #e5e7eb;
}
</style>

View File

@ -37,8 +37,7 @@
<div class="guide-text-container"> <div class="guide-text-container">
<div class="text-content"> <div class="text-content">
<h2 class="guide-title">{{ step.title }}</h2> <h2 class="guide-title">{{ step.title }}</h2>
<p class="guide-description">{{ step.description }}</p> <p class="guide-description" v-html="step.description"></p>
<!-- 额外提示信息 --> <!-- 额外提示信息 -->
<div v-if="step.tips" class="guide-tips"> <div v-if="step.tips" class="guide-tips">
<div class="tips-icon">💡</div> <div class="tips-icon">💡</div>
@ -113,30 +112,30 @@ const guideSteps = computed(() => [
id: 1, id: 1,
title: t('guideModal.step1.title'), title: t('guideModal.step1.title'),
description: t('guideModal.step1.description'), description: t('guideModal.step1.description'),
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/de7142df-ceb9-48f9-9367-af2a65e786a5.png', image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/f1cde4d7-bafd-41cb-8795-ad9ddcd521fb.png',
tips: t('guideModal.step1.tips') tips: t('guideModal.step1.tips')
}, },
{ {
id: 2, id: 2,
title: t('guideModal.step2.title'), title: t('guideModal.step2.title'),
description: t('guideModal.step2.description'), description: t('guideModal.step2.description'),
image:'https://draft-user.s3.us-east-2.amazonaws.com/images/40773ee8-7f85-40c9-8c23-7ea1718e58a8.png', image:'https://draft-user.s3.us-east-2.amazonaws.com/images/2ec4b422-3b42-4447-b049-6342184a5c50.png',
tips: t('guideModal.step2.tips') tips: t('guideModal.step2.tips')
}, },
{ // {
id: 3, // id: 3,
title: t('guideModal.step3.title'), // title: t('guideModal.step3.title'),
description: t('guideModal.step3.description'), // description: t('guideModal.step3.description'),
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/47cdd95f-23fb-486b-bd19-5fca67c2ce45.png', // image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/47cdd95f-23fb-486b-bd19-5fca67c2ce45.png',
tips: t('guideModal.step3.tips') // tips: t('guideModal.step3.tips')
}, // },
{ // {
id: 4, // id: 4,
title: t('guideModal.step4.title'), // title: t('guideModal.step4.title'),
description: t('guideModal.step4.description'), // description: t('guideModal.step4.description'),
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/690f2da4-400e-4cb0-a815-246e614d79b1.png', // image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/690f2da4-400e-4cb0-a815-246e614d79b1.png',
tips: t('guideModal.step4.tips') // tips: t('guideModal.step4.tips')
} // }
]); ]);
// //
@ -312,7 +311,7 @@ const skipGuide = () => {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 350px; max-height:350px;
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
@ -321,7 +320,7 @@ const skipGuide = () => {
.guide-image { .guide-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: contain;
transition: transform 0.5s ease; transition: transform 0.5s ease;
} }

View File

@ -42,6 +42,9 @@
<MagicStick /> <MagicStick />
</el-icon> </el-icon>
<span class="count-text">{{ t('header.remainingCredits') }}: {{ total_score }}</span> <span class="count-text">{{ t('header.remainingCredits') }}: {{ total_score }}</span>
<button class="recharge-btn" @click="handleRecharge" :title="t('header.recharge')">
<span class="recharge-text">{{ t('header.recharge') }}</span>
</button>
</div> </div>
</div> </div>
<LanguageToggle <LanguageToggle
@ -64,6 +67,7 @@
<script setup> <script setup>
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue' import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { Picture, MagicStick, 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 { ElButton, ElIcon, ElInput } from 'element-plus'
import ThemeToggle from '../ui/ThemeToggle.vue' import ThemeToggle from '../ui/ThemeToggle.vue'
@ -79,14 +83,17 @@ const props = defineProps({
default: 'project' default: 'project'
} }
}) })
// const router = useRouter()
const { t, locale } = useI18n()
const handleRecharge = () => {
router.push('/points-recharge')
}
const isEditing = ref(false) const isEditing = ref(false)
const editedProjectName = ref('') const editedProjectName = ref('')
const editInput = ref(null) const editInput = ref(null)
//
const { t, locale } = useI18n()
// LanguageToggle // LanguageToggle
// //
const handleBack = () => { const handleBack = () => {
@ -412,6 +419,48 @@ html.dark .project-name-input :deep(.el-input__wrapper:focus) {
white-space: nowrap; white-space: nowrap;
} }
.recharge-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
border: none;
background-color: #6B46C1;
color: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 11px;
font-weight: 500;
margin-left: 4px;
}
.recharge-btn:hover {
background-color: #7C3AED;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
}
.recharge-btn:active {
transform: translateY(0);
}
.recharge-text {
font-size: 11px;
font-weight: 500;
color: white;
white-space: nowrap;
}
html.dark .recharge-btn {
background-color: #8B5CF6;
}
html.dark .recharge-btn:hover {
background-color: #A78BFA;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4);
}
/* 指南按钮样式 */ /* 指南按钮样式 */
.guide-btn { .guide-btn {
display: flex; display: flex;
@ -508,6 +557,11 @@ html.dark .count-text {
padding: 6px 10px; padding: 6px 10px;
} }
.recharge-btn {
padding: 3px 8px;
font-size: 10px;
}
.count-icon { .count-icon {
font-size: 14px; font-size: 14px;
} }

View File

@ -15,7 +15,7 @@
ref="textInputRef" ref="textInputRef"
v-model="textInputValue" v-model="textInputValue"
class="text-input-area" class="text-input-area"
placeholder="请输入文本内容..." :placeholder="t('modelModal.textInputPlaceholder')"
rows="4" rows="4"
@keydown.enter.ctrl="handleTextInputConfirm" @keydown.enter.ctrl="handleTextInputConfirm"
@keydown.esc="handleTextInputCancel" @keydown.esc="handleTextInputCancel"
@ -53,6 +53,9 @@
<!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> --> <!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> -->
</template> </template>
</el-skeleton> </el-skeleton>
<button v-if="formData.internalImageUrl" class="customize-to-home-btn" @click="handleCustomizeToHome" @touchend.prevent="handleCustomizeToHome">
{{ t('modelModal.customizeToHome') }}
</button>
</div> </div>
</div> </div>
<!-- 右侧控件区域 --> <!-- 右侧控件区域 -->
@ -71,6 +74,10 @@
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="scene graph" @click="handleTextcjt" @touchend.prevent="handleTextcjt"> <button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="scene graph" @click="handleTextcjt" @touchend.prevent="handleTextcjt">
<el-icon class="btn-icon"><Grid /></el-icon> <el-icon class="btn-icon"><Grid /></el-icon>
</button> </button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="Picture board editor" @click="handlePartialEdit" @touchend.prevent="handlePartialEdit">
<el-icon class="btn-icon"><EditPen /></el-icon>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -80,6 +87,7 @@
import {cjt} from './tsc.js' import {cjt} from './tsc.js'
// import cjimg from '@/assets/sketches/cjt.png'; // import cjimg from '@/assets/sketches/cjt.png';
import { computed, ref, onMounted, watch, nextTick } from 'vue'; import { computed, ref, onMounted, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { GiminiServer } from '@deotaland/utils'; import { GiminiServer } from '@deotaland/utils';
// import humanTypeImg from '@/assets/sketches/tcww.png' // import humanTypeImg from '@/assets/sketches/tcww.png'
// import humanTypeImg from '@/assets/sketches/tcww2.webp' // import humanTypeImg from '@/assets/sketches/tcww2.webp'
@ -87,8 +95,9 @@ import { GiminiServer } from '@deotaland/utils';
// import anTypeImg from '@/assets/sketches/dwww2.png'; // import anTypeImg from '@/assets/sketches/dwww2.png';
// import cz2 from '@/assets/material/cz2.png'; // import cz2 from '@/assets/material/cz2.png';
// Element Plus // Element Plus
import { Cpu, ChatDotRound, CloseBold,Grid,View } from '@element-plus/icons-vue' import { Cpu, ChatDotRound, CloseBold,Grid,View,EditPen } from '@element-plus/icons-vue'
import { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus' import { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus'
const { t } = useI18n();
const formData = ref({ const formData = ref({
internalImageUrl: '',//URL internalImageUrl: '',//URL
status:'loading',// status:'loading',//
@ -138,6 +147,12 @@ const handleTextcjt = ()=>{
cardData: props.cardData cardData: props.cardData
}); });
} }
const handleCustomizeToHome = () => {
emit('customize-to-home', {
imageUrl: formData.value.internalImageUrl,
cardData: props.cardData
});
}
// //
const handleTextInputConfirm = () => { const handleTextInputConfirm = () => {
// //
@ -173,7 +188,7 @@ const handleTouchEnd = () => {
}; };
// //
const props = defineProps({ const props = defineProps({
combinedPromptJson:{ combinedPromptJson:{//
type: Object, type: Object,
default: () => ({}) default: () => ({})
}, },
@ -210,8 +225,10 @@ const props = defineProps({
}); });
// //
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete','preview-image']); const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete','preview-image','customize-to-home','handlePartialEdit']);
const handlePartialEdit = ()=>{
emit('handlePartialEdit', formData.value.internalImageUrl);
}
// //
const handleGenerateImage = async () => { const handleGenerateImage = async () => {
const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1; const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
@ -222,72 +239,32 @@ const handleGenerateImage = async () => {
if (props?.cardData?.inspirationImage) { if (props?.cardData?.inspirationImage) {
referenceImages.push(props.cardData.inspirationImage); referenceImages.push(props.cardData.inspirationImage);
} }
if(props?.cardData?.ipType){
if(props?.cardData?.ipType==1){
humanTypeImg&&referenceImages.push(humanTypeImg);
}else{
anTypeImg&&referenceImages.push(anTypeImg);
}
}
if(iscjt){ if(iscjt){
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt); props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
referenceImages.push(props.cardData.diyPromptImg); referenceImages.push(props.cardData.diyPromptImg);
referenceImages.push(cjimg); referenceImages.push(cjimg);
} }
// if(props.cardData.diyPromptText){ if(props.cardData.diyPromptText){
// console.log(props.cardData.diyPromptImg,'diyPromptImgdiyPromptImgdiyPromptImg'); referenceImages.push(props.cardData.diyPromptImg);
// referenceImages.push(props.cardData.diyPromptImg); }
// if(iscjt){ let dtprompt;
// props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt); if(props?.cardData?.ipType==1){
// referenceImages.push(cjimg); dtprompt = props.combinedPromptJson.person.content;
// } referenceImages.push(...props.combinedPromptJson.person.imgs);
// }else{ }else if(props?.cardData?.ipType==2){
// referenceImages.push(humanTypeImg); dtprompt = props.combinedPromptJson.animal.content;
// referenceImages.push(anTypeImg); referenceImages.push(...props.combinedPromptJson.animal.imgs);
// } }
// referenceImages.push(cz2); if(props.cardData.prompt){
// referenceImages.push(humanTypeImg); dtprompt = `角色外观:${props.cardData.prompt}.${dtprompt}`
// if(props?.cardData?.selectedExpression){ }
// referenceImages.push(props.cardData.selectedExpression.imageUrl); let prompt = props.cardData.diyPromptText|| dtprompt
// }
//
let prompt = props.cardData.diyPromptText|| `
首先保证生成的角色符合以下要求
角色肤色和衣服材质都为纯色一种颜色如下
重点保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
一个通体由单一纯色木材雕刻而成的角色全身无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计.
A full-body character portrait
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征).
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
Note: The image should not have white borders.
去除原图中复杂的背景只保留人物角色的主体
适配3D打印请保持服装边缘装饰等细节略微加厚避免过细结构以提高打印稳定性手指头轮廓清晰重点保证角色全身包括衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
3D打印结构优化
模型用于3D打印必须保持结构厚实稳定无细小悬空部件或过薄结构
不生成透明或复杂内构
保持厚度和连贯性适合打印
材质处理
整体需光滑稳固边缘柔和防止打印时断裂
模型应呈现专业3D效果
${props.cardData?.ipType==1?`
调整角色的发型使其厚实蓬松且结构坚固轮廓清晰扎实适合3D打印
确保头发具备足够的厚度与结构完整性避免在打印过程中出现脆弱断裂同时保留原有的可爱美感
头发纹理细节需针对3D制造进行优化层次平滑且分明兼顾视觉吸引力与可打印性维持整体俏皮且高品质的盲盒角色风格
`:`采用疯狂动物城的设计风格`}
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
保证生成的图片一定要有眼睛一定要有嘴巴,眼睛效果要可爱童真Q版大眼睛
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
衣服如果不适合做木制一定要简化衣服不能用复杂的衣服设计保留衣服特征即可衣服一定要纯色木质材质
保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
`
;
// 姿${props.cardData.ipType==1?``:``} // 姿${props.cardData.ipType==1?``:``}
if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){ if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){
prompt = '按原图生成' prompt = '按原图生成'
referenceImages = [props.cardData.inspirationImage]; referenceImages = [props.cardData.inspirationImage];
formData.value.internalImageUrl = props?.cardData?.inspirationImage;
return
} }
const taskResult = await giminiServer.handleGenerateImage(referenceImages, prompt,{ const taskResult = await giminiServer.handleGenerateImage(referenceImages, prompt,{
project_id: props.cardData.project_id, project_id: props.cardData.project_id,
@ -301,6 +278,7 @@ const handleGenerateImage = async () => {
formData.value.status = 'success'; formData.value.status = 'success';
saveProject(); saveProject();
} catch (error) { } catch (error) {
console.log(error);
emit('delete'); emit('delete');
} }
}; };
@ -686,6 +664,77 @@ const handleImageLoad = (event) => {
border: 2px solid rgba(167, 139, 250, 0.2); border: 2px solid rgba(167, 139, 250, 0.2);
} }
.ip-card-image {
width: 100%;
height: 100%;
display: block;
}
.customize-to-home-btn {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
min-width: 120px;
padding: 10px 24px;
border-radius: 8px;
border: 1px solid rgba(139, 92, 246, 0.25);
background: rgba(107, 70, 193, 0.9);
color: #ffffff;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
opacity: 0;
pointer-events: none;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(107, 70, 193, 0.3);
z-index: 5;
white-space: nowrap;
}
.ip-card-container:hover .customize-to-home-btn,
.ip-card-container.controls-visible .customize-to-home-btn {
opacity: 1;
pointer-events: auto;
}
.customize-to-home-btn:hover {
background: rgba(139, 92, 246, 1);
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 6px 20px rgba(107, 70, 193, 0.4);
transform: translateX(-50%) translateY(-2px);
}
:global(.dark) .customize-to-home-btn {
background: rgba(139, 92, 246, 0.85);
border-color: rgba(167, 139, 250, 0.4);
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
}
:global(.dark) .customize-to-home-btn:hover {
background: rgba(167, 139, 250, 0.95);
border-color: rgba(167, 139, 250, 0.6);
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5);
}
@media (hover: none) and (pointer: coarse) {
.ip-card-container:active .customize-to-home-btn,
.ip-card-container.controls-visible .customize-to-home-btn {
opacity: 1;
pointer-events: auto;
}
}
@media (max-width: 768px) {
.customize-to-home-btn {
padding: 8px 20px;
font-size: 13px;
bottom: 12px;
}
}
/* 右侧控件容器 - 使用绝对定位避免布局重排 */ /* 右侧控件容器 - 使用绝对定位避免布局重排 */
.right-controls-container { .right-controls-container {
position: absolute; position: absolute;

View File

@ -53,31 +53,43 @@
<!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> --> <!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> -->
</template> </template>
</el-skeleton> </el-skeleton>
<button v-if="formData.internalImageUrl" class="customize-to-home-btn" @click="handleCustomizeToHome" @touchend.prevent="handleCustomizeToHome"> <button v-if="formData.internalImageUrl&&!(props.cardData.diyPromptText==cjt)" class="customize-to-home-btn" @click="handleCustomizeToHome" @touchend.prevent="handleCustomizeToHome">
{{ t('modelModal.customizeToHome') }} {{ t('modelModal.customizeToHome') }}
</button> </button>
</div> </div>
</div> </div>
<!-- 右侧控件区域 --> <!-- 上侧控件区域 -->
<div class="right-controls-container" v-if="formData.internalImageUrl"> <div class="top-controls-container" v-if="formData.internalImageUrl">
<!-- 右侧圆形按钮控件 --> <!-- 上侧按钮控件 -->
<div class="right-circular-controls"> <div class="top-controls">
<button class="control-button share-btn" title="Preview Image" @click="handleImageClick" @touchend.prevent="handleImageClick"> <button class="control-item" title="Preview Image" @click="handleImageClick" @touchend.prevent="handleImageClick">
<el-icon class="btn-icon"><View /></el-icon> <span class="control-text">{{ t('modelModal.preview') }}</span>
<el-icon class="control-icon"><View /></el-icon>
</button> </button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="model" @click="handleGenerateModel" @touchend.prevent="handleGenerateModel"> <!-- <button v-if="!(props.cardData.imgyt)" class="control-item" title="model" @click="handleGenerateModel" @touchend.prevent="handleGenerateModel">
<el-icon class="btn-icon"><Cpu /></el-icon> <span class="control-text">模型</span>
<el-icon class="control-icon"><Cpu /></el-icon>
</button> -->
<button v-if="!(props.cardData.diyPromptText==cjt)" class="control-item" title="textInput" @click="toggleTextInput" @touchend.prevent="toggleTextInput">
<span class="control-text">{{ t('modelModal.modify') }}</span>
<el-icon class="control-icon"><ChatDotRound /></el-icon>
</button> </button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="textInput" @click="toggleTextInput" @touchend.prevent="toggleTextInput"> <button v-if="!(props.cardData.diyPromptText==cjt)" class="control-item" title="Picture board editor" @click="handlePartialEdit" @touchend.prevent="handlePartialEdit">
<el-icon class="btn-icon"><ChatDotRound /></el-icon> <span class="control-text">{{ t('modelModal.edit') }}</span>
<el-icon class="control-icon"><EditPen /></el-icon>
</button> </button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="scene graph" @click="handleTextcjt" @touchend.prevent="handleTextcjt"> <button v-if="!(props.cardData.diyPromptText==cjt)" class="control-item" title="scene graph" @click="handleTextcjt" @touchend.prevent="handleTextcjt">
<el-icon class="btn-icon"><Grid /></el-icon> <span class="control-text">{{ t('modelModal.sceneGraph') }}</span>
<el-icon class="control-icon"><Grid /></el-icon>
</button> </button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="Picture board editor" @click="handlePartialEdit" @touchend.prevent="handlePartialEdit"> <button class="control-item" title="Download" @click="downloadImage" @touchend.prevent="downloadImage">
<el-icon class="btn-icon"><EditPen /></el-icon> <span class="control-text">{{ t('modelModal.download') }}</span>
<el-icon class="control-icon"><Download /></el-icon>
</button>
<button class="control-item" title="Delete" @click="handleDeleteCard" @touchend.prevent="handleDeleteCard">
<span class="control-text">{{ t('common.close') }}</span>
<el-icon class="control-icon"><Delete /></el-icon>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
@ -95,7 +107,7 @@ import { GiminiServer } from '@deotaland/utils';
// import anTypeImg from '@/assets/sketches/dwww2.png'; // import anTypeImg from '@/assets/sketches/dwww2.png';
// import cz2 from '@/assets/material/cz2.png'; // import cz2 from '@/assets/material/cz2.png';
// Element Plus // Element Plus
import { Cpu, ChatDotRound, CloseBold,Grid,View,EditPen } from '@element-plus/icons-vue' import { Cpu, ChatDotRound, CloseBold,Grid,View,EditPen,Download,Delete } from '@element-plus/icons-vue'
import { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus' import { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus'
const { t } = useI18n(); const { t } = useI18n();
const formData = ref({ const formData = ref({
@ -225,10 +237,17 @@ const props = defineProps({
}); });
// //
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete','preview-image','customize-to-home','handlePartialEdit']); const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete','preview-image','customize-to-home','handlePartialEdit','download-image','delete-card']);
const handlePartialEdit = ()=>{ const handlePartialEdit = ()=>{
emit('handlePartialEdit', formData.value.internalImageUrl); emit('handlePartialEdit', formData.value.internalImageUrl);
} }
//
const handleDeleteCard = ()=>{
emit('delete-card');
}
const downloadImage = async ()=>{
emit('download-image', formData.value.internalImageUrl);
}
// //
const handleGenerateImage = async () => { const handleGenerateImage = async () => {
const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1; const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
@ -735,90 +754,107 @@ const handleImageLoad = (event) => {
} }
} }
/* 侧控件容器 - 使用绝对定位避免布局重排 */ /* 侧控件容器 - 使用绝对定位避免布局重排 */
.right-controls-container { .top-controls-container {
position: absolute; position: absolute;
left: 100%; top: 100%;
top: 50%; left: 50%;
transform: translateY(-50%); transform: translateX(-50%);
margin-left: 24px; margin-top: 16px;
z-index: 10; z-index: 10;
} }
/* 右侧圆形按钮控件 */ /* 上侧按钮控件 */
.right-circular-controls { .top-controls {
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: flex-start; gap: 12px;
gap: 16px; padding: 12px 16px;
background: linear-gradient(135deg, rgba(107, 70, 193, 0.95) 0%, rgba(139, 92, 246, 0.9) 100%);
border-radius: 12px;
border: 1px solid rgba(167, 139, 250, 0.3);
box-shadow: 0 8px 24px rgba(107, 70, 193, 0.3);
opacity: 0; opacity: 0;
transform: translateX(-20px); transform: translateY(-10px);
transition: all 0.3s ease; transition: all 0.3s ease;
pointer-events: none; pointer-events: none;
backdrop-filter: blur(8px);
} }
/* 当鼠标悬停在卡片容器上时显示控件 */ /* 当鼠标悬停在卡片容器上时显示控件 */
.ip-card-container:hover .right-circular-controls { .ip-card-container:hover .top-controls {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateY(0);
pointer-events: auto; pointer-events: auto;
} }
/* 在 iPad 及更大触控屏上,按下即显示右侧圆形控件 */ /* 在 iPad 及更大触控屏上,按下即显示上侧控件 */
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
.ip-card-container:active .right-circular-controls, .ip-card-container:active .top-controls,
.ip-card-container.controls-visible .right-circular-controls { .ip-card-container.controls-visible .top-controls {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateY(0);
pointer-events: auto; pointer-events: auto;
} }
} }
/* 非触控设备保持 hover 显示 */ /* 非触控设备保持 hover 显示 */
@media (hover: hover) { @media (hover: hover) {
.ip-card-container:hover .right-circular-controls { .ip-card-container:hover .top-controls {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateY(0);
pointer-events: auto; pointer-events: auto;
} }
} }
/* 移动端适配:点击卡片后显示功能按钮 */ /* 移动端适配:点击卡片后显示功能按钮 */
@media (max-width: 1024px) { @media (max-width: 1024px) {
/* 点击卡片容器时显示功能按钮 */ /* 点击卡片容器时显示功能按钮 */
.ip-card-container:active .right-circular-controls, .ip-card-container:active .top-controls,
.ip-card-container.controls-visible .right-circular-controls { .ip-card-container.controls-visible .top-controls {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateY(0);
pointer-events: auto; pointer-events: auto;
} }
} }
.control-button { .control-item {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(167, 139, 250, 0.15) 0%, rgba(107, 70, 193, 0.1) 100%);
border: 1px solid rgba(167, 139, 250, 0.3);
color: #A78BFA;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 6px;
padding: 8px 12px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
.control-button:hover { .control-item:hover {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.25) 0%, rgba(107, 70, 193, 0.2) 100%); background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
} }
.control-button:active { .control-item:active {
transform: translateY(0); transform: translateY(0);
} }
.btn-icon { .control-text {
font-size: 20px; font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.control-icon {
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
} }
.ip-card:hover { .ip-card:hover {
@ -1016,21 +1052,32 @@ const handleImageLoad = (event) => {
transform: none; transform: none;
} }
.right-controls-container { .top-controls-container {
position: static; position: static;
transform: none; transform: none;
margin-left: 0;
margin-top: 16px; margin-top: 16px;
} }
.right-circular-controls { .top-controls {
flex-direction: row; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center;
margin-bottom: 16px;
opacity: 1; opacity: 1;
transform: none; transform: none;
pointer-events: auto; pointer-events: auto;
padding: 10px 12px;
}
.control-item {
padding: 6px 10px;
font-size: 12px;
}
.control-text {
font-size: 12px;
}
.control-icon {
font-size: 16px;
} }
.right-actions-controls { .right-actions-controls {

View File

@ -258,7 +258,8 @@ onMounted(()=>{
border-radius: 12px; border-radius: 12px;
margin-bottom: 16px; margin-bottom: 16px;
position: relative; position: relative;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); /* background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); */
background-color: #fdfdfd;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -800,9 +800,11 @@ const handleFileChange = async (event) => {
try { try {
const imgUrl = await filePlug.uploadFile(file); const imgUrl = await filePlug.uploadFile(file);
formData.value.previewImage = imgUrl; formData.value.previewImage = imgUrl;
window?.closeMethods.close();
} catch (error) { } catch (error) {
console.error('图片上传失败:', error); console.error('图片上传失败:', error);
ElMessage.error('图片上传失败,请重试'); ElMessage.error('图片上传失败,请重试');
window?.closeMethods.close();
} finally { } finally {
isUploading.value = false; isUploading.value = false;
} }

View File

@ -643,7 +643,7 @@ onUnmounted(() => {
left: 0; left: 0;
top: 0; top: 0;
height: 100vh; height: 100vh;
z-index: 1000; z-index: 20;
transform: translateX(-100%); transform: translateX(-100%);
box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2); box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2);
/* transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); */ /* transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); */

View File

@ -244,7 +244,7 @@ watch(() => window.location.pathname, () => {
top: 0; top: 0;
/* left: -120px; */ /* left: -120px; */
transform: translateX(-100%); transform: translateX(-100%);
z-index: 999; z-index: 99;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 60px; width: 60px;
} }

File diff suppressed because it is too large Load Diff

View File

@ -125,7 +125,12 @@ export default {
}, },
modelModal: { modelModal: {
customizeToHome: '定制到家', customizeToHome: '定制到家',
textInputPlaceholder: '请输入调整内容,例如:更改角色表情' textInputPlaceholder: '请输入调整内容,例如:更改角色表情',
preview: '预览',
modify: '修改',
sceneGraph: '场景图',
edit: '编辑',
download: '下载'
}, },
modelCard: { modelCard: {
generateModelButton: '生成模型', generateModelButton: '生成模型',
@ -212,6 +217,7 @@ export default {
modelFreeCount: '模型免费', modelFreeCount: '模型免费',
times: '次', times: '次',
remainingCredits: '剩余积分', remainingCredits: '剩余积分',
recharge: '充值',
guide: '使用指南', guide: '使用指南',
back: '返回', back: '返回',
skip: '跳过', skip: '跳过',
@ -334,13 +340,16 @@ export default {
guideModal: { guideModal: {
step1: { step1: {
title: '参考图片', title: '参考图片',
description: '选择您喜欢的图片作为创作参考', description: '输入文字描述您的IP或者选择您喜欢的图片作为创作参考',
tips: '点击生成按钮后,平台会根据您的选择生成相应的3D模型。' tips: '点击生成按钮后,平台会根据您的选择生成相应的图片。'
}, },
step2: { step2: {
title: '模型生成/文字优化', title: '图片节点按钮控件',
description: '根据您的参考图片平台会生成对应的3D模型。', description: `
tips: '您也可以输入文字描述,平台会根据您的需求进行图片优化。' .<text style="font-weight: bold;">customizeToHome</text>将您的IP角色定制为您的专属3D模型机器人。<br/>
.<text style="font-weight: bold;">controls</text>:图片节点控件,支持修改预览和删除等操作<br/>
`,
tips: '您通过画布或者聊天框修改图片,平台会根据您的需求进行图片优化。'
}, },
step3: { step3: {
title: '查看详情', title: '查看详情',
@ -1088,6 +1097,7 @@ export default {
currentPoints: '当前积分', currentPoints: '当前积分',
expiryDate: '积分到期时间', expiryDate: '积分到期时间',
pointsList: '积分明细', pointsList: '积分明细',
recharge: '充值',
consumptionRules: { consumptionRules: {
title: '积分消耗规则', title: '积分消耗规则',
behavior: '行为', behavior: '行为',
@ -1166,10 +1176,10 @@ export default {
}, },
premium: { premium: {
name: '高级套餐', name: '高级套餐',
badge: '热门' badge: '推荐'
} }
}, },
period: '', period: '',
points: '积分', points: '积分',
validity: '有效期', validity: '有效期',
features: { features: {
@ -1296,7 +1306,8 @@ export default {
goHome: '返回首页', goHome: '返回首页',
submitInviteCode: '提交邀请码', submitInviteCode: '提交邀请码',
hasInviteCode: '已有邀请码?', hasInviteCode: '已有邀请码?',
inviteCodePlaceholder: '填写邀请码升级为免费会员' inviteCodePlaceholder: '填写邀请码升级为免费会员',
logout: '退出登录'
} }
}, },
en: { en: {
@ -1326,10 +1337,10 @@ export default {
}, },
premium: { premium: {
name: 'Premium Plan', name: 'Premium Plan',
badge: 'HOT' badge: 'Picks'
} }
}, },
period: 'year', period: 'day',
points: 'Points', points: 'Points',
validity: 'Validity', validity: 'Validity',
features: { features: {
@ -1448,7 +1459,12 @@ export default {
}, },
modelModal: { modelModal: {
customizeToHome: 'Customize to Home', customizeToHome: 'Customize to Home',
textInputPlaceholder: 'Please enter adjustment content, e.g. change character expression' textInputPlaceholder: 'Please enter adjustment content, e.g. change character expression',
preview: 'Preview',
modify: 'Modify',
sceneGraph: 'Scene Graph',
edit: 'Edit',
download: 'Download'
}, },
modelCard: { modelCard: {
generateModelButton: 'Generate Model', generateModelButton: 'Generate Model',
@ -1535,6 +1551,7 @@ export default {
modelFreeCount: 'Free Model', modelFreeCount: 'Free Model',
times: 'times', times: 'times',
remainingCredits: 'Remaining Credits', remainingCredits: 'Remaining Credits',
recharge: 'Recharge',
guide: 'User Guide', guide: 'User Guide',
back: 'Back', back: 'Back',
skip: 'Skip', skip: 'Skip',
@ -1552,6 +1569,7 @@ export default {
currentPoints: 'Current Points', currentPoints: 'Current Points',
expiryDate: 'Points Expiry Date', expiryDate: 'Points Expiry Date',
pointsList: 'Points Details', pointsList: 'Points Details',
recharge: 'Recharge',
consumptionRules: { consumptionRules: {
title: 'Points Consumption Rules', title: 'Points Consumption Rules',
behavior: 'Behavior', behavior: 'Behavior',
@ -1732,9 +1750,9 @@ export default {
}, },
guideModal: { guideModal: {
step1: { step1: {
title: 'Reference Images', title: 'Reference Image',
description: 'Select images you like as creative references', description: 'Enter text to describe your IP, or select an image you like as a creative reference.',
tips: 'After clicking the generate button, the platform will generate corresponding 3D models based on your selection.' tips: 'After clicking the generate button, the platform will generate the corresponding image based on your selection.'
}, },
step2: { step2: {
title: 'Model Generation / Text Optimization', title: 'Model Generation / Text Optimization',
@ -2585,7 +2603,8 @@ export default {
goHome: 'Go Home', goHome: 'Go Home',
submitInviteCode: 'Submit Invite Code', submitInviteCode: 'Submit Invite Code',
hasInviteCode: 'Already have an invite code?', hasInviteCode: 'Already have an invite code?',
inviteCodePlaceholder: 'Enter invite code to upgrade to free membership' inviteCodePlaceholder: 'Enter invite code to upgrade to free membership',
logout: 'Logout'
} }
} }
}, },

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router' import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import NProgress from 'nprogress' import NProgress from 'nprogress'
@ -17,21 +17,21 @@ const AddAgent = () => import('../views/AddAgent.vue')
const DeviceList = () => import('../views/DeviceList.vue') const DeviceList = () => import('../views/DeviceList.vue')
const UiTest = () => import('../views/UiTest.vue') const UiTest = () => import('../views/UiTest.vue')
const home = () => import('../views/home/index.vue') const home = () => import('../views/home/index.vue')
const PointsRecharge = () => import('../views/PointsRecharge.vue') const PointsRecharge = () => import('../views/PointsRecharge/PointsRecharge.vue')
const UserCenter = () => import('../views/user/index.vue') const UserCenter = () => import('../views/user/index.vue')
const NotFound = () => import('../views/NotFound.vue') const NotFound = () => import('../views/NotFound.vue')
const Waitlist = () => import('../views/Waitlist.vue') const Waitlist = () => import('../views/Waitlist.vue')
NProgress.configure({ NProgress.configure({
showSpinner: false, showSpinner: false,
})// 开启轻量模式(顶部细线) })// 开启轻量模式(顶部细线)
// 路由配置 // 路由配置
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: home, component: home,
meta: { fullScreen: false } meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
},{ }, {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: Login, component: Login,
@ -47,7 +47,7 @@ const routes = [
path: '/czhome', path: '/czhome',
name: 'czhome', name: 'czhome',
component: ModernHome, component: ModernHome,
meta: { requiresAuth: false, keepAlive: false } meta: { requiresAuth: true, keepAlive: false }
}, },
{ {
path: '/register', path: '/register',
@ -61,6 +61,12 @@ const routes = [
component: ForgotPassword, component: ForgotPassword,
meta: { requiresGuest: true, fullScreen: true } meta: { requiresGuest: true, fullScreen: true }
}, },
{
path: '/Waitlist',
name: 'Waitlist',
component: Waitlist, // 升级页
meta: { requiresAuth: true, keepAlive: false, fullScreen: true }
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',
@ -146,7 +152,7 @@ export const freeRoutes = [
path: '/points-recharge', path: '/points-recharge',
name: 'points-recharge', name: 'points-recharge',
component: PointsRecharge, component: PointsRecharge,
meta: { requiresAuth: true,fullScreen: true } meta: { requiresAuth: true, fullScreen: true }
}, },
{ {
path: '/list', path: '/list',
@ -163,7 +169,7 @@ export const freeRoutes = [
] ]
const router = createRouter({ const router = createRouter({
// history: createWebHistory(), // history: createWebHistory(),
history:createWebHashHistory(), history: createWebHashHistory(),
routes, routes,
}) })
// 路由守卫 // 路由守卫
@ -173,7 +179,7 @@ router.beforeEach(async (to, from, next) => {
// window.localStorage.setItem('token','123') // window.localStorage.setItem('token','123')
// return next() // return next()
// } // }
if(to.path=='/login'||to.path=='/login/phone'||to.path=='/register'||to.path=='/forgot-password'){ if (to.path == '/login' || to.path == '/login/phone' || to.path == '/register' || to.path == '/forgot-password') {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
// 如果有 token跳转到首页 // 如果有 token跳转到首页
if (token) { if (token) {
@ -181,7 +187,8 @@ router.beforeEach(async (to, from, next) => {
return return
} }
} }
const newto = freeRoutes.find(route => route.path == to.path) const findRoutes = [...freeRoutes, ...routes]
let newto = findRoutes.find(route => route.path == to.path)
if (newto?.meta?.requiresAuth) { if (newto?.meta?.requiresAuth) {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
// 如果没有 token跳转到登录页 // 如果没有 token跳转到登录页
@ -195,27 +202,24 @@ router.beforeEach(async (to, from, next) => {
// const info = await authStore.updateUserInfo(); // const info = await authStore.updateUserInfo();
// console.log(info,'infoinfo'); // console.log(info,'infoinfo');
const user_role = authStore.user?.userRole; const user_role = authStore.user?.userRole;
if((user_role == 1||user_role == 2) && router.getRoutes().length == routes.length) { if ((user_role == 1 || user_role == 2) && router.getRoutes().length == routes.length) {
// 添加动态路由 // 添加动态路由
addDynamicRoutes(); addDynamicRoutes();
if(isDynamicRoute(to.path)) { if (isDynamicRoute(to.path)) {
next('/czhome') next('/czhome')
setTimeout(() => { setTimeout(() => {
router.push(to.path) router.push(to.path)
}, 20); }, 20);
return return
} }
}else if(user_role == 0){
// 恢复默认路由
removeDynamicRoutes()
router.addRoute(
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: Waitlist, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
} }
) // else if (user_role == 0) {
// // 恢复默认路由
// removeDynamicRoutes()
// }
if (user_role == 0&&to.path!='/Waitlist'&&to.path!='/') {
// 跳转升级页
return next('/Waitlist')
} }
next() next()
}) })
@ -228,7 +232,7 @@ function addDynamicRoutes() {
} }
//恢复默认路由 //恢复默认路由
function removeDynamicRoutes() { function removeDynamicRoutes() {
if(router.getRoutes().length == routes.length){ if (router.getRoutes().length == routes.length) {
return return
} }
router.getRoutes().forEach(route => { router.getRoutes().forEach(route => {

View File

@ -243,7 +243,18 @@ const handleFileSelect = (event) => {
} }
const openProject = (project) => { const openProject = (project) => {
router.push(`/project/${project.id}/${project.tags[0]}`) let series = project.tags[0]
switch(series){
case 'Oone':
series = 'E1';
break;
case 'Done':
series = 'D1';
break;
default:
break;
}
router.push(`/project/${project.id}/${series}`)
} }
const createNewProject = (series) => { const createNewProject = (series) => {

View File

@ -4,6 +4,7 @@ export class OrderManagement {
} }
//获取订单列表 //获取订单列表
getOrderList(params){ getOrderList(params){
params.source_type = 0
return requestUtils.common(clientApi.default.getOrderList, params); return requestUtils.common(clientApi.default.getOrderList, params);
} }
//获取订单详情 //获取订单详情

View File

@ -6,13 +6,11 @@
<button class="back-button" @click="goBack"> <button class="back-button" @click="goBack">
{{ t('common.back') }} {{ t('common.back') }}
</button> </button>
<!-- 语言切换组件 --> <!-- 语言切换组件 -->
<div class="language-switcher"> <div class="language-switcher">
<LanguageToggle /> <LanguageToggle />
</div> </div>
</div> </div>
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="page-header"> <div class="page-header">
<h1 class="page-title">{{ t('pointsRecharge.title') }}</h1> <h1 class="page-title">{{ t('pointsRecharge.title') }}</h1>
@ -21,68 +19,48 @@
<!-- 套餐选择 --> <!-- 套餐选择 -->
<div class="plans-container"> <div class="plans-container">
<!-- 套餐卡片 1: 300积分 --> <!-- 动态渲染套餐卡片 -->
<div class="plan-card" :class="{ active: selectedPlan === 'basic' }" @click="selectPlan('basic')"> <div
v-for="pkg in sortedPackages"
:key="pkg.id"
class="plan-card"
:class="{
premium: pkg.is_recommended === 1
}"
@click="selectPlan(pkg.id)"
>
<div class="plan-header"> <div class="plan-header">
<h3 class="plan-name">{{ t('pointsRecharge.plans.basic.name') }}</h3> <h3 class="plan-name">{{ pkg.package_name }}</h3>
<div v-if="pkg.is_recommended === 1" class="plan-badge">{{ t('pointsRecharge.plans.premium.badge') }}</div>
</div> </div>
<div class="plan-price"> <div class="plan-price">
<span class="price-amount">${{ plans.basic.price }}</span> <span class="price-amount">${{ pkg.amount }}</span>
<span class="price-period">/ {{ t('pointsRecharge.period') }}</span> <span class="price-period">/ {{ t('pointsRecharge.period') }}</span>
</div> </div>
<div class="plan-points"> <div class="plan-points">
{{ plans.basic.points }} {{ t('pointsRecharge.points') }} {{ pkg.credits }} {{ t('pointsRecharge.points') }}
</div> </div>
<div class="plan-validity"> <div class="plan-validity">
{{ t('pointsRecharge.validity') }}: {{ plans.basic.validity }} {{ t('pointsRecharge.period') }} {{ t('pointsRecharge.validity') }}: {{ pkg.validity_days }} {{ t('pointsRecharge.period') }}
</div> </div>
<div class="plan-features"> <div class="plan-features">
<ul> <ul>
<li>{{ pkg.description }}</li>
<li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li> <li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li>
<li>{{ t('pointsRecharge.features.prioritySupport') }}</li> <li>{{ t('pointsRecharge.features.prioritySupport') }}</li>
<li>{{ t('pointsRecharge.features.securePayment') }}</li> <li>{{ t('pointsRecharge.features.securePayment') }}</li>
</ul> </ul>
</div> </div>
<button class="purchase-button" @click.stop="purchasePlan('basic')"> <button
{{ t('pointsRecharge.purchase') }} class="purchase-button"
</button> :class="{ 'premium-button': pkg.is_recommended === 1 }"
</div> @click.stop="purchasePlan(pkg)"
>
<!-- 套餐卡片 2: 1000积分 -->
<div class="plan-card premium" :class="{ active: selectedPlan === 'premium' }" @click="selectPlan('premium')">
<div class="plan-header">
<h3 class="plan-name">{{ t('pointsRecharge.plans.premium.name') }}</h3>
<div class="plan-badge">{{ t('pointsRecharge.plans.premium.badge') }}</div>
</div>
<div class="plan-price">
<span class="price-amount">${{ plans.premium.price }}</span>
<span class="price-period">/ {{ t('pointsRecharge.period') }}</span>
</div>
<div class="plan-points">
{{ plans.premium.points }} {{ t('pointsRecharge.points') }}
</div>
<div class="plan-validity">
{{ t('pointsRecharge.validity') }}: {{ plans.premium.validity }} {{ t('pointsRecharge.period') }}
</div>
<div class="plan-features">
<ul>
<li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li>
<li>{{ t('pointsRecharge.features.prioritySupport') }}</li>
<li>{{ t('pointsRecharge.features.securePayment') }}</li>
<li>{{ t('pointsRecharge.features.extraBenefits') }}</li>
</ul>
</div>
<button class="purchase-button premium-button" @click.stop="purchasePlan('premium')">
{{ t('pointsRecharge.purchase') }} {{ t('pointsRecharge.purchase') }}
</button> </button>
</div> </div>
@ -101,48 +79,51 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import LanguageToggle from '@/components/ui/LanguageToggle.vue' import LanguageToggle from '@/components/ui/LanguageToggle.vue'
import { PointsRecharge } from './index.js'
const pointsRecharge = new PointsRecharge()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
// const selectedPlan = ref(null)
const selectedPlan = ref('basic') const packagesList = ref([])
// const sortedPackages = computed(() => {
const plans = ref({ return [...packagesList.value].sort((a, b) => a.sort_order - b.sort_order)
basic: {
name: 'basic',
points: 300,
price: 30,
validity: '1'
},
premium: {
name: 'premium',
points: 1000,
price: 80,
validity: '1'
}
}) })
//
const goBack = () => { const goBack = () => {
const history = router.options.history.state
console.log(history);
if(!history.back){
router.replace('/user-center')
}
router.back() router.back()
} }
// const selectPlan = (planId) => {
const selectPlan = (planName) => { selectedPlan.value = planId
selectedPlan.value = planName
} }
// const purchasePlan = (pkg) => {
const purchasePlan = (planName) => { let parmas = {
console.log('购买套餐:', planName) package_id: pkg.id,
// }
pointsRecharge.confirmPay(parmas)
} }
const init = async () => {
let res = await pointsRecharge.getRechargePackageList()
const data = res.items || []
packagesList.value = data
}
onMounted(() => {
init()
})
</script> </script>
<style scoped> <style scoped>
@ -257,7 +238,8 @@ const purchasePlan = (planName) => {
.plan-card.active { .plan-card.active {
border-color: #8B5CF6; border-color: #8B5CF6;
box-shadow: 0 20px 40px rgba(139, 92, 246, 0.2); border-width: 3px;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3), 0 20px 40px rgba(139, 92, 246, 0.3);
} }
.plan-card.active::before { .plan-card.active::before {
@ -267,7 +249,6 @@ const purchasePlan = (planName) => {
/* 高级套餐样式 */ /* 高级套餐样式 */
.plan-card.premium { .plan-card.premium {
background: linear-gradient(135deg, #1F2937 0%, #374151 100%); background: linear-gradient(135deg, #1F2937 0%, #374151 100%);
border-color: #8B5CF6;
} }
/* 套餐头部 */ /* 套餐头部 */
@ -276,6 +257,8 @@ const purchasePlan = (planName) => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
flex-wrap: nowrap;
gap: 12px;
} }
.plan-name { .plan-name {
@ -283,6 +266,11 @@ const purchasePlan = (planName) => {
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
color: #F9FAFB; color: #F9FAFB;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.plan-badge { .plan-badge {
@ -292,6 +280,7 @@ const purchasePlan = (planName) => {
font-weight: 600; font-weight: 600;
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
flex-shrink: 0;
} }
/* 价格样式 */ /* 价格样式 */

View File

@ -0,0 +1,20 @@
import {clientApi,requestUtils,PayServer} from '@deotaland/utils';
export class PointsRecharge extends PayServer{
constructor() {
super()
}
//获取充值包列表
async getRechargePackageList(){
let res = await requestUtils.common(clientApi.default.RECHARGE_PACKAGES);
if (res.code == 0) {
let data = res.data
return data
} else {
throw new Error(res.msg)
}
}
//确定支付
async confirmPay(orderInfo){
this.createPayorOrder(orderInfo,2);
}
}

View File

@ -50,7 +50,7 @@
> >
<!-- @mouseleave="stopElementDrag" --> <!-- @mouseleave="stopElementDrag" -->
<!-- 删除按钮 --> <!-- 删除按钮 -->
<button <!-- <button
v-if="(card.imageUrl&&card.type==='image')||(card.type==='model'&&card.modelUrl)" v-if="(card.imageUrl&&card.type==='image')||(card.type==='model'&&card.modelUrl)"
class="delete-button" class="delete-button"
@click.stop="handleDeleteCard(index)" @click.stop="handleDeleteCard(index)"
@ -62,17 +62,18 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
</button> </button> -->
<!-- 根据卡片类型显示不同组件 --> <!-- 根据卡片类型显示不同组件 -->
<IPCard <IPCard
@delete-card="handleDeleteCard(index)"
:combinedPromptJson="combinedPromptJson" :combinedPromptJson="combinedPromptJson"
@handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)" @handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)"
@customize-to-home="handleCustomizeToHome(index)" @customize-to-home="handleCustomizeToHome(index)"
@preview-image="handlePreviewImage" @preview-image="handlePreviewImage"
@generate-smooth-white-model="(imageUrl)=>handleGenerateSmoothWhiteModel(index,imageUrl)" @generate-smooth-white-model="(imageUrl)=>handleGenerateSmoothWhiteModel(index,imageUrl)"
@create-new-card="(data)=>handleCreateFourViewCard(index,data)" @create-new-card="(data)=>handleCreateFourViewCard(index,data)"
@delete="handleDeleteCard(index)" @download-image="downloadImage"
:projectId="projectId" :projectId="projectId"
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)" @create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)" @generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
@ -123,7 +124,8 @@
:initialIndex="currentImageIndex" :initialIndex="currentImageIndex"
@close="showImagePreview = false" @close="showImagePreview = false"
/> />
<CanvasEditor <DtCanvasEditor
:language="locale"
v-model:visible="canvasEditorVisible" v-model:visible="canvasEditorVisible"
:image-url="canvasEditorImageUrl" :image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave" @add-prompt-card="handleCanvasSave"
@ -167,6 +169,8 @@ import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
import PurchaseModal from '../../components/PurchaseModal/index.vue'; import PurchaseModal from '../../components/PurchaseModal/index.vue';
import {Project} from './index'; import {Project} from './index';
import {ModernHome} from '../ModernHome/index.js' import {ModernHome} from '../ModernHome/index.js'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const fileServer = new FileServer(); const fileServer = new FileServer();
const modernHome = new ModernHome(); const modernHome = new ModernHome();
const router = useRouter(); const router = useRouter();
@ -197,6 +201,19 @@ const getGenerateCount = async ()=>{
// Limits.value.generateCount = data[0].model_count; // Limits.value.generateCount = data[0].model_count;
// Limits.value.modelCount = data[1].model_count; // Limits.value.modelCount = data[1].model_count;
} }
//
const downloadImage = async (url) => {
try {
const blobUrl = await fileServer.fetchImage(url);
const link = document.createElement('a');
link.href = blobUrl;
link.download = `image-${Date.now()}.jpg`;
link.click();
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('下载图片失败:', error);
}
}
const handlePartialEdit = (imageUrl, index) => { const handlePartialEdit = (imageUrl, index) => {
canvasEditorImageUrl.value = imageUrl; canvasEditorImageUrl.value = imageUrl;
canvasEditorVisible.value = true; canvasEditorVisible.value = true;
@ -219,6 +236,14 @@ const handlePartialEdit = (imageUrl, index) => {
const cards = ref([ const cards = ref([
]); ]);
//
const showGuideModalLogic = ()=>{
const lastShowDate = localStorage.getItem('lastShowGuideModalDate');
const currentDate = new Date().toDateString();
if (!lastShowDate || lastShowDate !== currentDate) {
showGuideModal.value = true;
}
}
const getMaxZIndexNum = ref(0); const getMaxZIndexNum = ref(0);
//z-index+1 //z-index+1
const getMaxZIndex = (type)=>{ const getMaxZIndex = (type)=>{
@ -286,6 +311,7 @@ const handleSaveProject = (index,item,type='image')=>{
const createProject = async ()=>{ const createProject = async ()=>{
const {id} = await PluginProject.createProject(); const {id} = await PluginProject.createProject();
// project/id/ // project/id/
await router.replace(`/project/${id}/${series.value}`); await router.replace(`/project/${id}/${series.value}`);
projectId.value = id; projectId.value = id;
getProjectInfo(id); getProjectInfo(id);
@ -341,11 +367,15 @@ const closeImportModal = () => {
// //
const closeGuideModal = () => { const closeGuideModal = () => {
showGuideModal.value = false; showGuideModal.value = false;
const currentDate = new Date().toDateString();
localStorage.setItem('lastShowGuideModalDate', currentDate);
}; };
// //
const completeGuide = () => { const completeGuide = () => {
showGuideModal.value = false; showGuideModal.value = false;
const currentDate = new Date().toDateString();
localStorage.setItem('lastShowGuideModalDate', currentDate);
}; };
// ==================== ==================== // ==================== ====================
@ -1027,7 +1057,6 @@ const init = ()=>{
const route = useRoute(); const route = useRoute();
projectId.value = route.params.id; projectId.value = route.params.id;
series.value = route.params.series; series.value = route.params.series;
console.log(series.value);
if(projectId.value === 'new'){ if(projectId.value === 'new'){
createProject(); createProject();
return return
@ -1037,6 +1066,7 @@ const init = ()=>{
} }
// //
onMounted(() => { onMounted(() => {
showGuideModalLogic();
MeshyServer.pollingEnabled = true; MeshyServer.pollingEnabled = true;
GiminiServer.pollingEnabled = true; GiminiServer.pollingEnabled = true;
// //

View File

@ -1,5 +1,5 @@
import { clientApi, requestUtils } from '@deotaland/utils'; import { clientApi, requestUtils } from '@deotaland/utils';
export class Project{ export class Project {
static timer = null; static timer = null;
//项目时常 //项目时常
duration_seconds = 0; duration_seconds = 0;
@ -10,7 +10,7 @@ export class Project{
this.latestUpdateParams = null; this.latestUpdateParams = null;
} }
//创建项目 //创建项目
async createProject(name='project',) { async createProject(name = 'project',) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let params = { let params = {
"thumbnail": "",//项目缩略图 "thumbnail": "",//项目缩略图
@ -18,7 +18,7 @@ export class Project{
"description": "init",//项目描述 "description": "init",//项目描述
"details": { "details": {
"node_card": [], "node_card": [],
"prompt":'', "prompt": '',
"thumbnail": "", // 缩略图 "thumbnail": "", // 缩略图
}, },
"tags": [ "tags": [
@ -26,16 +26,16 @@ export class Project{
], ],
"duration_seconds": 0,//项目时长(秒) "duration_seconds": 0,//项目时长(秒)
} }
let res = await requestUtils.common(clientApi.default.PROJECT_CREATE,params) let res = await requestUtils.common(clientApi.default.PROJECT_CREATE, params)
if(res.code !== 0){ if (res.code !== 0) {
reject(res) reject(res)
}else{ } else {
resolve(res.data) resolve(res.data)
} }
}) })
} }
//更新项目(带防抖处理,三秒内只执行最后一次请求) //更新项目(带防抖处理,三秒内只执行最后一次请求)
async updateProject(projectId,projectConfig) { async updateProject(projectId, projectConfig) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// 存储最新的请求参数 // 存储最新的请求参数
const params = { const params = {
@ -52,9 +52,9 @@ export class Project{
this.updateTimer = setTimeout(async () => { this.updateTimer = setTimeout(async () => {
try { try {
const res = await requestUtils.common(clientApi.default.PROJECT_UPDATE, this.latestUpdateParams); const res = await requestUtils.common(clientApi.default.PROJECT_UPDATE, this.latestUpdateParams);
if(res.code !== 0){ if (res.code !== 0) {
reject(res) reject(res)
}else{ } else {
resolve(res.data) resolve(res.data)
} }
} catch (error) { } catch (error) {
@ -72,10 +72,10 @@ export class Project{
let params = { let params = {
"id": projectId,//项目ID "id": projectId,//项目ID
} }
let res = await requestUtils.common(clientApi.default.PROJECT_DELETE,params) let res = await requestUtils.common(clientApi.default.PROJECT_DELETE, params)
if(res.code !== 0){ if (res.code !== 0) {
reject(res) reject(res)
}else{ } else {
resolve(res.data) resolve(res.data)
} }
}) })
@ -86,10 +86,10 @@ export class Project{
let params = { let params = {
"id": projectId,//项目ID "id": projectId,//项目ID
} }
let res = await requestUtils.common(clientApi.default.PROJECT_GET,params) let res = await requestUtils.common(clientApi.default.PROJECT_GET, params)
if(res.code !== 0){ if (res.code !== 0) {
reject(res) reject(res)
}else{ } else {
resolve(res.data) resolve(res.data)
} }
}) })
@ -433,12 +433,12 @@ export class Project{
// { // {
// "type": "model", // "type": "model",
// "status": "success", // "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", // "taskId": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroE1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
// "zIndex": 101, // "zIndex": 101,
// "offsetX": "-263px", // "offsetX": "-263px",
// "offsetY": "-388px", // "offsetY": "-388px",
// "imageUrl": "https://api.deotaland.ai/upload/526afeec880e46688d6e2edd1893e778.png", // "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", // "modelUrl": "https://api.deotaland.ai/model/ae501c6d-79fd-4bca-9768-f315ee977749/tasks/019ac8d3-959d-7432-ac15-b5bab5c75b74/output/model.glb?Expires=4917906081&Signature=W88kkM3xYsvvJkiroE1RV141Jfp03bSQCMATSoBQ2gvct87jfWADJTm80rJTDWuNCBmXcF7dt2kDgB2mGWXjQri1DhAwzY87gIp~bJsvtis6UreApFesKHPhU~BkZGfOW2culwT29NTIY~PkTL9Whg8WZ-Mu9xb6Og6iJHol6E4gIX-jGTrXN67~BWsh17kvgSHN5bmackn9IZ~GGwC2threl0BzeFzgpYEkFuP60fkXeCvFAiZBCd0shZR9tuXa4drdzeg7FJMTWpPhhSBPXEFNeDxOpxiLd8iCrQuyu5s1sxNJuGeDcMfTTDh864J0r4-ap11L0LAnWXyHE69-kw__&Key-Pair-Id=KL5I0C8H7HX83",
// "cardWidth": "250", // "cardWidth": "250",
// "timestamp": "2025-11-28T04:51:05.620Z", // "timestamp": "2025-11-28T04:51:05.620Z",
// "project_id": "11", // "project_id": "11",
@ -709,11 +709,11 @@ export class Project{
// "total": 18 // "total": 18
// }) // })
// return // return
let res = await requestUtils.common(clientApi.default.PROJECT_LIST,params) let res = await requestUtils.common(clientApi.default.PROJECT_LIST, params)
if(res.code !== 0){ if (res.code !== 0) {
// reject(res) // reject(res)
}else{ } else {
resolve(res.data) resolve(res.data)
} }
}) })
@ -721,28 +721,28 @@ export class Project{
//叠加项目时常 //叠加项目时常
addProjectDuration = (time) => { addProjectDuration = (time) => {
this.duration_seconds = time; this.duration_seconds = time;
if(Project.timer){ if (Project.timer) {
clearInterval(Project.timer); clearInterval(Project.timer);
} }
Project.timer = setInterval(() => { Project.timer = setInterval(() => {
this.duration_seconds +=1; this.duration_seconds += 1;
}, 1000); }, 1000);
} }
//获取动态提示词 //获取动态提示词
async getCombinedPrompt(series){//series:项目系列D1 O1 async getCombinedPrompt(series) {//series:项目系列D1 E1
try { try {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const res = await requestUtils.common(clientApi.default.combined) const res = await requestUtils.common(clientApi.default.combined)
if(res.code === 0){ if (res.code === 0) {
let data = res.data; let data = res.data;
// 如果是Oone系列过滤掉title中包含"动物坐姿"或"人物姿势"的提示词 // 如果是Oone系列过滤掉title中包含"动物坐姿"或"人物姿势"的提示词
if (series === 'O1') { if (series === 'E1') {
data = data.filter(item => { data = data.filter(item => {
if (!item.title) return true; if (!item.title) return true;
return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势')&& item.type != 'D1'; return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势') && item.type != 'D1';
}); });
}else if(series === 'D1'){// 如果是Done系列过滤掉type为O1的提示词 } else if (series === 'D1') {// 如果是Done系列过滤掉type为E1的提示词
data = data.filter(item => item.type !== 'O1'); data = data.filter(item => item.type !== 'E1');
} }
// 初始化返回数据结构 // 初始化返回数据结构
const result = { const result = {
@ -758,7 +758,7 @@ export class Project{
// 按sortOrder排序 // 按sortOrder排序
data.sort((a, b) => a.sortOrder - b.sortOrder); data.sort((a, b) => a.sortOrder - b.sortOrder);
// 处理person和general类型的数据 // 处理person和general类型的数据
const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general'|| item.type === 'O1'); const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general' || item.type === 'E1');
personAndGeneral.forEach(item => { personAndGeneral.forEach(item => {
// 拼接content // 拼接content
result.person.content += item.content; result.person.content += item.content;
@ -775,7 +775,7 @@ export class Project{
} }
}); });
// 处理animal和general类型的数据 // 处理animal和general类型的数据
const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general'|| item.type === 'O1'); const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general' || item.type === 'E1');
animalAndGeneral.forEach(item => { animalAndGeneral.forEach(item => {
// 拼接content // 拼接content
result.animal.content += item.content; result.animal.content += item.content;
@ -792,7 +792,7 @@ export class Project{
} }
}); });
resolve(result); resolve(result);
}else{ } else {
reject(res.msg) reject(res.msg)
} }
}) })

View File

@ -26,13 +26,16 @@
<button <button
class="primary-button" class="primary-button"
@click="submitInviteCode" @click="submitInviteCode"
:loading="submitting" :disabled="submitting"
> >
{{ t('waitlist.submitInviteCode') }} {{ t('waitlist.submitInviteCode') }}
</button> </button>
<button class="secondary-button" @click="goHome"> <button class="secondary-button" @click="goHome">
{{ t('waitlist.goHome') }} {{ t('waitlist.goHome') }}
</button> </button>
<button class="logout-button" @click="handleLogout">
{{ t('waitlist.logout') }}
</button>
</div> </div>
</div> </div>
<div class="decoration"> <div class="decoration">
@ -43,7 +46,7 @@
</div> </div>
</template> </template>
<script> <script setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ref } from 'vue' import { ref } from 'vue'
@ -51,38 +54,42 @@ import { ElMessage } from 'element-plus'
import { UserController } from './user/index.js' import { UserController } from './user/index.js'
import { useAuthStore } from '@/stores/auth.js' import { useAuthStore } from '@/stores/auth.js'
export default { const { t } = useI18n()
name: 'Waitlist', const router = useRouter()
setup() { const authStore = useAuthStore()
const { t } = useI18n() const userController = new UserController()
const router = useRouter()
const authStore = useAuthStore()
const userController = new UserController()
const inviteCode = ref('') const inviteCode = ref('')
const submitting = ref(false) const submitting = ref(false)
const goHome = () => { const goHome = () => {
router.push('/') router.replace('/')
}
const handleLogout = async () => {
try {
await authStore.logout(() => {
ElMessage.success('退出登录成功')
router.replace('/login')
})
} catch (error) {
console.error('退出登录失败:', error)
ElMessage.error('退出登录失败,请稍后重试')
} }
}
// const submitInviteCode = async () => {
const submitInviteCode = async () => {
if (!inviteCode.value.trim()) { if (!inviteCode.value.trim()) {
// ElMessage.warning('')
return return
} }
submitting.value = true submitting.value = true
try { try {
// upgrade
const upgradeRes = await userController.upgrade({ inviteCode: inviteCode.value }) const upgradeRes = await userController.upgrade({ inviteCode: inviteCode.value })
if (upgradeRes.code === 0) { if (upgradeRes.code === 0) {
//
await authStore.updateUserInfo() await authStore.updateUserInfo()
ElMessage.success('升级成功') ElMessage.success('升级成功')
// router.replace('/czhome')
goHome()
} else { } else {
ElMessage.error(upgradeRes.message || '升级失败') ElMessage.error(upgradeRes.message || '升级失败')
} }
@ -92,16 +99,6 @@ export default {
} finally { } finally {
submitting.value = false submitting.value = false
} }
}
return {
t,
goHome,
inviteCode,
submitting,
submitInviteCode
}
}
} }
</script> </script>
@ -286,6 +283,34 @@ export default {
color: #6B46C1; color: #6B46C1;
border: 1px solid #6B46C1; border: 1px solid #6B46C1;
} }
.logout-button {
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background-color: #EF4444;
color: white;
}
.logout-button:hover {
background-color: #DC2626;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
}
.logout-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
.logout-button:disabled:hover {
box-shadow: none;
}
@media (max-width: 480px) { @media (max-width: 480px) {
.waitlist-icon svg { .waitlist-icon svg {
width: 80px; width: 80px;

View File

@ -33,12 +33,7 @@
> >
{{ t('nav.land') }} {{ t('nav.land') }}
</a> --> </a> -->
<!-- <router-link
to="/points-recharge"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
pricing
</router-link> -->
<a <a
href="#" href="#"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors" class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
@ -57,6 +52,12 @@
> >
{{ t('nav.about') }} {{ t('nav.about') }}
</a> </a>
<router-link
to="/points-recharge"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
pricing
</router-link>
</nav> </nav>
<!-- Right Action & Mobile Toggle --> <!-- Right Action & Mobile Toggle -->
@ -547,7 +548,7 @@
<script setup> <script setup>
import MotionCom from './motion.vue' import MotionCom from './motion.vue'
// import spline from './spline.vue'; import spline from './spline.vue';
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import Bg from './bg.vue' import Bg from './bg.vue'
// import dog from '@/assets/home/dog.webp' // import dog from '@/assets/home/dog.webp'
@ -641,8 +642,8 @@ const i18n = {
// land: '', // land: '',
// pricing: '', // pricing: '',
creator: 'Creator', creator: 'Creator',
done: 'D one', done: 'D1',
about: 'About us' about: 'About'
}, },
hero: { hero: {
title: '使用 Deotaland 创作', title: '使用 Deotaland 创作',

View File

@ -1,4 +1,4 @@
<!-- <template> <template>
<ParentSize <ParentSize
:parent-size-styles="parentSizeStyles" :parent-size-styles="parentSizeStyles"
:debounce-time="50" :debounce-time="50"
@ -159,4 +159,4 @@ onUnmounted(() => {
splineApp.value = null; splineApp.value = null;
} }
}); });
</script> --> </script>

View File

@ -12,30 +12,29 @@
</div> </div>
</div> </div>
<!-- 隐藏的文件上传输入 --> <!-- 隐藏的文件上传输入 -->
<!-- <input <input
ref="avatarInput" ref="avatarInput"
type="file" type="file"
accept="image/*" accept="image/*"
class="avatar-input" class="avatar-input"
@change="handleAvatarUpload" @change="handleAvatarUpload"
hidden hidden
/> --> />
</div> </div>
<!-- @click="toggleEditName" -->
<div class="user-details-section"> <div class="user-details-section">
<div class="user-name-row"> <div class="user-name-row">
<div class="editable-name-container"> <div class="editable-name-container">
<h1 <h1
@click="toggleEditName"
class="user-name" class="user-name"
:class="{ 'editable': true }" :class="{ 'editable': true }"
>{{ userData.nickname }}</h1> >{{ userData.nickname }}</h1>
<div v-if="isEditingName" class="name-edit-form"> <div v-if="isEditingName" class="name-edit-form">
<!-- @blur="saveNickname"
@keyup.enter="saveNickname" -->
<el-input <el-input
v-model="editNameValue" v-model="editNameValue"
size="large" size="large"
@blur="saveNickname"
@keyup.enter="saveNickname"
ref="nameInput" ref="nameInput"
placeholder="Please enter a nickname" placeholder="Please enter a nickname"
/> />
@ -48,7 +47,7 @@
</div> </div>
<!-- 积分信息区域 - 免费会员可见 --> <!-- 积分信息区域 - 免费会员可见 -->
<div class="points-section" v-if="false"> <div class="points-section" >
<h2>{{ $t('userCenter.points.title') }}</h2> <h2>{{ $t('userCenter.points.title') }}</h2>
<!-- 积分明细和规则并排容器 --> <!-- 积分明细和规则并排容器 -->
@ -57,7 +56,15 @@
<div class="total-points"> <div class="total-points">
<span class="total-icon">🪄</span> <span class="total-icon">🪄</span>
<span class="total-label">{{ $t('userCenter.points.currentPoints') }}</span> <span class="total-label">{{ $t('userCenter.points.currentPoints') }}</span>
<span class="points-value">{{ userData.currentPoints }}</span> <span class="points-value">{{total_score}}</span>
<el-button
type="primary"
size="small"
class="recharge-btn"
@click="router.push('/points-recharge')"
>
{{ $t('userCenter.points.recharge') }}
</el-button>
</div> </div>
<!-- 积分明细列表 --> <!-- 积分明细列表 -->
<div> <div>
@ -127,6 +134,7 @@
<div class="invite-codes-section"> <div class="invite-codes-section">
<h3>{{ $t('userCenter.invitation.inviteCodes') }}</h3> <h3>{{ $t('userCenter.invitation.inviteCodes') }}</h3>
<div class="invite-cards-container"> <div class="invite-cards-container">
<!-- v-for="(inviteCode, index) in (userData.inviteCodes.filter(item => item.codeType!='permanent'))" -->
<div <div
v-for="(inviteCode, index) in (userData.inviteCodes.filter(item => item.codeType!='permanent'))" v-for="(inviteCode, index) in (userData.inviteCodes.filter(item => item.codeType!='permanent'))"
:key="index" :key="index"
@ -267,11 +275,15 @@
<script setup> <script setup>
import { ref, nextTick, computed, onMounted } from 'vue' import { ref, nextTick, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Plus, Edit } from '@element-plus/icons-vue' import { Plus, Edit } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { UserController } from './index.js' import { UserController } from './index.js'
import {ModernHome} from '../ModernHome/index.js'
const router = useRouter()
const modernHome = new ModernHome()
const { t } = useI18n() const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const userController = new UserController() const userController = new UserController()
@ -328,10 +340,16 @@ const fetchInviteCodes = async () => {
loading.value = false loading.value = false
} }
} }
const total_score = ref(0);
//
const pointDetail = async () => {
const {data} = await modernHome.getModelLimits();
total_score.value = data.total_score
}
// //
onMounted(() => { onMounted(() => {
fetchInviteCodes() fetchInviteCodes();
pointDetail();
}) })
// //
const getRoleName = () => { const getRoleName = () => {
@ -903,6 +921,33 @@ html.dark .copy-btn:hover:not(:disabled) {
color: var(--text-secondary, #6b7280); color: var(--text-secondary, #6b7280);
} }
.points-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary, #1f2937);
}
.recharge-btn {
margin-left: auto;
background: linear-gradient(135deg, #8B5CF6 0%, #6B46C1 100%);
border: none;
border-radius: 8px;
padding: 8px 20px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
}
.recharge-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
background: linear-gradient(135deg, #9D74FF 0%, #7B5BD9 100%);
}
.recharge-btn:active {
transform: translateY(0);
}
/* 积分列表样式 */ /* 积分列表样式 */
.points-list { .points-list {
margin-top: 24px; margin-top: 24px;

View File

@ -66,8 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题 // 配置代理解决CORS问题
proxy: { proxy: {
'/api': { '/api': {
target: 'https://api.deotaland.ai', // target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local', target: 'http://api.deotaland.local',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<el-dialog <el-dialog
v-model="dialogVisible" v-model="dialogVisible"
title="局部修改" :title="t.dialogTitle"
width="90%" width="90%"
:fullscreen="isFullscreen" :fullscreen="isFullscreen"
append-to-body append-to-body
@ -11,7 +11,7 @@
<div class="canvas-editor-container"> <div class="canvas-editor-container">
<div class="canvas-toolbar"> <div class="canvas-toolbar">
<div class="toolbar-section"> <div class="toolbar-section">
<span class="toolbar-label">画笔颜色:</span> <span class="toolbar-label">{{ t.brushColor }}</span>
<div class="color-picker"> <div class="color-picker">
<div <div
v-for="color in colors" v-for="color in colors"
@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<div class="toolbar-section"> <div class="toolbar-section">
<span class="toolbar-label">画笔大小:</span> <span class="toolbar-label">{{ t.brushSize }}</span>
<el-slider <el-slider
v-model="brushSize" v-model="brushSize"
:min="1" :min="1"
@ -44,7 +44,7 @@
@click="toggleEraser" @click="toggleEraser"
> >
<el-icon><Delete /></el-icon> <el-icon><Delete /></el-icon>
橡皮擦 {{ t.eraser }}
</el-button> </el-button>
<el-button <el-button
type="warning" type="warning"
@ -52,7 +52,7 @@
@click="clearCanvas" @click="clearCanvas"
> >
<el-icon><RefreshLeft /></el-icon> <el-icon><RefreshLeft /></el-icon>
清空 {{ t.clear }}
</el-button> </el-button>
<el-button <el-button
type="success" type="success"
@ -60,20 +60,20 @@
@click="handleSave" @click="handleSave"
> >
<el-icon><Check /></el-icon> <el-icon><Check /></el-icon>
确定 {{ t.confirm }}
</el-button> </el-button>
</div> </div>
</div> </div>
<div class="canvas-wrapper" ref="canvasWrapper"> <div class="canvas-wrapper" ref="canvasWrapper">
<div v-if="imageLoading" class="loading-overlay"> <div v-if="imageLoading" class="loading-overlay">
<el-icon class="is-loading"><Loading /></el-icon> <el-icon class="is-loading"><Loading /></el-icon>
<span>加载图片中...</span> <span>{{ t.loadingImage }}</span>
</div> </div>
<div v-if="imageLoadError" class="error-overlay"> <div v-if="imageLoadError" class="error-overlay">
<el-icon><Warning /></el-icon> <el-icon><Warning /></el-icon>
<p>图片加载失败</p> <p>{{ t.imageLoadError }}</p>
<el-button type="primary" size="small" @click="retryLoadImage">重试</el-button> <el-button type="primary" size="small" @click="retryLoadImage">{{ t.retry }}</el-button>
<el-button size="small" @click="dialogVisible = false">关闭</el-button> <el-button size="small" @click="dialogVisible = false">{{ t.close }}</el-button>
</div> </div>
<canvas <canvas
v-show="!imageLoading && !imageLoadError" v-show="!imageLoading && !imageLoadError"
@ -92,7 +92,7 @@
v-model="editContent" v-model="editContent"
type="textarea" type="textarea"
:rows="3" :rows="3"
placeholder="请输入要修改的内容" :placeholder="t.editPlaceholder"
class="edit-textarea" class="edit-textarea"
></el-input> ></el-input>
</div> </div>
@ -101,7 +101,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue' import { ref, watch, nextTick, onMounted, onBeforeUnmount, computed } from 'vue'
import { Delete, RefreshLeft, Check, Loading, Warning } from '@element-plus/icons-vue' import { Delete, RefreshLeft, Check, Loading, Warning } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@ -113,11 +113,52 @@ const props = defineProps({
imageUrl: { imageUrl: {
type: String, type: String,
default: '' default: ''
},
language: {
type: String,
default: 'cn'
} }
}) })
const emit = defineEmits(['update:visible', 'add-prompt-card', 'close']) const emit = defineEmits(['update:visible', 'add-prompt-card', 'close'])
const i18n = {
cn: {
dialogTitle: '局部修改',
brushColor: '画笔颜色:',
brushSize: '画笔大小:',
eraser: '橡皮擦',
clear: '清空',
confirm: '确定',
loadingImage: '加载图片中...',
imageLoadError: '图片加载失败',
retry: '重试',
close: '关闭',
editPlaceholder: '请输入要修改的内容',
imageLoadFailed: '图片加载失败,请重试',
retryingImage: '正在重试加载图片',
maxRetryReached: '已达到最大重试次数,请稍后再试'
},
en: {
dialogTitle: 'Local Edit',
brushColor: 'Brush Color:',
brushSize: 'Brush Size:',
eraser: 'Eraser',
clear: 'Clear',
confirm: 'Confirm',
loadingImage: 'Loading image...',
imageLoadError: 'Failed to load image',
retry: 'Retry',
close: 'Close',
editPlaceholder: 'Please enter the content to modify',
imageLoadFailed: 'Failed to load image, please retry',
retryingImage: 'Retrying to load image',
maxRetryReached: 'Maximum retry attempts reached, please try again later'
}
}
const t = computed(() => i18n[props.language] || i18n.cn)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const isFullscreen = ref(false) const isFullscreen = ref(false)
const canvas = ref(null) const canvas = ref(null)
@ -194,17 +235,17 @@ const initCanvas = async () => {
console.error('Failed to load image:', props.imageUrl, error) console.error('Failed to load image:', props.imageUrl, error)
imageLoading.value = false imageLoading.value = false
imageLoadError.value = true imageLoadError.value = true
ElMessage.error('图片加载失败,请重试') ElMessage.error(t.value.imageLoadFailed)
} }
} }
const retryLoadImage = () => { const retryLoadImage = () => {
if (loadRetryCount.value < maxRetryCount) { if (loadRetryCount.value < maxRetryCount) {
loadRetryCount.value++ loadRetryCount.value++
ElMessage.info(`正在重试加载图片 (${loadRetryCount.value}/${maxRetryCount})`) ElMessage.info(`${t.value.retryingImage} (${loadRetryCount.value}/${maxRetryCount})`)
initCanvas() initCanvas()
} else { } else {
ElMessage.warning('已达到最大重试次数,请稍后再试') ElMessage.warning(t.value.maxRetryReached)
} }
} }

View File

@ -1,5 +1,8 @@
import 'element-plus/dist/index.css'
// UI组件库入口文件 // UI组件库入口文件
import LoadingCom from './components/LoadingCom/index.vue' import LoadingCom from './components/LoadingCom/index.vue'
import CanvasEditor from './components/CanvasEditor/CanvasEditor.vue'
import './style.css' import './style.css'
// 创建带有Dt前缀的组件 // 创建带有Dt前缀的组件
@ -10,15 +13,24 @@ const DtLoadingCom = {
app.component('DtLoadingCom', DtLoadingCom) app.component('DtLoadingCom', DtLoadingCom)
} }
} }
const DtCanvasEditor = {
...CanvasEditor,
name: 'DtCanvasEditor',
install(app) {
app.component('DtCanvasEditor', DtCanvasEditor)
}
}
// 组件列表 // 组件列表
const components = [ const components = [
DtLoadingCom DtLoadingCom,
DtCanvasEditor
] ]
// 导出组件 // 导出组件
export { export {
DtLoadingCom DtLoadingCom,
DtCanvasEditor
} }
// 批量注册组件的函数 // 批量注册组件的函数

View File

@ -7,6 +7,7 @@ import order from './order.js';
import user from './user.js'; import user from './user.js';
import logistics from './logistics.js'; import logistics from './logistics.js';
import agent from './agent.js'; import agent from './agent.js';
import rechargeconfig from './rechargeconfig.js';
export default { export default {
...meshy, ...meshy,
@ -18,4 +19,5 @@ export default {
...user, ...user,
...logistics, ...logistics,
...agent, ...agent,
...rechargeconfig,
}; };

View File

@ -0,0 +1,5 @@
const rechargeconfig = {
RECHARGE_PACKAGES:{url:'/api-core/front/order/recharge/packages',method:'POST',isLoading:true},//获取充值包列表
CREATE_RECHARGE_ORDER:{url:'/api-core/front/order/recharge/create',method:'POST',isLoading:true},//创建充值订单
}
export default rechargeconfig;

View File

@ -11,7 +11,7 @@ const getPorjectType = () => {
} }
// Node.js 环境 // Node.js 环境
if (typeof process !== 'undefined') { if (typeof process !== 'undefined') {
return process.env.VITE_PROJECTTYPE ; return process.env.VITE_PROJECTTYPE;
} }
}; };
export class FileServer { export class FileServer {
@ -31,7 +31,7 @@ export class FileServer {
//文件拼接 //文件拼接
concatUrl(url) { concatUrl(url) {
return urlRule.replace('IMGURL',url) return urlRule.replace('IMGURL', url)
} }
//生成唯一的缓存键 //生成唯一的缓存键
@ -285,8 +285,13 @@ export class FileServer {
}); });
} }
//上传文件 //上传文件
async uploadFile(url,config={}) { async uploadFile(url, config = {}) {
this.closeLoading = window.setElLoading(); this.closeLoading = window.setElLoading();
// 如果是网络路径直接返回
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
this.closeLoading.close();
return Promise.resolve(url);
}
let originalFileName = null; let originalFileName = null;
// 判断参数是否为File对象保存原始文件名 // 判断参数是否为File对象保存原始文件名
if (url instanceof File) { if (url instanceof File) {
@ -294,33 +299,27 @@ export class FileServer {
try { try {
url = await this.fileToBase64FromFile(url); url = await this.fileToBase64FromFile(url);
} catch (error) { } catch (error) {
console.error('File对象转base64失败:', error); console.error('File对象转base64失败:', error);
throw error; throw error;
} }
} }
// 如果是网络路径直接返回
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
return Promise.resolve(url);
}
const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (FileServer.fileCacheMap.has(cacheKey) && FileServer.fileCacheMap.get(cacheKey) != 'loading') {
if(FileServer.fileCacheMap.has(cacheKey)&&FileServer.fileCacheMap.get(cacheKey)!='loading'){
// resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey))); // resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey)));
resolve(FileServer.fileCacheMap.get(cacheKey)); resolve(FileServer.fileCacheMap.get(cacheKey));
return; return;
} }
let loadUrl = null; let loadUrl = null;
if(FileServer.fileCacheMap.get(cacheKey)=='loading'){ if (FileServer.fileCacheMap.get(cacheKey) == 'loading') {
loadUrl = await this.pollFileCacheMap(cacheKey); loadUrl = await this.pollFileCacheMap(cacheKey);
} }
if(loadUrl!='loading'&&loadUrl!=null){ if (loadUrl != 'loading' && loadUrl != null) {
// resolve(this.concatUrl(loadUrl)); // resolve(this.concatUrl(loadUrl));
resolve(loadUrl); resolve(loadUrl);
return return
} }
FileServer.fileCacheMap.set(cacheKey,'loading'); FileServer.fileCacheMap.set(cacheKey, 'loading');
let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象 let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
const fileName = originalFileName || this.extractFileName(url); const fileName = originalFileName || this.extractFileName(url);
const isModelFile = this.isModelFile(fileName); const isModelFile = this.isModelFile(fileName);
@ -346,17 +345,17 @@ export class FileServer {
try { try {
// const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData); // const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData);
let params = { let params = {
filename:fileName, filename: fileName,
file_type:file.type.split('/')[0], file_type: file.type.split('/')[0],
prefix:isModelFile ? 'uploads' : 'images', prefix: isModelFile ? 'uploads' : 'images',
...config ...config
} }
const requestUrl = this.RULE=='admin'?adminApi.default.adminUPLOADS3:clientApi.default.UPLOADS3; const requestUrl = this.RULE == 'admin' ? adminApi.default.adminUPLOADS3 : clientApi.default.UPLOADS3;
// const requestUrl = clientApi.default.UPLOADS3; // const requestUrl = clientApi.default.UPLOADS3;
const response = await requestUtils.common(requestUrl, params); const response = await requestUtils.common(requestUrl, params);
if(response.code==0){ if (response.code == 0) {
let data = response.data; let data = response.data;
let {url,fields,file_key,file_url } = data; let { url, fields, file_key, file_url } = data;
const formData = new FormData(); const formData = new FormData();
for (const key in fields) { for (const key in fields) {
formData.append(key, fields[key]); formData.append(key, fields[key]);
@ -370,7 +369,7 @@ export class FileServer {
}); });
// 注意S3可能返回204或303状态码表示成功 // 注意S3可能返回204或303状态码表示成功
if (uploadResponse.status === 204 || uploadResponse.status === 303 || uploadResponse.ok) { if (uploadResponse.status === 204 || uploadResponse.status === 303 || uploadResponse.ok) {
if(file_url){ if (file_url) {
// 截取后八位作为缓存 key // 截取后八位作为缓存 key
FileServer.fileCacheMap.set(cacheKey, file_url); FileServer.fileCacheMap.set(cacheKey, file_url);
// resolve(urlRule.replace('IMGURL',file_url)); // resolve(urlRule.replace('IMGURL',file_url));
@ -393,6 +392,19 @@ export class FileServer {
} }
}) })
} }
// 从URL获取图片并返回Blob URL
async fetchImage(url) {
const cacheBusterUrl = url + '?v=1.0.0';
const response = await fetch(cacheBusterUrl, {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'no-cache'
});
const blob = await response.blob();
const imgurl = URL.createObjectURL(blob);
return imgurl;
}
//文件文件或者base64文件转为blob对象 //文件文件或者base64文件转为blob对象
fileToBlob(fileOrBase64) { fileToBlob(fileOrBase64) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -145,28 +145,50 @@ export class PayServer {
}) })
} }
//创建订单并且支付 //创建订单并且支付
async createPayorOrder(orderInfo) { async createPayorOrder(orderInfo, type = 1) {//1 ip订单 2充值包订单
// let payReducerUrl = 'https://www.deotaland.ai/#/order-management' let payReducerUrl = ''
let payReducerUrl = `${window.location.origin}/#/order-management`
// await this.init(); // await this.init();
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let pamras = { let pamras = {}
switch (type) {
case 1:
payReducerUrl = `${window.location.origin}/#/order-management`
pamras = {
product_id: orderInfo.product_id, product_id: orderInfo.product_id,
"methods": [ "methods": [
"card" "card"
], ],
"success_url":payReducerUrl, "success_url": payReducerUrl,
"cancel_url":payReducerUrl, "cancel_url": payReducerUrl,
"quantity":orderInfo.quantity, "quantity": orderInfo.quantity,
"project_id": orderInfo.project_id, "project_id": orderInfo.project_id,
"project_details": orderInfo.project_details, "project_details": orderInfo.project_details,
"order_info": orderInfo.order_info "order_info": orderInfo.order_info
} }
let res = await requestUtils.common(clientApi.default.createPayorOrder, pamras); break;
case 2:
payReducerUrl = `${window.location.origin}/#/points-recharge`
pamras = {
"package_id": orderInfo.package_id,
"methods": [
"card"
],
"success_url": payReducerUrl,
"cancel_url": payReducerUrl
}
break;
default:
break;
}
let requestUrl = {
1: clientApi.default.createPayorOrder,
2: clientApi.default.CREATE_RECHARGE_ORDER,
}[type]
let res = await requestUtils.common(requestUrl, pamras);
if (res.code == 0) { if (res.code == 0) {
let data = res.data let data = res.data
// return // return
window.location.href = data.url window.location.href = (data.url)||data.payment_url
resolve(data); resolve(data);
} else { } else {
reject(res.msg) reject(res.msg)

View File

@ -54,8 +54,9 @@ service.interceptors.request.use(
return config; return config;
}, },
error => { error => {
closeMethods?.close()
// 请求错误处理 // 请求错误处理
console.error('请求错误:', error); console.log('请求错误:', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
@ -84,6 +85,7 @@ service.interceptors.response.use(
return res; return res;
}, },
error => { error => {
closeMethods?.close()
// 响应错误处理 // 响应错误处理
let message = '网络请求失败'; let message = '网络请求失败';
if (error.response) { if (error.response) {
@ -108,7 +110,6 @@ service.interceptors.response.use(
// 请求已发送但没有收到响应 // 请求已发送但没有收到响应
message = '网络连接失败,请检查网络'; message = '网络连接失败,请检查网络';
} }
console.error(message);
return Promise.reject(error); return Promise.reject(error);
} }
); );

View File

@ -117,6 +117,9 @@ importers:
'@google/genai': '@google/genai':
specifier: ^1.27.0 specifier: ^1.27.0
version: 1.27.0 version: 1.27.0
'@splinetool/runtime':
specifier: ^1.12.29
version: 1.12.29
'@twind/core': '@twind/core':
specifier: ^1.1.3 specifier: ^1.1.3
version: 1.1.3 version: 1.1.3
@ -1088,6 +1091,13 @@ packages:
dev: true dev: true
optional: true optional: true
/@splinetool/runtime@1.12.29:
resolution: {integrity: sha512-uxqg497LrJdYy3bEE3fBVsnNJvy6xORq7+3kCrFkCsFYeQmffzcASRJr2giEcOgR7UKm+0KHVy/annXPBXBNfQ==}
dependencies:
on-change: 4.0.2
semver-compare: 1.0.0
dev: false
/@sxzz/popperjs-es@2.11.7: /@sxzz/popperjs-es@2.11.7:
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
dev: false dev: false
@ -3084,6 +3094,11 @@ packages:
boolbase: 1.0.0 boolbase: 1.0.0
dev: true dev: true
/on-change@4.0.2:
resolution: {integrity: sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/once@1.4.0: /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
@ -3408,6 +3423,10 @@ packages:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
dev: true dev: true
/semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
dev: false
/semver@7.7.3: /semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'} engines: {node: '>=10'}