This commit is contained in:
parent
324b670ed5
commit
4c59a1349d
|
|
@ -128,6 +128,7 @@ window.setElLoading = (qp=false)=>{
|
|||
}else{
|
||||
loading.value = true
|
||||
}
|
||||
window.closeMethods = closeMethods;
|
||||
return closeMethods
|
||||
}
|
||||
window.closeMethods = closeMethods;
|
||||
|
|
|
|||
|
|
@ -797,7 +797,7 @@ export default {
|
|||
animal: 'Animal',
|
||||
person: 'Person',
|
||||
general: 'General',
|
||||
O1: 'O1',
|
||||
E1: 'E1',
|
||||
title: 'Title',
|
||||
enterTitle: 'Please enter prompt title',
|
||||
content: 'Content',
|
||||
|
|
|
|||
|
|
@ -791,7 +791,7 @@ orderManagement: {
|
|||
animal: '动物',
|
||||
person: '人物',
|
||||
general: '通用',
|
||||
O1: 'O1',
|
||||
E1: 'E1',
|
||||
title: '标题',
|
||||
enterTitle: '请输入提示词标题',
|
||||
content: '内容',
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@
|
|||
</template>
|
||||
</el-dialog>
|
||||
<!-- 画布编辑器对话框 -->
|
||||
<CanvasEditor
|
||||
<DtCanvasEditor
|
||||
v-model:visible="canvasEditorVisible"
|
||||
:image-url="canvasEditorImageUrl"
|
||||
@add-prompt-card="handleCanvasSave"
|
||||
|
|
@ -354,7 +354,6 @@ import {
|
|||
import ModelViewer from '@/components/common/ModelViewer.vue'
|
||||
import ModelCom from '@/components/modelCom/index.vue'
|
||||
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
|
||||
import CanvasEditor from '@/components/common/CanvasEditor.vue'
|
||||
import ModelUploadModal from '@/components/ModelUploadModal/index.vue'
|
||||
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
|
||||
import { MeshyServer,GiminiServer,FileServer } from '@deotaland/utils';
|
||||
|
|
@ -412,7 +411,6 @@ const handlePartialEdit = (imageUrl, index) => {
|
|||
currentEditImageIndex.value = index;
|
||||
canvasEditorVisible.value = true;
|
||||
}
|
||||
|
||||
const handleCanvasSave = (editedImageUrl,editContent) => {
|
||||
fileServer.uploadFile(editedImageUrl).then((url) => {
|
||||
const newItem = {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
|
||||
<el-option :label="t('admin.promptManagement.person')" value="person" />
|
||||
<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-form-item>
|
||||
|
||||
|
|
|
|||
|
|
@ -522,7 +522,6 @@ const formatDate = (dateString) => {
|
|||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error(t('admin.productManagement.imageTypeError'))
|
||||
return false
|
||||
|
|
@ -539,12 +538,12 @@ const handleImageChange = (file) => {
|
|||
if (!beforeImageUpload(file.raw)) {
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const imageUrl = e.target.result
|
||||
fileServer.uploadFile(imageUrl).then((url) => {
|
||||
formData.value.image = url
|
||||
window?.closeMethods?.close();
|
||||
ElMessage.success(t('admin.productManagement.imageUploadSuccess'))
|
||||
}).catch((error) => {
|
||||
console.error('图片上传失败:', error)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export default defineConfig({
|
|||
// 配置代理解决CORS问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.deotaland.ai',
|
||||
// target: 'http://api.deotaland.local',
|
||||
// target: 'https://api.deotaland.ai',
|
||||
target: 'http://api.deotaland.local',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@deotaland/utils": "workspace:*",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@google/genai": "^1.27.0",
|
||||
"@splinetool/runtime": "^1.12.29",
|
||||
"@twind/core": "^1.1.3",
|
||||
"@twind/preset-autoprefix": "^1.0.7",
|
||||
"@twind/preset-tailwind": "^1.1.4",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ window.setElLoading = (qp=false)=>{
|
|||
}else{
|
||||
loading.value = true
|
||||
}
|
||||
window.closeMethods = closeMethods;
|
||||
return closeMethods
|
||||
}
|
||||
window.closeMethods = closeMethods;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -37,8 +37,7 @@
|
|||
<div class="guide-text-container">
|
||||
<div class="text-content">
|
||||
<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 class="tips-icon">💡</div>
|
||||
|
|
@ -113,30 +112,30 @@ const guideSteps = computed(() => [
|
|||
id: 1,
|
||||
title: t('guideModal.step1.title'),
|
||||
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')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t('guideModal.step2.title'),
|
||||
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')
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t('guideModal.step3.title'),
|
||||
description: t('guideModal.step3.description'),
|
||||
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/47cdd95f-23fb-486b-bd19-5fca67c2ce45.png',
|
||||
tips: t('guideModal.step3.tips')
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t('guideModal.step4.title'),
|
||||
description: t('guideModal.step4.description'),
|
||||
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/690f2da4-400e-4cb0-a815-246e614d79b1.png',
|
||||
tips: t('guideModal.step4.tips')
|
||||
}
|
||||
// {
|
||||
// id: 3,
|
||||
// title: t('guideModal.step3.title'),
|
||||
// description: t('guideModal.step3.description'),
|
||||
// image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/47cdd95f-23fb-486b-bd19-5fca67c2ce45.png',
|
||||
// tips: t('guideModal.step3.tips')
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// title: t('guideModal.step4.title'),
|
||||
// description: t('guideModal.step4.description'),
|
||||
// image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/690f2da4-400e-4cb0-a815-246e614d79b1.png',
|
||||
// tips: t('guideModal.step4.tips')
|
||||
// }
|
||||
]);
|
||||
|
||||
// 计算属性:当前步骤数据
|
||||
|
|
@ -312,7 +311,7 @@ const skipGuide = () => {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 350px;
|
||||
max-height:350px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
|
|
@ -321,7 +320,7 @@ const skipGuide = () => {
|
|||
.guide-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@
|
|||
<MagicStick />
|
||||
</el-icon>
|
||||
<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>
|
||||
<LanguageToggle
|
||||
|
|
@ -64,6 +67,7 @@
|
|||
<script setup>
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElIcon, ElInput } from 'element-plus'
|
||||
import ThemeToggle from '../ui/ThemeToggle.vue'
|
||||
|
|
@ -79,14 +83,17 @@ const props = defineProps({
|
|||
default: 'project'
|
||||
}
|
||||
})
|
||||
// 编辑相关状态
|
||||
const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const handleRecharge = () => {
|
||||
router.push('/points-recharge')
|
||||
}
|
||||
|
||||
const isEditing = ref(false)
|
||||
const editedProjectName = ref('')
|
||||
const editInput = ref(null)
|
||||
|
||||
// 国际化支持
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// 语言切换功能已由LanguageToggle组件内部处理
|
||||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
|
|
@ -412,6 +419,48 @@ html.dark .project-name-input :deep(.el-input__wrapper:focus) {
|
|||
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 {
|
||||
display: flex;
|
||||
|
|
@ -508,6 +557,11 @@ html.dark .count-text {
|
|||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.count-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
ref="textInputRef"
|
||||
v-model="textInputValue"
|
||||
class="text-input-area"
|
||||
placeholder="请输入文本内容..."
|
||||
:placeholder="t('modelModal.textInputPlaceholder')"
|
||||
rows="4"
|
||||
@keydown.enter.ctrl="handleTextInputConfirm"
|
||||
@keydown.esc="handleTextInputCancel"
|
||||
|
|
@ -53,6 +53,9 @@
|
|||
<!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> -->
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<button v-if="formData.internalImageUrl" class="customize-to-home-btn" @click="handleCustomizeToHome" @touchend.prevent="handleCustomizeToHome">
|
||||
{{ t('modelModal.customizeToHome') }}
|
||||
</button>
|
||||
</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">
|
||||
<el-icon class="btn-icon"><Grid /></el-icon>
|
||||
</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>
|
||||
|
|
@ -80,6 +87,7 @@
|
|||
import {cjt} from './tsc.js'
|
||||
// import cjimg from '@/assets/sketches/cjt.png';
|
||||
import { computed, ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { GiminiServer } from '@deotaland/utils';
|
||||
// import humanTypeImg from '@/assets/sketches/tcww.png'
|
||||
// import humanTypeImg from '@/assets/sketches/tcww2.webp'
|
||||
|
|
@ -87,8 +95,9 @@ import { GiminiServer } from '@deotaland/utils';
|
|||
// import anTypeImg from '@/assets/sketches/dwww2.png';
|
||||
// import cz2 from '@/assets/material/cz2.png';
|
||||
// 引入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'
|
||||
const { t } = useI18n();
|
||||
const formData = ref({
|
||||
internalImageUrl: '',//内部图片URL
|
||||
status:'loading',//状态
|
||||
|
|
@ -138,6 +147,12 @@ const handleTextcjt = ()=>{
|
|||
cardData: props.cardData
|
||||
});
|
||||
}
|
||||
const handleCustomizeToHome = () => {
|
||||
emit('customize-to-home', {
|
||||
imageUrl: formData.value.internalImageUrl,
|
||||
cardData: props.cardData
|
||||
});
|
||||
}
|
||||
// 处理文本输入确认
|
||||
const handleTextInputConfirm = () => {
|
||||
// 触发创建新卡片事件,传递用户输入的文本内容
|
||||
|
|
@ -173,7 +188,7 @@ const handleTouchEnd = () => {
|
|||
};
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
combinedPromptJson:{
|
||||
combinedPromptJson:{//动态提示词
|
||||
type: Object,
|
||||
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 iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
|
||||
|
|
@ -222,72 +239,32 @@ const handleGenerateImage = async () => {
|
|||
if (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){
|
||||
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
|
||||
referenceImages.push(props.cardData.diyPromptImg);
|
||||
referenceImages.push(cjimg);
|
||||
}
|
||||
// if(props.cardData.diyPromptText){
|
||||
// console.log(props.cardData.diyPromptImg,'diyPromptImgdiyPromptImgdiyPromptImg');
|
||||
// referenceImages.push(props.cardData.diyPromptImg);
|
||||
// if(iscjt){
|
||||
// props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
|
||||
// referenceImages.push(cjimg);
|
||||
// }
|
||||
// }else{
|
||||
// referenceImages.push(humanTypeImg);
|
||||
// referenceImages.push(anTypeImg);
|
||||
// }
|
||||
// referenceImages.push(cz2);
|
||||
// referenceImages.push(humanTypeImg);
|
||||
// if(props?.cardData?.selectedExpression){
|
||||
// referenceImages.push(props.cardData.selectedExpression.imageUrl);
|
||||
// }
|
||||
// 忽略第二张参考图
|
||||
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。
|
||||
`
|
||||
;
|
||||
}
|
||||
if(props.cardData.diyPromptText){
|
||||
referenceImages.push(props.cardData.diyPromptImg);
|
||||
}
|
||||
let dtprompt;
|
||||
if(props?.cardData?.ipType==1){
|
||||
dtprompt = props.combinedPromptJson.person.content;
|
||||
referenceImages.push(...props.combinedPromptJson.person.imgs);
|
||||
}else if(props?.cardData?.ipType==2){
|
||||
dtprompt = props.combinedPromptJson.animal.content;
|
||||
referenceImages.push(...props.combinedPromptJson.animal.imgs);
|
||||
}
|
||||
if(props.cardData.prompt){
|
||||
dtprompt = `角色外观:${props.cardData.prompt}.${dtprompt}`
|
||||
}
|
||||
let prompt = props.cardData.diyPromptText|| dtprompt
|
||||
// 角色姿势:${props.cardData.ipType==1?``:``}
|
||||
|
||||
if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){
|
||||
prompt = '按原图生成'
|
||||
referenceImages = [props.cardData.inspirationImage];
|
||||
formData.value.internalImageUrl = props?.cardData?.inspirationImage;
|
||||
return
|
||||
}
|
||||
const taskResult = await giminiServer.handleGenerateImage(referenceImages, prompt,{
|
||||
project_id: props.cardData.project_id,
|
||||
|
|
@ -301,6 +278,7 @@ const handleGenerateImage = async () => {
|
|||
formData.value.status = 'success';
|
||||
saveProject();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
emit('delete');
|
||||
}
|
||||
};
|
||||
|
|
@ -686,6 +664,77 @@ const handleImageLoad = (event) => {
|
|||
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 {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -53,31 +53,43 @@
|
|||
<!-- <el-skeleton-item variant="text" style="width:100%;height: 100%;" /> -->
|
||||
</template>
|
||||
</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') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧控件区域 -->
|
||||
<div class="right-controls-container" v-if="formData.internalImageUrl">
|
||||
<!-- 右侧圆形按钮控件 -->
|
||||
<div class="right-circular-controls">
|
||||
<button class="control-button share-btn" title="Preview Image" @click="handleImageClick" @touchend.prevent="handleImageClick">
|
||||
<el-icon class="btn-icon"><View /></el-icon>
|
||||
<!-- 上侧控件区域 -->
|
||||
<div class="top-controls-container" v-if="formData.internalImageUrl">
|
||||
<!-- 上侧按钮控件 -->
|
||||
<div class="top-controls">
|
||||
<button class="control-item" title="Preview Image" @click="handleImageClick" @touchend.prevent="handleImageClick">
|
||||
<span class="control-text">{{ t('modelModal.preview') }}</span>
|
||||
<el-icon class="control-icon"><View /></el-icon>
|
||||
</button>
|
||||
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="model" @click="handleGenerateModel" @touchend.prevent="handleGenerateModel">
|
||||
<el-icon class="btn-icon"><Cpu /></el-icon>
|
||||
</button>
|
||||
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="textInput" @click="toggleTextInput" @touchend.prevent="toggleTextInput">
|
||||
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
|
||||
<!-- <button v-if="!(props.cardData.imgyt)" class="control-item" title="model" @click="handleGenerateModel" @touchend.prevent="handleGenerateModel">
|
||||
<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 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>
|
||||
<button v-if="!(props.cardData.diyPromptText==cjt)" class="control-item" title="Picture board editor" @click="handlePartialEdit" @touchend.prevent="handlePartialEdit">
|
||||
<span class="control-text">{{ t('modelModal.edit') }}</span>
|
||||
<el-icon class="control-icon"><EditPen /></el-icon>
|
||||
</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 v-if="!(props.cardData.diyPromptText==cjt)" class="control-item" title="scene graph" @click="handleTextcjt" @touchend.prevent="handleTextcjt">
|
||||
<span class="control-text">{{ t('modelModal.sceneGraph') }}</span>
|
||||
<el-icon class="control-icon"><Grid /></el-icon>
|
||||
</button>
|
||||
<button class="control-item" title="Download" @click="downloadImage" @touchend.prevent="downloadImage">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,7 +107,7 @@ import { GiminiServer } from '@deotaland/utils';
|
|||
// import anTypeImg from '@/assets/sketches/dwww2.png';
|
||||
// import cz2 from '@/assets/material/cz2.png';
|
||||
// 引入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'
|
||||
const { t } = useI18n();
|
||||
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 = ()=>{
|
||||
emit('handlePartialEdit', formData.value.internalImageUrl);
|
||||
}
|
||||
//删除
|
||||
const handleDeleteCard = ()=>{
|
||||
emit('delete-card');
|
||||
}
|
||||
const downloadImage = async ()=>{
|
||||
emit('download-image', formData.value.internalImageUrl);
|
||||
}
|
||||
// 处理图片生成
|
||||
const handleGenerateImage = async () => {
|
||||
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;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 24px;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 右侧圆形按钮控件 */
|
||||
.right-circular-controls {
|
||||
/* 上侧按钮控件 */
|
||||
.top-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
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;
|
||||
transform: translateX(-20px);
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* 当鼠标悬停在卡片容器上时显示控件 */
|
||||
.ip-card-container:hover .right-circular-controls {
|
||||
.ip-card-container:hover .top-controls {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 在 iPad 及更大触控屏上,按下即显示右侧圆形控件 */
|
||||
/* 在 iPad 及更大触控屏上,按下即显示上侧控件 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.ip-card-container:active .right-circular-controls,
|
||||
.ip-card-container.controls-visible .right-circular-controls {
|
||||
.ip-card-container:active .top-controls,
|
||||
.ip-card-container.controls-visible .top-controls {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 非触控设备保持 hover 显示 */
|
||||
@media (hover: hover) {
|
||||
.ip-card-container:hover .right-circular-controls {
|
||||
.ip-card-container:hover .top-controls {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配:点击卡片后显示功能按钮 */
|
||||
@media (max-width: 1024px) {
|
||||
/* 点击卡片容器时显示功能按钮 */
|
||||
.ip-card-container:active .right-circular-controls,
|
||||
.ip-card-container.controls-visible .right-circular-controls {
|
||||
.ip-card-container:active .top-controls,
|
||||
.ip-card-container.controls-visible .top-controls {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.control-button {
|
||||
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;
|
||||
.control-item {
|
||||
display: flex;
|
||||
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;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.25) 0%, rgba(107, 70, 193, 0.2) 100%);
|
||||
.control-item:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
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);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 20px;
|
||||
.control-text {
|
||||
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 {
|
||||
|
|
@ -1016,21 +1052,32 @@ const handleImageLoad = (event) => {
|
|||
transform: none;
|
||||
}
|
||||
|
||||
.right-controls-container {
|
||||
.top-controls-container {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-left: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.right-circular-controls {
|
||||
flex-direction: row;
|
||||
.top-controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -258,7 +258,8 @@ onMounted(()=>{
|
|||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
/* background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); */
|
||||
background-color: #fdfdfd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -800,9 +800,11 @@ const handleFileChange = async (event) => {
|
|||
try {
|
||||
const imgUrl = await filePlug.uploadFile(file);
|
||||
formData.value.previewImage = imgUrl;
|
||||
window?.closeMethods.close();
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
ElMessage.error('图片上传失败,请重试');
|
||||
window?.closeMethods.close();
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -643,7 +643,7 @@ onUnmounted(() => {
|
|||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
z-index: 20;
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2);
|
||||
/* transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); */
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ watch(() => window.location.pathname, () => {
|
|||
top: 0;
|
||||
/* left: -120px; */
|
||||
transform: translateX(-100%);
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 60px;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -125,7 +125,12 @@ export default {
|
|||
},
|
||||
modelModal: {
|
||||
customizeToHome: '定制到家',
|
||||
textInputPlaceholder: '请输入调整内容,例如:更改角色表情'
|
||||
textInputPlaceholder: '请输入调整内容,例如:更改角色表情',
|
||||
preview: '预览',
|
||||
modify: '修改',
|
||||
sceneGraph: '场景图',
|
||||
edit: '编辑',
|
||||
download: '下载'
|
||||
},
|
||||
modelCard: {
|
||||
generateModelButton: '生成模型',
|
||||
|
|
@ -212,6 +217,7 @@ export default {
|
|||
modelFreeCount: '模型免费',
|
||||
times: '次',
|
||||
remainingCredits: '剩余积分',
|
||||
recharge: '充值',
|
||||
guide: '使用指南',
|
||||
back: '返回',
|
||||
skip: '跳过',
|
||||
|
|
@ -334,13 +340,16 @@ export default {
|
|||
guideModal: {
|
||||
step1: {
|
||||
title: '参考图片',
|
||||
description: '选择您喜欢的图片作为创作参考',
|
||||
tips: '点击生成按钮后,平台会根据您的选择生成相应的3D模型。'
|
||||
description: '输入文字描述您的IP,或者选择您喜欢的图片作为创作参考',
|
||||
tips: '点击生成按钮后,平台会根据您的选择生成相应的图片。'
|
||||
},
|
||||
step2: {
|
||||
title: '模型生成/文字优化',
|
||||
description: '根据您的参考图片,平台会生成对应的3D模型。',
|
||||
tips: '您也可以输入文字描述,平台会根据您的需求进行图片优化。'
|
||||
title: '图片节点按钮控件',
|
||||
description: `
|
||||
①.<text style="font-weight: bold;">customizeToHome</text>:将您的IP角色定制为您的专属3D模型机器人。<br/>
|
||||
②.<text style="font-weight: bold;">controls</text>:图片节点控件,支持修改预览和删除等操作<br/>
|
||||
`,
|
||||
tips: '您通过画布或者聊天框修改图片,平台会根据您的需求进行图片优化。'
|
||||
},
|
||||
step3: {
|
||||
title: '查看详情',
|
||||
|
|
@ -1088,6 +1097,7 @@ export default {
|
|||
currentPoints: '当前积分',
|
||||
expiryDate: '积分到期时间',
|
||||
pointsList: '积分明细',
|
||||
recharge: '充值',
|
||||
consumptionRules: {
|
||||
title: '积分消耗规则',
|
||||
behavior: '行为',
|
||||
|
|
@ -1166,10 +1176,10 @@ export default {
|
|||
},
|
||||
premium: {
|
||||
name: '高级套餐',
|
||||
badge: '热门'
|
||||
badge: '推荐'
|
||||
}
|
||||
},
|
||||
period: '年',
|
||||
period: '天',
|
||||
points: '积分',
|
||||
validity: '有效期',
|
||||
features: {
|
||||
|
|
@ -1296,7 +1306,8 @@ export default {
|
|||
goHome: '返回首页',
|
||||
submitInviteCode: '提交邀请码',
|
||||
hasInviteCode: '已有邀请码?',
|
||||
inviteCodePlaceholder: '填写邀请码升级为免费会员'
|
||||
inviteCodePlaceholder: '填写邀请码升级为免费会员',
|
||||
logout: '退出登录'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
|
|
@ -1326,10 +1337,10 @@ export default {
|
|||
},
|
||||
premium: {
|
||||
name: 'Premium Plan',
|
||||
badge: 'HOT'
|
||||
badge: 'Picks'
|
||||
}
|
||||
},
|
||||
period: 'year',
|
||||
period: 'day',
|
||||
points: 'Points',
|
||||
validity: 'Validity',
|
||||
features: {
|
||||
|
|
@ -1448,7 +1459,12 @@ export default {
|
|||
},
|
||||
modelModal: {
|
||||
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: {
|
||||
generateModelButton: 'Generate Model',
|
||||
|
|
@ -1535,6 +1551,7 @@ export default {
|
|||
modelFreeCount: 'Free Model',
|
||||
times: 'times',
|
||||
remainingCredits: 'Remaining Credits',
|
||||
recharge: 'Recharge',
|
||||
guide: 'User Guide',
|
||||
back: 'Back',
|
||||
skip: 'Skip',
|
||||
|
|
@ -1552,6 +1569,7 @@ export default {
|
|||
currentPoints: 'Current Points',
|
||||
expiryDate: 'Points Expiry Date',
|
||||
pointsList: 'Points Details',
|
||||
recharge: 'Recharge',
|
||||
consumptionRules: {
|
||||
title: 'Points Consumption Rules',
|
||||
behavior: 'Behavior',
|
||||
|
|
@ -1732,9 +1750,9 @@ export default {
|
|||
},
|
||||
guideModal: {
|
||||
step1: {
|
||||
title: 'Reference Images',
|
||||
description: 'Select images you like as creative references',
|
||||
tips: 'After clicking the generate button, the platform will generate corresponding 3D models based on your selection.'
|
||||
title: 'Reference Image',
|
||||
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 the corresponding image based on your selection.'
|
||||
},
|
||||
step2: {
|
||||
title: 'Model Generation / Text Optimization',
|
||||
|
|
@ -2585,7 +2603,8 @@ export default {
|
|||
goHome: 'Go Home',
|
||||
submitInviteCode: 'Submit 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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { nextTick } from 'vue'
|
||||
import NProgress from 'nprogress'
|
||||
|
|
@ -17,50 +17,56 @@ const AddAgent = () => import('../views/AddAgent.vue')
|
|||
const DeviceList = () => import('../views/DeviceList.vue')
|
||||
const UiTest = () => import('../views/UiTest.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 NotFound = () => import('../views/NotFound.vue')
|
||||
const Waitlist = () => import('../views/Waitlist.vue')
|
||||
NProgress.configure({
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
})// 开启轻量模式(顶部细线)
|
||||
})// 开启轻量模式(顶部细线)
|
||||
// 路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: home,
|
||||
meta: { fullScreen: false }
|
||||
},{
|
||||
path: '/login',
|
||||
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
|
||||
}, {
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
meta: { requiresGuest: true }
|
||||
meta: { requiresGuest: true }
|
||||
},
|
||||
{
|
||||
path: '/login/phone',
|
||||
{
|
||||
path: '/login/phone',
|
||||
name: 'phone-login',
|
||||
component: () => import('@/views/Login/PhoneLogin.vue'),
|
||||
meta: { requiresGuest: true, fullScreen: true }
|
||||
meta: { requiresGuest: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/czhome',
|
||||
name: 'czhome',
|
||||
{
|
||||
path: '/czhome',
|
||||
name: 'czhome',
|
||||
component: ModernHome,
|
||||
meta: { requiresAuth: false, keepAlive: false }
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
meta: { requiresGuest: true, fullScreen: true }
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
meta: { requiresGuest: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
component: ForgotPassword,
|
||||
meta: { requiresGuest: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/Waitlist',
|
||||
name: 'Waitlist',
|
||||
component: Waitlist, // 升级页
|
||||
meta: { requiresAuth: true, keepAlive: false, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
|
@ -70,44 +76,44 @@ const routes = [
|
|||
]
|
||||
//免费会员/达人会员动态路由
|
||||
export const freeRoutes = [
|
||||
{
|
||||
path: '/ui-test',
|
||||
name: 'ui-test',
|
||||
{
|
||||
path: '/ui-test',
|
||||
name: 'ui-test',
|
||||
component: UiTest,
|
||||
meta: { requiresAuth: false, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/creation-workspace',
|
||||
name: 'creation-workspace',
|
||||
{
|
||||
path: '/creation-workspace',
|
||||
name: 'creation-workspace',
|
||||
component: CreationWorkspace,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/project-gallery',
|
||||
name: 'project-gallery',
|
||||
{
|
||||
path: '/project-gallery',
|
||||
name: 'project-gallery',
|
||||
component: ProjectGallery,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/order-management',
|
||||
name: 'order-management',
|
||||
{
|
||||
path: '/order-management',
|
||||
name: 'order-management',
|
||||
component: OrderManagement,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/order-management/:orderId',
|
||||
name: 'order-detail',
|
||||
{
|
||||
path: '/order-management/:orderId',
|
||||
name: 'order-detail',
|
||||
component: OrderDetail,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/agent-management',
|
||||
path: '/agent-management',
|
||||
name: 'agent-management',
|
||||
component: AgentManagement,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/user-center',
|
||||
path: '/user-center',
|
||||
name: 'user-center',
|
||||
component: UserCenter,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
|
|
@ -118,39 +124,39 @@ export const freeRoutes = [
|
|||
component: AddAgent,
|
||||
meta: { requiresAuth: true, fullScreen: false }
|
||||
},
|
||||
{
|
||||
path: '/device-settings',
|
||||
name: 'device-settings',
|
||||
{
|
||||
path: '/device-settings',
|
||||
name: 'device-settings',
|
||||
component: DeviceSettings,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/device-list/:agentId',
|
||||
name: 'device-list',
|
||||
{
|
||||
path: '/device-list/:agentId',
|
||||
name: 'device-list',
|
||||
component: DeviceList,
|
||||
meta: { requiresAuth: true, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/project/:id/:series',
|
||||
name: 'project',
|
||||
{
|
||||
path: '/project/:id/:series',
|
||||
name: 'project',
|
||||
component: () => import('../views/Project/CreateProject.vue'),
|
||||
meta: { requiresAuth: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/purchase',
|
||||
name: 'model-purchase',
|
||||
{
|
||||
path: '/purchase',
|
||||
name: 'model-purchase',
|
||||
component: () => import('../views/ModelPurchase.vue'),
|
||||
meta: { requiresAuth: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/points-recharge',
|
||||
name: 'points-recharge',
|
||||
{
|
||||
path: '/points-recharge',
|
||||
name: 'points-recharge',
|
||||
component: PointsRecharge,
|
||||
meta: { requiresAuth: true,fullScreen: true }
|
||||
meta: { requiresAuth: true, fullScreen: true }
|
||||
},
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: List,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
|
|
@ -163,7 +169,7 @@ export const freeRoutes = [
|
|||
]
|
||||
const router = createRouter({
|
||||
// history: createWebHistory(),
|
||||
history:createWebHashHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
// 路由守卫
|
||||
|
|
@ -171,9 +177,9 @@ router.beforeEach(async (to, from, next) => {
|
|||
NProgress.start()
|
||||
// if(window.location.hostname=='localhost'){
|
||||
// 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')
|
||||
// 如果有 token,跳转到首页
|
||||
if (token) {
|
||||
|
|
@ -181,7 +187,8 @@ router.beforeEach(async (to, from, next) => {
|
|||
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) {
|
||||
const token = localStorage.getItem('token')
|
||||
// 如果没有 token,跳转到登录页
|
||||
|
|
@ -195,27 +202,24 @@ router.beforeEach(async (to, from, next) => {
|
|||
// const info = await authStore.updateUserInfo();
|
||||
// console.log(info,'infoinfo');
|
||||
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();
|
||||
if(isDynamicRoute(to.path)) {
|
||||
if (isDynamicRoute(to.path)) {
|
||||
next('/czhome')
|
||||
setTimeout(() => {
|
||||
router.push(to.path)
|
||||
}, 20);
|
||||
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()
|
||||
})
|
||||
|
|
@ -228,7 +232,7 @@ function addDynamicRoutes() {
|
|||
}
|
||||
//恢复默认路由
|
||||
function removeDynamicRoutes() {
|
||||
if(router.getRoutes().length == routes.length){
|
||||
if (router.getRoutes().length == routes.length) {
|
||||
return
|
||||
}
|
||||
router.getRoutes().forEach(route => {
|
||||
|
|
|
|||
|
|
@ -243,7 +243,18 @@ const handleFileSelect = (event) => {
|
|||
}
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export class OrderManagement {
|
|||
}
|
||||
//获取订单列表
|
||||
getOrderList(params){
|
||||
params.source_type = 0
|
||||
return requestUtils.common(clientApi.default.getOrderList, params);
|
||||
}
|
||||
//获取订单详情
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@
|
|||
<button class="back-button" @click="goBack">
|
||||
← {{ t('common.back') }}
|
||||
</button>
|
||||
|
||||
<!-- 语言切换组件 -->
|
||||
<div class="language-switcher">
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ t('pointsRecharge.title') }}</h1>
|
||||
|
|
@ -21,68 +19,48 @@
|
|||
|
||||
<!-- 套餐选择 -->
|
||||
<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">
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<div class="plan-points">
|
||||
{{ plans.basic.points }} {{ t('pointsRecharge.points') }}
|
||||
{{ pkg.credits }} {{ t('pointsRecharge.points') }}
|
||||
</div>
|
||||
|
||||
<div class="plan-validity">
|
||||
{{ t('pointsRecharge.validity') }}: {{ plans.basic.validity }} {{ t('pointsRecharge.period') }}
|
||||
{{ t('pointsRecharge.validity') }}: {{ pkg.validity_days }} {{ t('pointsRecharge.period') }}
|
||||
</div>
|
||||
|
||||
<div class="plan-features">
|
||||
<ul>
|
||||
<li>{{ pkg.description }}</li>
|
||||
<li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li>
|
||||
<li>{{ t('pointsRecharge.features.prioritySupport') }}</li>
|
||||
<li>{{ t('pointsRecharge.features.securePayment') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="purchase-button" @click.stop="purchasePlan('basic')">
|
||||
{{ t('pointsRecharge.purchase') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 套餐卡片 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')">
|
||||
<button
|
||||
class="purchase-button"
|
||||
:class="{ 'premium-button': pkg.is_recommended === 1 }"
|
||||
@click.stop="purchasePlan(pkg)"
|
||||
>
|
||||
{{ t('pointsRecharge.purchase') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -101,48 +79,51 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import LanguageToggle from '@/components/ui/LanguageToggle.vue'
|
||||
|
||||
import { PointsRecharge } from './index.js'
|
||||
const pointsRecharge = new PointsRecharge()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式状态
|
||||
const selectedPlan = ref('basic')
|
||||
const selectedPlan = ref(null)
|
||||
const packagesList = ref([])
|
||||
|
||||
// 套餐数据
|
||||
const plans = ref({
|
||||
basic: {
|
||||
name: 'basic',
|
||||
points: 300,
|
||||
price: 30,
|
||||
validity: '1'
|
||||
},
|
||||
premium: {
|
||||
name: 'premium',
|
||||
points: 1000,
|
||||
price: 80,
|
||||
validity: '1'
|
||||
}
|
||||
const sortedPackages = computed(() => {
|
||||
return [...packagesList.value].sort((a, b) => a.sort_order - b.sort_order)
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
const history = router.options.history.state
|
||||
console.log(history);
|
||||
if(!history.back){
|
||||
router.replace('/user-center')
|
||||
}
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 选择套餐
|
||||
const selectPlan = (planName) => {
|
||||
selectedPlan.value = planName
|
||||
const selectPlan = (planId) => {
|
||||
selectedPlan.value = planId
|
||||
}
|
||||
|
||||
// 购买套餐
|
||||
const purchasePlan = (planName) => {
|
||||
console.log('购买套餐:', planName)
|
||||
// 这里可以添加购买逻辑
|
||||
const purchasePlan = (pkg) => {
|
||||
let parmas = {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -257,7 +238,8 @@ const purchasePlan = (planName) => {
|
|||
|
||||
.plan-card.active {
|
||||
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 {
|
||||
|
|
@ -267,7 +249,6 @@ const purchasePlan = (planName) => {
|
|||
/* 高级套餐样式 */
|
||||
.plan-card.premium {
|
||||
background: linear-gradient(135deg, #1F2937 0%, #374151 100%);
|
||||
border-color: #8B5CF6;
|
||||
}
|
||||
|
||||
/* 套餐头部 */
|
||||
|
|
@ -276,6 +257,8 @@ const purchasePlan = (planName) => {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
|
|
@ -283,6 +266,11 @@ const purchasePlan = (planName) => {
|
|||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #F9FAFB;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plan-badge {
|
||||
|
|
@ -292,6 +280,7 @@ const purchasePlan = (planName) => {
|
|||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 价格样式 */
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
>
|
||||
<!-- @mouseleave="stopElementDrag" -->
|
||||
<!-- 删除按钮 -->
|
||||
<button
|
||||
<!-- <button
|
||||
v-if="(card.imageUrl&&card.type==='image')||(card.type==='model'&&card.modelUrl)"
|
||||
class="delete-button"
|
||||
@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">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</button> -->
|
||||
|
||||
<!-- 根据卡片类型显示不同组件 -->
|
||||
<IPCard
|
||||
@delete-card="handleDeleteCard(index)"
|
||||
:combinedPromptJson="combinedPromptJson"
|
||||
@handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)"
|
||||
@customize-to-home="handleCustomizeToHome(index)"
|
||||
@preview-image="handlePreviewImage"
|
||||
@generate-smooth-white-model="(imageUrl)=>handleGenerateSmoothWhiteModel(index,imageUrl)"
|
||||
@create-new-card="(data)=>handleCreateFourViewCard(index,data)"
|
||||
@delete="handleDeleteCard(index)"
|
||||
@download-image="downloadImage"
|
||||
:projectId="projectId"
|
||||
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
|
||||
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
|
||||
|
|
@ -123,8 +124,9 @@
|
|||
:initialIndex="currentImageIndex"
|
||||
@close="showImagePreview = false"
|
||||
/>
|
||||
<CanvasEditor
|
||||
v-model:visible="canvasEditorVisible"
|
||||
<DtCanvasEditor
|
||||
:language="locale"
|
||||
v-model:visible="canvasEditorVisible"
|
||||
:image-url="canvasEditorImageUrl"
|
||||
@add-prompt-card="handleCanvasSave"
|
||||
/>
|
||||
|
|
@ -167,6 +169,8 @@ import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
|
|||
import PurchaseModal from '../../components/PurchaseModal/index.vue';
|
||||
import {Project} from './index';
|
||||
import {ModernHome} from '../ModernHome/index.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { locale } = useI18n()
|
||||
const fileServer = new FileServer();
|
||||
const modernHome = new ModernHome();
|
||||
const router = useRouter();
|
||||
|
|
@ -197,6 +201,19 @@ const getGenerateCount = async ()=>{
|
|||
// Limits.value.generateCount = data[0].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) => {
|
||||
canvasEditorImageUrl.value = imageUrl;
|
||||
canvasEditorVisible.value = true;
|
||||
|
|
@ -219,6 +236,14 @@ const handlePartialEdit = (imageUrl, index) => {
|
|||
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);
|
||||
//获取最大z-index+1
|
||||
const getMaxZIndex = (type)=>{
|
||||
|
|
@ -286,6 +311,7 @@ const handleSaveProject = (index,item,type='image')=>{
|
|||
const createProject = async ()=>{
|
||||
const {id} = await PluginProject.createProject();
|
||||
// 创建新项目后,将当前路由跳转到 project/项目id/系列
|
||||
|
||||
await router.replace(`/project/${id}/${series.value}`);
|
||||
projectId.value = id;
|
||||
getProjectInfo(id);
|
||||
|
|
@ -341,11 +367,15 @@ const closeImportModal = () => {
|
|||
// 关闭引导弹窗
|
||||
const closeGuideModal = () => {
|
||||
showGuideModal.value = false;
|
||||
const currentDate = new Date().toDateString();
|
||||
localStorage.setItem('lastShowGuideModalDate', currentDate);
|
||||
};
|
||||
|
||||
// 完成引导
|
||||
const completeGuide = () => {
|
||||
showGuideModal.value = false;
|
||||
const currentDate = new Date().toDateString();
|
||||
localStorage.setItem('lastShowGuideModalDate', currentDate);
|
||||
};
|
||||
|
||||
// ==================== 定位计算器 ====================
|
||||
|
|
@ -1027,7 +1057,6 @@ const init = ()=>{
|
|||
const route = useRoute();
|
||||
projectId.value = route.params.id;
|
||||
series.value = route.params.series;
|
||||
console.log(series.value);
|
||||
if(projectId.value === 'new'){
|
||||
createProject();
|
||||
return
|
||||
|
|
@ -1037,6 +1066,7 @@ const init = ()=>{
|
|||
}
|
||||
// 组件挂载时添加事件监听器
|
||||
onMounted(() => {
|
||||
showGuideModalLogic();
|
||||
MeshyServer.pollingEnabled = true;
|
||||
GiminiServer.pollingEnabled = true;
|
||||
// 每次进入都显示引导弹窗
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -26,13 +26,16 @@
|
|||
<button
|
||||
class="primary-button"
|
||||
@click="submitInviteCode"
|
||||
:loading="submitting"
|
||||
:disabled="submitting"
|
||||
>
|
||||
{{ t('waitlist.submitInviteCode') }}
|
||||
</button>
|
||||
<button class="secondary-button" @click="goHome">
|
||||
{{ t('waitlist.goHome') }}
|
||||
</button>
|
||||
<button class="logout-button" @click="handleLogout">
|
||||
{{ t('waitlist.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="decoration">
|
||||
|
|
@ -43,7 +46,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
|
@ -51,56 +54,50 @@ import { ElMessage } from 'element-plus'
|
|||
import { UserController } from './user/index.js'
|
||||
import { useAuthStore } from '@/stores/auth.js'
|
||||
|
||||
export default {
|
||||
name: 'Waitlist',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const userController = new UserController()
|
||||
|
||||
const inviteCode = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 提交邀请码升级
|
||||
const submitInviteCode = async () => {
|
||||
if (!inviteCode.value.trim()) {
|
||||
// ElMessage.warning('请输入邀请码')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// 调用upgrade方法
|
||||
const upgradeRes = await userController.upgrade({ inviteCode: inviteCode.value })
|
||||
if (upgradeRes.code === 0) {
|
||||
// 升级成功,刷新用户信息
|
||||
await authStore.updateUserInfo()
|
||||
ElMessage.success('升级成功')
|
||||
// 返回首页
|
||||
goHome()
|
||||
} else {
|
||||
ElMessage.error(upgradeRes.message || '升级失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('升级失败:', error)
|
||||
ElMessage.error(error.message || '升级失败,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
goHome,
|
||||
inviteCode,
|
||||
submitting,
|
||||
submitInviteCode
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const userController = new UserController()
|
||||
|
||||
const inviteCode = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const goHome = () => {
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authStore.logout(() => {
|
||||
ElMessage.success('退出登录成功')
|
||||
router.replace('/login')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
ElMessage.error('退出登录失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const submitInviteCode = async () => {
|
||||
if (!inviteCode.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const upgradeRes = await userController.upgrade({ inviteCode: inviteCode.value })
|
||||
if (upgradeRes.code === 0) {
|
||||
await authStore.updateUserInfo()
|
||||
ElMessage.success('升级成功')
|
||||
router.replace('/czhome')
|
||||
} else {
|
||||
ElMessage.error(upgradeRes.message || '升级失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('升级失败:', error)
|
||||
ElMessage.error(error.message || '升级失败,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -286,6 +283,34 @@ export default {
|
|||
color: #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) {
|
||||
.waitlist-icon svg {
|
||||
width: 80px;
|
||||
|
|
|
|||
|
|
@ -33,12 +33,7 @@
|
|||
>
|
||||
{{ t('nav.land') }}
|
||||
</a> -->
|
||||
<!-- <router-link
|
||||
to="/points-recharge"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
pricing
|
||||
</router-link> -->
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
|
|
@ -57,6 +52,12 @@
|
|||
>
|
||||
{{ t('nav.about') }}
|
||||
</a>
|
||||
<router-link
|
||||
to="/points-recharge"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
pricing
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- Right Action & Mobile Toggle -->
|
||||
|
|
@ -547,7 +548,7 @@
|
|||
|
||||
<script setup>
|
||||
import MotionCom from './motion.vue'
|
||||
// import spline from './spline.vue';
|
||||
import spline from './spline.vue';
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import Bg from './bg.vue'
|
||||
// import dog from '@/assets/home/dog.webp'
|
||||
|
|
@ -641,8 +642,8 @@ const i18n = {
|
|||
// land: '社区',
|
||||
// pricing: '价格',
|
||||
creator: 'Creator',
|
||||
done: 'D one',
|
||||
about: 'About us'
|
||||
done: 'D1',
|
||||
about: 'About'
|
||||
},
|
||||
hero: {
|
||||
title: '使用 Deotaland 创作',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!-- <template>
|
||||
<template>
|
||||
<ParentSize
|
||||
:parent-size-styles="parentSizeStyles"
|
||||
:debounce-time="50"
|
||||
|
|
@ -159,4 +159,4 @@ onUnmounted(() => {
|
|||
splineApp.value = null;
|
||||
}
|
||||
});
|
||||
</script> -->
|
||||
</script>
|
||||
|
|
@ -12,30 +12,29 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- 隐藏的文件上传输入 -->
|
||||
<!-- <input
|
||||
<input
|
||||
ref="avatarInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="avatar-input"
|
||||
@change="handleAvatarUpload"
|
||||
hidden
|
||||
/> -->
|
||||
/>
|
||||
</div>
|
||||
<!-- @click="toggleEditName" -->
|
||||
<div class="user-details-section">
|
||||
<div class="user-name-row">
|
||||
<div class="editable-name-container">
|
||||
<h1
|
||||
@click="toggleEditName"
|
||||
class="user-name"
|
||||
:class="{ 'editable': true }"
|
||||
>{{ userData.nickname }}</h1>
|
||||
<div v-if="isEditingName" class="name-edit-form">
|
||||
<!-- @blur="saveNickname"
|
||||
@keyup.enter="saveNickname" -->
|
||||
<el-input
|
||||
v-model="editNameValue"
|
||||
size="large"
|
||||
|
||||
@blur="saveNickname"
|
||||
@keyup.enter="saveNickname"
|
||||
ref="nameInput"
|
||||
placeholder="Please enter a nickname"
|
||||
/>
|
||||
|
|
@ -48,7 +47,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 积分信息区域 - 免费会员可见 -->
|
||||
<div class="points-section" v-if="false">
|
||||
<div class="points-section" >
|
||||
<h2>{{ $t('userCenter.points.title') }}</h2>
|
||||
<!-- 积分明细和规则并排容器 -->
|
||||
|
||||
|
|
@ -57,7 +56,15 @@
|
|||
<div class="total-points">
|
||||
<span class="total-icon">🪄</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>
|
||||
|
|
@ -127,6 +134,7 @@
|
|||
<div class="invite-codes-section">
|
||||
<h3>{{ $t('userCenter.invitation.inviteCodes') }}</h3>
|
||||
<div class="invite-cards-container">
|
||||
<!-- v-for="(inviteCode, index) in (userData.inviteCodes.filter(item => item.codeType!='permanent'))" -->
|
||||
<div
|
||||
v-for="(inviteCode, index) in (userData.inviteCodes.filter(item => item.codeType!='permanent'))"
|
||||
:key="index"
|
||||
|
|
@ -267,11 +275,15 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, nextTick, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Plus, Edit } from '@element-plus/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { UserController } from './index.js'
|
||||
import {ModernHome} from '../ModernHome/index.js'
|
||||
const router = useRouter()
|
||||
const modernHome = new ModernHome()
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const userController = new UserController()
|
||||
|
|
@ -328,10 +340,16 @@ const fetchInviteCodes = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const total_score = ref(0);
|
||||
//获取积分详情
|
||||
const pointDetail = async () => {
|
||||
const {data} = await modernHome.getModelLimits();
|
||||
total_score.value = data.total_score
|
||||
}
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchInviteCodes()
|
||||
fetchInviteCodes();
|
||||
pointDetail();
|
||||
})
|
||||
// 获取角色名称
|
||||
const getRoleName = () => {
|
||||
|
|
@ -903,6 +921,33 @@ html.dark .copy-btn:hover:not(:disabled) {
|
|||
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 {
|
||||
margin-top: 24px;
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ export default defineConfig({
|
|||
// 配置代理解决CORS问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.deotaland.ai',
|
||||
// target: 'http://api.deotaland.local',
|
||||
// target: 'https://api.deotaland.ai',
|
||||
target: 'http://api.deotaland.local',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="局部修改"
|
||||
:title="t.dialogTitle"
|
||||
width="90%"
|
||||
:fullscreen="isFullscreen"
|
||||
append-to-body
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="canvas-editor-container">
|
||||
<div class="canvas-toolbar">
|
||||
<div class="toolbar-section">
|
||||
<span class="toolbar-label">画笔颜色:</span>
|
||||
<span class="toolbar-label">{{ t.brushColor }}</span>
|
||||
<div class="color-picker">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="toolbar-section">
|
||||
<span class="toolbar-label">画笔大小:</span>
|
||||
<span class="toolbar-label">{{ t.brushSize }}</span>
|
||||
<el-slider
|
||||
v-model="brushSize"
|
||||
:min="1"
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
@click="toggleEraser"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
橡皮擦
|
||||
{{ t.eraser }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
@click="clearCanvas"
|
||||
>
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
清空
|
||||
{{ t.clear }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
|
|
@ -60,20 +60,20 @@
|
|||
@click="handleSave"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
{{ t.confirm }}
|
||||
</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>
|
||||
<span>{{ t.loadingImage }}</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>
|
||||
<p>{{ t.imageLoadError }}</p>
|
||||
<el-button type="primary" size="small" @click="retryLoadImage">{{ t.retry }}</el-button>
|
||||
<el-button size="small" @click="dialogVisible = false">{{ t.close }}</el-button>
|
||||
</div>
|
||||
<canvas
|
||||
v-show="!imageLoading && !imageLoadError"
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
v-model="editContent"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入要修改的内容"
|
||||
:placeholder="t.editPlaceholder"
|
||||
class="edit-textarea"
|
||||
></el-input>
|
||||
</div>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
</template>
|
||||
|
||||
<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 { ElMessage } from 'element-plus'
|
||||
|
||||
|
|
@ -113,11 +113,52 @@ const props = defineProps({
|
|||
imageUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'cn'
|
||||
}
|
||||
})
|
||||
|
||||
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 isFullscreen = ref(false)
|
||||
const canvas = ref(null)
|
||||
|
|
@ -194,17 +235,17 @@ const initCanvas = async () => {
|
|||
console.error('Failed to load image:', props.imageUrl, error)
|
||||
imageLoading.value = false
|
||||
imageLoadError.value = true
|
||||
ElMessage.error('图片加载失败,请重试')
|
||||
ElMessage.error(t.value.imageLoadFailed)
|
||||
}
|
||||
}
|
||||
|
||||
const retryLoadImage = () => {
|
||||
if (loadRetryCount.value < maxRetryCount) {
|
||||
loadRetryCount.value++
|
||||
ElMessage.info(`正在重试加载图片 (${loadRetryCount.value}/${maxRetryCount})`)
|
||||
ElMessage.info(`${t.value.retryingImage} (${loadRetryCount.value}/${maxRetryCount})`)
|
||||
initCanvas()
|
||||
} else {
|
||||
ElMessage.warning('已达到最大重试次数,请稍后再试')
|
||||
ElMessage.warning(t.value.maxRetryReached)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import 'element-plus/dist/index.css'
|
||||
// UI组件库入口文件
|
||||
import LoadingCom from './components/LoadingCom/index.vue'
|
||||
import CanvasEditor from './components/CanvasEditor/CanvasEditor.vue'
|
||||
|
||||
import './style.css'
|
||||
|
||||
// 创建带有Dt前缀的组件
|
||||
|
|
@ -10,15 +13,24 @@ const DtLoadingCom = {
|
|||
app.component('DtLoadingCom', DtLoadingCom)
|
||||
}
|
||||
}
|
||||
const DtCanvasEditor = {
|
||||
...CanvasEditor,
|
||||
name: 'DtCanvasEditor',
|
||||
install(app) {
|
||||
app.component('DtCanvasEditor', DtCanvasEditor)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件列表
|
||||
const components = [
|
||||
DtLoadingCom
|
||||
DtLoadingCom,
|
||||
DtCanvasEditor
|
||||
]
|
||||
|
||||
// 导出组件
|
||||
export {
|
||||
DtLoadingCom
|
||||
DtLoadingCom,
|
||||
DtCanvasEditor
|
||||
}
|
||||
|
||||
// 批量注册组件的函数
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import order from './order.js';
|
|||
import user from './user.js';
|
||||
import logistics from './logistics.js';
|
||||
import agent from './agent.js';
|
||||
import rechargeconfig from './rechargeconfig.js';
|
||||
|
||||
export default {
|
||||
...meshy,
|
||||
|
|
@ -18,4 +19,5 @@ export default {
|
|||
...user,
|
||||
...logistics,
|
||||
...agent,
|
||||
...rechargeconfig,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { request as requestUtils } from '../utils/request.js'
|
||||
import * as clientApi from '../api/frontend/index.js'
|
||||
import * as adminApi from '../api/FrontendDesigner/index.js'
|
||||
import * as clientApi from '../api/frontend/index.js'
|
||||
import * as adminApi from '../api/FrontendDesigner/index.js'
|
||||
let urlRule = 'https://api.deotaland.aiIMGURL'
|
||||
// 获取环境变量中的
|
||||
const getPorjectType = () => {
|
||||
// 浏览器环境
|
||||
if (typeof window !== 'undefined') {
|
||||
// Vite 环境变量
|
||||
return import.meta.env.VITE_PROJECTTYPE;
|
||||
return import.meta.env.VITE_PROJECTTYPE;
|
||||
}
|
||||
// Node.js 环境
|
||||
if (typeof process !== 'undefined') {
|
||||
return process.env.VITE_PROJECTTYPE ;
|
||||
return process.env.VITE_PROJECTTYPE;
|
||||
}
|
||||
};
|
||||
export class FileServer {
|
||||
|
|
@ -21,23 +21,23 @@ export class FileServer {
|
|||
static fileCacheMap = new Map();
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
isModelFile(fileName) {
|
||||
if (!fileName || typeof fileName !== 'string') return false;
|
||||
const modelExtensions = ['.glb', '.gltf', '.obj', '.fbx', '.dae', '.3ds', '.blend', '.max', '.ma', '.mb', '.x3d', '.usdz', '.ply', '.stl'];
|
||||
const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
||||
return modelExtensions.includes(ext);
|
||||
}
|
||||
|
||||
|
||||
//文件拼接
|
||||
concatUrl(url) {
|
||||
return urlRule.replace('IMGURL',url)
|
||||
return urlRule.replace('IMGURL', url)
|
||||
}
|
||||
|
||||
|
||||
//生成唯一的缓存键
|
||||
generateUniqueCacheKey(input) {
|
||||
let content = '';
|
||||
|
||||
|
||||
if (typeof input === 'string') {
|
||||
if (input.startsWith('data:')) {
|
||||
// 对于base64字符串,使用文件类型、原始长度和内容哈希
|
||||
|
|
@ -58,22 +58,22 @@ export class FileServer {
|
|||
// 对于File对象,使用文件名、大小、类型和最后修改时间
|
||||
content = `file_${input.name}_${input.size}_${input.type}_${input.lastModified}`;
|
||||
}
|
||||
|
||||
|
||||
// 使用同步哈希算法生成16位缓存键
|
||||
return this.generateSimpleHash(content).substring(0, 16);
|
||||
}
|
||||
|
||||
|
||||
//简单的字符串哈希函数(同步)
|
||||
generateSimpleHash(content) {
|
||||
// 使用MurmurHash或简单的多项式哈希
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = (prime * hash + char) | 0; // 使用位运算确保32位整数
|
||||
}
|
||||
|
||||
|
||||
// 转换为16进制字符串
|
||||
return (hash >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ export class FileServer {
|
|||
async compressFile(fileInput, quality = 0.5, maxWidth = null, maxHeight = null) {
|
||||
try {
|
||||
let base64String;
|
||||
|
||||
|
||||
// 根据输入类型获取base64字符串
|
||||
if (fileInput instanceof File) {
|
||||
// 直接是File对象
|
||||
|
|
@ -112,16 +112,16 @@ export class FileServer {
|
|||
} else {
|
||||
throw new Error('不支持的文件输入类型');
|
||||
}
|
||||
|
||||
|
||||
// 压缩图片
|
||||
return await this.compressImageFromBase64(base64String, quality, maxWidth, maxHeight);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('文件压缩失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从base64字符串压缩图片
|
||||
* @param {string} base64String - base64图片字符串
|
||||
|
|
@ -137,26 +137,26 @@ export class FileServer {
|
|||
try {
|
||||
// 计算新的尺寸
|
||||
let { width, height } = img;
|
||||
|
||||
|
||||
if (maxWidth && width > maxWidth) {
|
||||
height = (height * maxWidth) / width;
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
|
||||
if (maxHeight && height > maxHeight) {
|
||||
width = (width * maxHeight) / height;
|
||||
height = maxHeight;
|
||||
}
|
||||
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
|
||||
// 使用高质量的图像缩放
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const compressed = canvas.toDataURL('image/jpeg', quality);
|
||||
resolve(compressed);
|
||||
|
|
@ -170,7 +170,7 @@ export class FileServer {
|
|||
img.src = base64String;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量压缩文件
|
||||
* @param {Array} fileInputs - 文件输入数组
|
||||
|
|
@ -180,7 +180,7 @@ export class FileServer {
|
|||
async compressFilesBatch(fileInputs, options = {}) {
|
||||
const { quality = 0.5, maxWidth = null, maxHeight = null } = options;
|
||||
const results = [];
|
||||
|
||||
|
||||
for (const fileInput of fileInputs) {
|
||||
try {
|
||||
const compressed = await this.compressFile(fileInput, quality, maxWidth, maxHeight);
|
||||
|
|
@ -197,7 +197,7 @@ export class FileServer {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return results;
|
||||
}
|
||||
/**
|
||||
|
|
@ -212,29 +212,29 @@ export class FileServer {
|
|||
const extension = this.getExtensionFromMimeType(mimeType);
|
||||
return `uploaded_file_${Date.now()}.${extension}`;
|
||||
}
|
||||
|
||||
|
||||
// 如果是普通URL,提取文件名并清理
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const fileName = pathname.split('/').pop() || 'uploaded_file';
|
||||
|
||||
|
||||
// 移除查询参数和哈希
|
||||
const cleanFileName = fileName.split('?')[0].split('#')[0];
|
||||
|
||||
|
||||
// 如果没有扩展名,尝试从URL路径推断
|
||||
if (!cleanFileName.includes('.')) {
|
||||
const extension = this.inferExtensionFromPath(pathname);
|
||||
return cleanFileName + (extension ? `.${extension}` : '');
|
||||
}
|
||||
|
||||
|
||||
return cleanFileName || `uploaded_file_${Date.now()}`;
|
||||
} catch (error) {
|
||||
// URL解析失败,使用默认文件名
|
||||
return `uploaded_file_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从MIME类型获取文件扩展名
|
||||
*/
|
||||
|
|
@ -253,7 +253,7 @@ export class FileServer {
|
|||
};
|
||||
return mimeMap[mimeType] || 'bin';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从URL路径推断文件扩展名
|
||||
*/
|
||||
|
|
@ -276,17 +276,22 @@ export class FileServer {
|
|||
} else if (FileServer.fileCacheMap.get(cacheKey) != 'loading') {
|
||||
clearInterval(interval);
|
||||
resolve(FileServer.fileCacheMap.get(cacheKey));
|
||||
}
|
||||
else if (pollCount >= maxPollCount) {
|
||||
}
|
||||
else if (pollCount >= maxPollCount) {
|
||||
clearInterval(interval);
|
||||
resolve('loading');
|
||||
}
|
||||
}, 1000); // 每1秒检查一次
|
||||
});
|
||||
}
|
||||
//上传文件
|
||||
async uploadFile(url,config={}) {
|
||||
//上传文件
|
||||
async uploadFile(url, config = {}) {
|
||||
this.closeLoading = window.setElLoading();
|
||||
// 如果是网络路径直接返回
|
||||
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
this.closeLoading.close();
|
||||
return Promise.resolve(url);
|
||||
}
|
||||
let originalFileName = null;
|
||||
// 判断参数是否为File对象,保存原始文件名
|
||||
if (url instanceof File) {
|
||||
|
|
@ -294,105 +299,112 @@ export class FileServer {
|
|||
try {
|
||||
url = await this.fileToBase64FromFile(url);
|
||||
} catch (error) {
|
||||
|
||||
console.error('File对象转base64失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 如果是网络路径直接返回
|
||||
if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
return Promise.resolve(url);
|
||||
}
|
||||
const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key
|
||||
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(FileServer.fileCacheMap.get(cacheKey));
|
||||
return;
|
||||
}
|
||||
let loadUrl = null;
|
||||
if(FileServer.fileCacheMap.get(cacheKey)=='loading'){
|
||||
loadUrl = await this.pollFileCacheMap(cacheKey);
|
||||
}
|
||||
if(loadUrl!='loading'&&loadUrl!=null){
|
||||
// resolve(this.concatUrl(loadUrl));
|
||||
resolve(loadUrl);
|
||||
return
|
||||
}
|
||||
FileServer.fileCacheMap.set(cacheKey,'loading');
|
||||
let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
|
||||
const fileName = originalFileName || this.extractFileName(url);
|
||||
const isModelFile = this.isModelFile(fileName);
|
||||
// 模型文件不进行压缩
|
||||
if (!isModelFile) {
|
||||
}
|
||||
let loadUrl = null;
|
||||
if (FileServer.fileCacheMap.get(cacheKey) == 'loading') {
|
||||
loadUrl = await this.pollFileCacheMap(cacheKey);
|
||||
}
|
||||
if (loadUrl != 'loading' && loadUrl != null) {
|
||||
// resolve(this.concatUrl(loadUrl));
|
||||
resolve(loadUrl);
|
||||
return
|
||||
}
|
||||
FileServer.fileCacheMap.set(cacheKey, 'loading');
|
||||
let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
|
||||
const fileName = originalFileName || this.extractFileName(url);
|
||||
const isModelFile = this.isModelFile(fileName);
|
||||
// 模型文件不进行压缩
|
||||
if (!isModelFile) {
|
||||
try {
|
||||
console.log(`文件大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB,开始压缩...`);
|
||||
// 将Blob转换为File对象以便压缩
|
||||
const fileObject = new File([file], fileName, { type: file.type });
|
||||
|
||||
const compressedFile = await this.compressFile(fileObject, 0.6); // 使用0.6质量压缩
|
||||
if (compressedFile && compressedFile.length < file.size) {
|
||||
// 将压缩后的base64转换回Blob
|
||||
const response = await fetch(compressedFile);
|
||||
const compressedBlob = await response.blob();
|
||||
file = compressedBlob;
|
||||
console.log(`文件压缩成功,压缩后大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('文件压缩失败,使用原文件上传:', error.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log(`文件大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB,开始压缩...`);
|
||||
// 将Blob转换为File对象以便压缩
|
||||
const fileObject = new File([file], fileName, { type: file.type });
|
||||
|
||||
const compressedFile = await this.compressFile(fileObject, 0.6); // 使用0.6质量压缩
|
||||
if (compressedFile && compressedFile.length < file.size) {
|
||||
// 将压缩后的base64转换回Blob
|
||||
const response = await fetch(compressedFile);
|
||||
const compressedBlob = await response.blob();
|
||||
file = compressedBlob;
|
||||
console.log(`文件压缩成功,压缩后大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
||||
// const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData);
|
||||
let params = {
|
||||
filename: fileName,
|
||||
file_type: file.type.split('/')[0],
|
||||
prefix: isModelFile ? 'uploads' : 'images',
|
||||
...config
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('文件压缩失败,使用原文件上传:', error.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData);
|
||||
let params = {
|
||||
filename:fileName,
|
||||
file_type:file.type.split('/')[0],
|
||||
prefix:isModelFile ? 'uploads' : 'images',
|
||||
...config
|
||||
}
|
||||
const requestUrl = this.RULE=='admin'?adminApi.default.adminUPLOADS3:clientApi.default.UPLOADS3;
|
||||
// const requestUrl = clientApi.default.UPLOADS3;
|
||||
const response = await requestUtils.common(requestUrl, params);
|
||||
if(response.code==0){
|
||||
let data = response.data;
|
||||
let {url,fields,file_key,file_url } = data;
|
||||
const formData = new FormData();
|
||||
for (const key in fields) {
|
||||
formData.append(key, fields[key]);
|
||||
}
|
||||
formData.append('file', file, fileName);
|
||||
const requestUrl = this.RULE == 'admin' ? adminApi.default.adminUPLOADS3 : clientApi.default.UPLOADS3;
|
||||
// const requestUrl = clientApi.default.UPLOADS3;
|
||||
const response = await requestUtils.common(requestUrl, params);
|
||||
if (response.code == 0) {
|
||||
let data = response.data;
|
||||
let { url, fields, file_key, file_url } = data;
|
||||
const formData = new FormData();
|
||||
for (const key in fields) {
|
||||
formData.append(key, fields[key]);
|
||||
}
|
||||
formData.append('file', file, fileName);
|
||||
// 上传到S3
|
||||
const uploadResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
mode: 'cors' // 明确指定CORS模式
|
||||
const uploadResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
mode: 'cors' // 明确指定CORS模式
|
||||
});
|
||||
// 注意:S3可能返回204或303状态码表示成功
|
||||
if (uploadResponse.status === 204 || uploadResponse.status === 303 || uploadResponse.ok) {
|
||||
if(file_url){
|
||||
// 截取后八位作为缓存 key
|
||||
FileServer.fileCacheMap.set(cacheKey, file_url);
|
||||
// resolve(urlRule.replace('IMGURL',file_url));
|
||||
resolve(file_url);
|
||||
this.closeLoading.close();
|
||||
if (uploadResponse.status === 204 || uploadResponse.status === 303 || uploadResponse.ok) {
|
||||
if (file_url) {
|
||||
// 截取后八位作为缓存 key
|
||||
FileServer.fileCacheMap.set(cacheKey, file_url);
|
||||
// resolve(urlRule.replace('IMGURL',file_url));
|
||||
resolve(file_url);
|
||||
this.closeLoading.close();
|
||||
}
|
||||
} else {
|
||||
this.closeLoading.close();
|
||||
reject(errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
this.closeLoading.close();
|
||||
reject(errorMsg);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.closeLoading.close();
|
||||
//删除对应键值
|
||||
FileServer.fileCacheMap.delete(cacheKey);
|
||||
reject(error);
|
||||
console.error('上传文件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
this.closeLoading.close();
|
||||
//删除对应键值
|
||||
FileServer.fileCacheMap.delete(cacheKey);
|
||||
reject(error);
|
||||
console.error('上传文件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
}
|
||||
// 从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对象
|
||||
fileToBlob(fileOrBase64) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// import { loadStripe } from '@stripe/stripe-js';
|
||||
import { request as requestUtils } from '../utils/request.js'
|
||||
import * as clientApi from '../api/frontend/index.js'
|
||||
import * as clientApi from '../api/frontend/index.js'
|
||||
//获取Stripe公钥
|
||||
export function getStripePublishableKey() {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -21,10 +21,10 @@ export class PayServer {
|
|||
static stripe = null// Stripe实例
|
||||
static isInitializing = false // 防止重复初始化
|
||||
static initPromise = null // 初始化Promise
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
//初始化
|
||||
async init() {
|
||||
return
|
||||
|
|
@ -32,12 +32,12 @@ export class PayServer {
|
|||
if (PayServer.stripe) {
|
||||
return PayServer.stripe;
|
||||
}
|
||||
|
||||
|
||||
// 如果正在初始化,等待初始化完成
|
||||
if (PayServer.isInitializing && PayServer.initPromise) {
|
||||
return PayServer.initPromise;
|
||||
}
|
||||
|
||||
|
||||
// 开始初始化
|
||||
PayServer.isInitializing = true;
|
||||
PayServer.initPromise = new Promise(async (resolve, reject) => {
|
||||
|
|
@ -46,7 +46,7 @@ export class PayServer {
|
|||
if (!stripeKey) {
|
||||
throw new Error('Stripe publishable key not found');
|
||||
}
|
||||
|
||||
|
||||
const stripe = await loadStripe(stripeKey);
|
||||
PayServer.stripe = stripe;
|
||||
PayServer.isInitializing = false;
|
||||
|
|
@ -56,7 +56,7 @@ export class PayServer {
|
|||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return PayServer.initPromise;
|
||||
}
|
||||
/**
|
||||
|
|
@ -145,28 +145,50 @@ export class PayServer {
|
|||
})
|
||||
}
|
||||
//创建订单并且支付
|
||||
async createPayorOrder(orderInfo) {
|
||||
// let payReducerUrl = 'https://www.deotaland.ai/#/order-management'
|
||||
let payReducerUrl = `${window.location.origin}/#/order-management`
|
||||
async createPayorOrder(orderInfo, type = 1) {//1 ip订单 2充值包订单
|
||||
let payReducerUrl = ''
|
||||
// await this.init();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let pamras = {
|
||||
product_id: orderInfo.product_id,
|
||||
"methods": [
|
||||
"card"
|
||||
],
|
||||
"success_url":payReducerUrl,
|
||||
"cancel_url":payReducerUrl,
|
||||
"quantity":orderInfo.quantity,
|
||||
"project_id": orderInfo.project_id,
|
||||
"project_details": orderInfo.project_details,
|
||||
"order_info": orderInfo.order_info
|
||||
let pamras = {}
|
||||
switch (type) {
|
||||
case 1:
|
||||
payReducerUrl = `${window.location.origin}/#/order-management`
|
||||
pamras = {
|
||||
product_id: orderInfo.product_id,
|
||||
"methods": [
|
||||
"card"
|
||||
],
|
||||
"success_url": payReducerUrl,
|
||||
"cancel_url": payReducerUrl,
|
||||
"quantity": orderInfo.quantity,
|
||||
"project_id": orderInfo.project_id,
|
||||
"project_details": orderInfo.project_details,
|
||||
"order_info": orderInfo.order_info
|
||||
}
|
||||
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 res = await requestUtils.common(clientApi.default.createPayorOrder, pamras);
|
||||
let requestUrl = {
|
||||
1: clientApi.default.createPayorOrder,
|
||||
2: clientApi.default.CREATE_RECHARGE_ORDER,
|
||||
}[type]
|
||||
let res = await requestUtils.common(requestUrl, pamras);
|
||||
if (res.code == 0) {
|
||||
let data = res.data
|
||||
// return
|
||||
window.location.href = data.url
|
||||
window.location.href = (data.url)||data.payment_url
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(res.msg)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,9 @@ service.interceptors.request.use(
|
|||
return config;
|
||||
},
|
||||
error => {
|
||||
closeMethods?.close()
|
||||
// 请求错误处理
|
||||
console.error('请求错误:', error);
|
||||
console.log('请求错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -84,6 +85,7 @@ service.interceptors.response.use(
|
|||
return res;
|
||||
},
|
||||
error => {
|
||||
closeMethods?.close()
|
||||
// 响应错误处理
|
||||
let message = '网络请求失败';
|
||||
if (error.response) {
|
||||
|
|
@ -108,7 +110,6 @@ service.interceptors.response.use(
|
|||
// 请求已发送但没有收到响应
|
||||
message = '网络连接失败,请检查网络';
|
||||
}
|
||||
console.error(message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ importers:
|
|||
'@google/genai':
|
||||
specifier: ^1.27.0
|
||||
version: 1.27.0
|
||||
'@splinetool/runtime':
|
||||
specifier: ^1.12.29
|
||||
version: 1.12.29
|
||||
'@twind/core':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
|
|
@ -1088,6 +1091,13 @@ packages:
|
|||
dev: 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:
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
dev: false
|
||||
|
|
@ -3084,6 +3094,11 @@ packages:
|
|||
boolbase: 1.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
|
|
@ -3408,6 +3423,10 @@ packages:
|
|||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
dev: true
|
||||
|
||||
/semver-compare@1.0.0:
|
||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||
dev: false
|
||||
|
||||
/semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
|
|||
Loading…
Reference in New Issue