deotalandAi/apps/frontend/src/views/Project/CreateProject.vue

1588 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="creative-zone" @contextmenu.prevent>
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
</div>
<!-- 导入的侧边栏组件 -->
<div class="sidebar-container">
<iPandCardLeft
:Info="projectInfo.details"
@generate-requested="handleGenerateRequested"
@model-generated="handleModelGenerated"
@import-character="openImportModal"
/>
</div>
<!-- 主内容区域 - 可拖动场景 -->
<main
class="main-content scene-container"
:style="{ cursor: contentCursor }"
@mousedown="startSceneDrag"
@mousemove="dragScene"
@mouseup="stopSceneDrag"
@mouseleave="stopSceneDrag"
@touchstart="startSceneDrag"
@touchmove="dragScene"
@touchend="stopSceneDrag"
@wheel="handleWheel"
@selectstart.prevent
>
<!-- 场景内容容器 -->
<div
class="scene-content"
:style="sceneContainerStyle"
>
<!-- IP图片展示组件 - 每个组件独立可拖动 -->
<!-- @touchstart="startElementDrag($event, index)" -->
<div
v-for="(card, index) in cards"
:key="card.id"
class="draggable-element"
:style="getElementStyle(index)"
@mousedown="startElementDrag($event, index)"
@touchstart="startElementDrag($event, index)"
@mousemove="dragElement"
@touchmove="dragElement"
@mouseup="stopElementDrag"
@mouseleave="sendToBack(index)"
@touchend="stopElementDrag"
@mouseenter="bringToFront(index)"
>
<!-- @mouseleave="stopElementDrag" -->
<!-- 删除按钮 -->
<button
v-if="(card.imageUrl&&card.type==='image')||(card.type==='model'&&card.modelUrl)"
class="delete-button"
@click.stop="handleDeleteCard(index)"
@mousedown.stop
@touchstart.stop
@touchend.stop
title="删除卡片"
>
<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>
<!-- 根据卡片类型显示不同组件 -->
<IPCard
: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)"
:projectId="projectId"
@create-prompt-card="(data)=>handleCreatePromptCard(index,data)"
@generate-model-requested="(data)=>handleGenerateModelRequested(index,data)"
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
@create-remaining-image-data="handleCreateRemainingImageData"
v-if="card.type === 'image'&&combinedPromptJson.animal"
:cardData="card"
/>
<ModelCard
v-else-if="card.type === 'model'"
:cardData="card"
:clickDisabled="isElementDragging"
@customize-to-home="handleCustomizeToHome(index)"
@delete="handleDeleteCard(index)"
:projectId="projectId"
@save-project="(item)=>{handleSaveProject(index,item,'model')}"
@clickModel="handleModelClick"
@refineModel="handleRefineModel"
/>
</div>
</div>
</main>
<!-- 模型展示弹窗 -->
<ModelModal
:show="showModelModal"
:modelData="selectedModel"
@close="closeModal()"
/>
<!-- 角色导入弹窗iframe -->
<CharacterImportModal
:show="showImportModal"
:url="importUrl"
@close="closeImportModal"
/>
<!-- 引导弹窗 -->
<GuideModal
:show="showGuideModal"
@close="closeGuideModal"
@complete="completeGuide"
/>
<!-- 图片预览弹窗 -->
<ImagePreviewModal
:visible="showImagePreview"
:images="previewImages"
:initialIndex="currentImageIndex"
@close="showImagePreview = false"
/>
<CanvasEditor
v-model:visible="canvasEditorVisible"
:image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave"
/>
<!-- 测试侧边栏动画的按钮 -->
<!-- <button
class="test-animation-btn"
@click="triggerSidebarAnimation"
style="position: fixed; bottom: 20px; right: 20px; z-index: 1000;"
>
测试动画
</button> -->
<!-- 定制到家弹窗 -->
<OrderProcessModal
:show="showOrderProcessModal"
:modelData="CustomizeModalData"
@close="showOrderProcessModal=false"
@acknowledge="handleBuyFromCustomize" />
<PurchaseModal
:show="showPurchaseModal"
:modelData="CustomizeModalData"
@close="showPurchaseModal=false" />
</div>
</template>
<script setup>
// 创意空间组件逻辑
import { ref, computed, onMounted, onUnmounted,watch } from 'vue';
import iPandCardLeft from '../../components/iPandCardLeft/index.vue';
import IPCard from '../../components/IPCard/index.vue';
import ModelCard from '../../components/modelCard/index.vue';
import ModelModal from '../../components/ModelModal/index.vue';
import CharacterImportModal from '../../components/CharacterImportModal/index.vue';
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
import GuideModal from '../../components/GuideModal/index.vue';
import ImagePreviewModal from '../../components/ImagePreviewModal/index.vue';
import {useRoute,useRouter} from 'vue-router';
import {MeshyServer,GiminiServer,FileServer} from '@deotaland/utils';
import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
import PurchaseModal from '../../components/PurchaseModal/index.vue';
import { ElMessage } from 'element-plus';
import {Project} from './index';
import {ModernHome} from '../ModernHome/index.js'
const fileServer = new FileServer();
const modernHome = new ModernHome();
const Limits = ref({
generateCount: 0,
modelCount: 0,
})
const router = useRouter();
const PluginProject = new Project();
// 弹窗相关状态
const showModelModal = ref(false);
const selectedModel = ref(null);
const showImportModal = ref(false);
const importUrl = ref('https://xiaozhi.me/console/agents');
const showGuideModal = ref(false);
const canvasEditorVisible = ref(false);//画布编辑弹窗是否可见
const canvasEditorImageUrl = ref('');//画布编辑弹窗图片url
// 图片预览弹窗相关状态
const showImagePreview = ref(false);
const previewImages = ref([]);
const currentImageIndex = ref(0);
// 事件监听器清理函数存储
const cleanupFunctions = ref({});
const projectId = ref(null);
const series = ref(null);//项目系列
//项目数据
const projectInfo = ref({});
const total_score = ref(0);
//获取生图次数和模型次数
const getGenerateCount = async ()=>{
const {data} = await modernHome.getModelLimits();
total_score.value = data.total_score
// Limits.value.generateCount = data[0].model_count;
// Limits.value.modelCount = data[1].model_count;
}
const handlePartialEdit = (imageUrl, index) => {
canvasEditorImageUrl.value = imageUrl;
canvasEditorVisible.value = true;
}
// 处理画布保存事件
const handleCanvasSave = (editedImageUrl,editContent) => {
fileServer.uploadFile(editedImageUrl).then((url) => {
const newCard = createSmartCard({
diyPromptImg:url,
diyPromptText:editContent,
status:'loading',
type:'image',
// inspirationImage:cardData.inspirationImage||'',
});
// console.log(newCard,'newCardnewCard');
cards.value.push(newCard);
})
}
// 多个可拖动元素的数据
const cards = ref([
]);
const getMaxZIndexNum = ref(0);
//获取最大z-index+1
const getMaxZIndex = (type)=>{
if(type==1&&getMaxZIndexNum.value!=0){
return getMaxZIndexNum.value;
}
if(getMaxZIndexNum.value!=0){
getMaxZIndexNum.value ++;
return getMaxZIndexNum.value;
}
getMaxZIndexNum.value = cards.value.reduce((max, card) => Math.max(max, card.zIndex || 0), 0)+1;
return getMaxZIndexNum.value;
}
const combinedPromptJson = ref({});
//获取动态提示词
const getCombinedPrompt = async ()=>{
try {
const data = await PluginProject.getCombinedPrompt(series.value);
combinedPromptJson.value = data;
console.log(combinedPromptJson.value);
} catch (error) {
console.error(error);
}
}
watch(()=>[projectInfo.value,cards.value], () => {
let newProjectInfo = {...projectInfo.value};
newProjectInfo.details.node_card = cards.value;
updateProjectInfo(newProjectInfo)
}, {deep: true});
const handlePreviewImage = (imageUrl) => {
console.log('点击了图片', imageUrl);
// 如果是字符串,转换为数组
if (typeof imageUrl === 'string') {
previewImages.value = [imageUrl];
} else if (Array.isArray(imageUrl)) {
previewImages.value = imageUrl;
} else {
console.error('无效的图片URL:', imageUrl);
return;
}
currentImageIndex.value = 0;
showImagePreview.value = true;
}
//保存卡片项目
const handleSaveProject = (index,item,type='image')=>{
let cardItem = cards.value[index];
switch(type){
case 'image':
cardItem.imageUrl = item.imageUrl;
cardItem.taskId = item.taskId;
cardItem.taskQueue = item.taskQueue;
break;
case 'model':
cardItem.modelUrl = item.modelUrl;
cardItem.taskId = item.taskId;
cardItem.resultTask = item.resultTask;
break;
}
cardItem.status = item.status;
getGenerateCount();
console.log(cards.value,'保存项目');
}
const createProject = async ()=>{
const {id} = await PluginProject.createProject();
// 创建新项目后,将当前路由跳转到 project/项目id/系列
await router.replace(`/project/${id}/${series.value}`);
projectId.value = id;
getProjectInfo(id);
// projectId.value = 8;
// getProjectInfo(8)
}
//获取项目信息
const getProjectInfo = async (id)=>{
const data = await PluginProject.getProject(id);
projectInfo.value = {...data};
PluginProject.addProjectDuration(data.duration_seconds);
// 为没有id的卡片添加唯一id
cards.value = [...projectInfo.value.details.node_card].map(card => ({
...card,
id: card.id || Date.now() + Math.random().toString(36).substr(2, 9)
}));
projectInfo.value.tags = [series.value];
}
//更新项目信息
const updateProjectInfo = async (newProjectInfo)=>{
newProjectInfo.duration_seconds = PluginProject.duration_seconds;
PluginProject.updateProject(projectId.value,newProjectInfo);
}
// 处理模型卡片点击事件
const handleModelClick = (modelData) => {
// 如果正在拖动元素,则不触发弹窗
if (isElementDragging.value) {
return;
}
console.log('点击了模型卡片', modelData);
// 接收并使用新的modelData对象
selectedModel.value = modelData;
showModelModal.value = true;
};
// 关闭弹窗
const closeModal = () => {
showModelModal.value = false;
selectedModel.value = null;
};
// 打开角色导入弹窗
const openImportModal = () => {
showImportModal.value = true;
};
// 关闭角色导入弹窗
const closeImportModal = () => {
showImportModal.value = false;
};
// 关闭引导弹窗
const closeGuideModal = () => {
showGuideModal.value = false;
};
// 完成引导
const completeGuide = () => {
showGuideModal.value = false;
};
// ==================== 定位计算器 ====================
/**
* 卡片定位计算系统
* 负责计算新卡片相对于最高层级卡片的确切位置
*/
class CardPositionCalculator {
constructor() {
this.ANIMATION_DURATION = 400; // 动画持续时间(ms)
this.OFFSET_MARGIN = 30; // 卡片间距
this.SCREEN_PADDING = 50; // 屏幕边缘最小间距
}
/**
* 计算新卡片相对于最高层级卡片右侧的位置
* @param {Object} highestCard - 当前最高层级的卡片
* @param {number} scale - 当前缩放比例
* @returns {Object} 位置信息 { x, y }
*/
calculateRightSidePosition(highestCard, scale) {
if (!highestCard) {
return { x: -100, y:-100 };
}
// 获取最高层级卡片的当前偏移位置
const currentX = this.parseOffset(highestCard.offsetX);
const currentY = this.parseOffset(highestCard.offsetY);
// 获取卡片宽度默认250px
const cardWidth = this.parseCardWidth(highestCard.cardWidth);
// 计算右侧位置当前X + 卡片宽度 + 间距 + 屏幕边距
const rightOffsetX = currentX + cardWidth + this.OFFSET_MARGIN + this.SCREEN_PADDING;
// 获取场景容器的尺寸(假设场景容器充满整个视口)
const containerWidth = window.innerWidth;
const maxSafeX = containerWidth - cardWidth - this.SCREEN_PADDING;
// 如果计算出的位置超出边界,则换行到下一行
const finalX = rightOffsetX > maxSafeX ?
this.SCREEN_PADDING : // 如果超界则从左侧开始
rightOffsetX;
// Y坐标如果换行则向下移动一行的高度
const finalY = rightOffsetX > maxSafeX ?
currentY + cardWidth + this.OFFSET_MARGIN : // 换行到下一行
currentY; // 保持在同一行
return { x: finalX-90, y: finalY };
}
/**
* 解析偏移值字符串为数值
* @param {string} offset - 偏移值字符串
* @returns {number} 数值
*/
parseOffset(offset) {
if (typeof offset === 'number') return offset;
if (typeof offset === 'string') {
const num = parseFloat(offset.replace('px', ''));
return isNaN(num) ? 0 : num;
}
return 0;
}
/**
* 解析卡片宽度
* @param {string|number} cardWidth - 卡片宽度
* @returns {number} 数值
*/
parseCardWidth(cardWidth) {
if (typeof cardWidth === 'number') return cardWidth;
if (typeof cardWidth === 'string') {
const num = parseFloat(cardWidth.replace('px', ''));
return isNaN(num) ? 250 : num;
}
return 250;
}
/**
* 生成动画配置
* @returns {Object} CSS动画属性
*/
getAnimationConfig() {
return {
transition: `transform ${this.ANIMATION_DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
willChange: 'transform' // 性能优化告知浏览器该元素将改变transform
};
}
}
const positionCalculator = new CardPositionCalculator();
// 删除卡片方法
const handleDeleteCard = (index) => {
cards.value.splice(index, 1);
};
/**
* 创建带智能定位和层级管理的新卡片
* @param {Object} cardConfig - 卡片配置对象
* @returns {Object} 配置完整的新卡片
*/
const createSmartCard = (cardConfig,index=null) => {
// 首先生成唯一ID
const uniqueId = Date.now() + Math.random().toString(36).substr(2, 9);
// 获取当前最高层级的卡片
// const highestCard;
// 获取当前最高层级的卡片(按 zIndex 最大值)
let highestCard = cards.value.reduce((prev, current) =>
prev.zIndex > current.zIndex ? prev : current, {});
// 计算位置在最高层级卡片右侧
const position = highestCard ?
positionCalculator.calculateRightSidePosition(highestCard, scale.value) :
{ x: -100, y: -100 };
// 为新卡片分配最高层级使用新生成的ID
const newCard = {
...cardConfig,
id: uniqueId,
offsetX: `${position.x}px`,
offsetY: `${position.y}px`,
project_id: projectId.value,
// 分配最高层级
zIndex: getMaxZIndex(),
};
return newCard;
};
const handleRefineModel = (taskId)=>{
console.log('点击了模型卡片', taskId);
// 使用统一的智能定位和层级管理系统创建模型卡片
const newModelCard = createSmartCard({
cardWidth: "250", // 默认宽度
type: 'model', // 标记为模型类型
modelUrl: null, // 初始为空将由modelCard组件自己生成
refineModel: true,//标记为模型细化
refineModelTaskId: taskId
});
// 将新模型卡片添加到cards数组中
cards.value.push(newModelCard);
}
// 处理精确定位白膜卡片生成
const handleGenerateSmoothWhiteModel = (index,imageUrl) => {
console.log(imageUrl, 'imageUrlimageUrlimageUrl');
// 使用统一的智能定位和层级管理系统创建白膜卡片
const newCard = createSmartCard({
imageUrl: imageUrl, // 传入基础图片作为初始图片
type: 'image', // 标记为图片类型
generateSmoothWhiteModelStatus: true
},index);
// 添加到卡片数组
cards.value.push(newCard);
console.log('已创建精确定位白膜卡片:', newCard);
}
// 处理四视图生成
const handleCreateFourViewCard = (index,params) => {
const { baseImage } = params;
// 创建新的四视图卡片对象
const newCard = createSmartCard({
imageUrl: baseImage, // 传入基础图片作为初始图片
style: 'realistic', // 四视图通常使用写实风格
type: 'image', // 标记为图片类型
inspirationImage: null,
module: null,
sketch: null,
isGenerating: true, // 标记正在生成中
generateFourView: true // 标记为四视图生成模式
},index);
// 添加到卡片数组
cards.value.push(newCard);
}
// 处理提示词卡片生成
const handleCreatePromptCard = (index,params) => {
const { img, diyPromptText,imgyt,cardData } = params;
console.log(cardData,'cardDatacardData');
// 创建新的提示词卡片对象
const newCard = createSmartCard({
diyPromptImg:img,
diyPromptText:diyPromptText,
status:'loading',
imgyt:imgyt||'',
type:'image',
// inspirationImage:cardData.inspirationImage||'',
},index);
// 添加到卡片数组
cards.value.push(newCard);
}
// 处理图片生成请求
const handleGenerateRequested = async (params) => {
// 根据请求的数量动态生成对应数量的IPCard组件实例
const {count,profile,inspirationImage,ipType,ipTypeImg} = params
for (let i = 0; i < count; i++) {
// 使用统一的智能定位和层级管理系统创建卡片
const newCard = createSmartCard({
imageUrl: null, // 初始为空将由IPCard组件自己生成
prompt:profile.appearance,
inspirationImage: inspirationImage,
ipType:ipType,
ipTypeImg:ipTypeImg,
status:'loading',
type:'image',
});
// 添加到卡片数组
cards.value.push(newCard);
// 等待 200 毫秒
await new Promise(resolve => setTimeout(resolve, 200));
}
};
const handleCreateRemainingImageData = (cardItem)=>{
const newCard = createSmartCard({
imageUrl: cardItem.imageUrl||'', // 初始为空将由IPCard组件自己生成
prompt:cardItem.prompt||'',
inspirationImage: cardItem.inspirationImage||'',
status:'success',
type:'image',
});
// 添加到卡片数组
cards.value.push(newCard);
}
// 处理模型生成请求
const handleGenerateModelRequested = (index,params) => {
// 接收IPCard组件传递的参数创建新的modelCard组件实例
const {imageUrl } = params;
// 使用统一的智能定位和层级管理系统创建模型卡片
const newModelCard = createSmartCard({
imageUrl: imageUrl, // 从IPCard接收的图片URL
cardWidth: "250", // 默认宽度
type: 'model', // 标记为模型类型
modelUrl: null, // 初始为空将由modelCard组件自己生成
generateFourView: params.generateFourView || false, // 标记为四视图生成模式
status:'loading',
taskId:'',
project_id: projectId.value,
},index);
// 将新模型卡片添加到cards数组中
cards.value.push(newModelCard);
};
// 处理模型生成事件
const handleModelGenerated = (modelData) => {
// 使用统一的智能定位和层级管理系统创建模型卡片
const newCard = createSmartCard({
imageUrl: modelData.imageUrl, // 保留原始图片URL
altText: `生成的3D模型 - ${modelData.prompt || '未知提示词'}`,
cardWidth: "250", // 默认宽度
prompt: modelData.prompt || '',
style: modelData.style || '',
profile: modelData.profile || '',
timestamp: modelData.timestamp || Date.now(),
type: 'model', // 标记为模型类型
modelUrl: modelData.modelUrl, // 3D模型URL
taskId: modelData.taskId, // 任务ID
status: modelData.status // 任务状态
});
// 将新卡片添加到cards数组中
cards.value.push(newCard);
};
// 场景和元素拖拽相关状态
const isElementDragging = ref(false);
const isSceneDragging = ref(false);
const isPinchZooming = ref(false); // 是否正在进行双指缩放
const draggedElementIndex = ref(null);
const elementStartX = ref(0);
const elementStartY = ref(0);
const sceneStartX = ref(0);
const sceneStartY = ref(0);
const sceneOffsetX = ref(0); // 调整初始X位置让场景向左移动
const sceneOffsetY = ref(0); // 调整初始Y位置让场景向上移动
const scale = ref(1); // 缩放比例
const originalZIndexes = ref(new Map()); // 记录每个元素的原始z-index
// 双指缩放相关状态
const initialPinchDistance = ref(0); // 双指初始距离
const initialPinchCenterX = ref(0); // 双指初始中心点X
const initialPinchCenterY = ref(0); // 双指初始中心点Y
const initialScale = ref(1); // 初始缩放比例
const initialOffsetX = ref(0); // 初始偏移量X
const initialOffsetY = ref(0); // 初始偏移量Y
// 添加惯性动画相关状态
const elementVelocityX = ref(0);
const elementVelocityY = ref(0);
const lastMouseX = ref(0);
const lastMouseY = ref(0);
const lastTimestamp = ref(0);
const animationFrameId = ref(null);
// 计算场景容器样式
const sceneContainerStyle = computed(() => ({
transform: `translate(${sceneOffsetX.value}px, ${sceneOffsetY.value}px) scale(${scale.value})`,
transformOrigin: 'center center', // 缩放中心为场景中心
transition: 'none' // 移除缩放过渡效果
}));
// 获取指定索引元素的样式
const getElementStyle = (index) => {
const card = cards.value[index];
// 确保offsetX和offsetY有默认值
const offsetX = card.offsetX || '0px';
const offsetY = card.offsetY || '0px';
// 初始化元素的原始z-index如果还没有记录
if (!originalZIndexes.value.has(index) && card.zIndex) {
originalZIndexes.value.set(index, card.zIndex);
}
return {
transform: `translate(${offsetX}, ${offsetY})`,
cursor: isElementDragging.value && draggedElementIndex.value === index ? 'grabbing' : 'grab',
transition: isElementDragging.value && draggedElementIndex.value === index ? 'none' : 'transform 0.2s ease',
zIndex: card.zIndex || 'auto' // 应用z-index
};
};
const showOrderProcessModal = ref(false);
const CustomizeModalData = ref({})
const showPurchaseModal = ref(false);
//定制到家
const handleCustomizeToHome = (index)=>{
const cardItem = cards.value[index];
CustomizeModalData.value = cardItem;
showOrderProcessModal.value = true;
}
const handleBuyFromCustomize = () => {
showOrderProcessModal.value = false
showPurchaseModal.value = true
};
// 将元素提升到最前面(悬停时)
const bringToFront = (index) => {
// let maxIndex = cards.value.reduce((max, card) => Math.max(max, card.zIndex || 0), 0);
// cards.value[index].zIndex = maxIndex+1;
cards.value[index].zIndex = getMaxZIndex();
};
// 将元素恢复到原始层级(鼠标离开时)
const sendToBack = (index) => {
return
// cards.value[index].zIndex =1;
};
// 计算主内容区域的鼠标样式
const contentCursor = computed(() => {
return isSceneDragging.value ? 'grabbing' : 'grab';
});
// 检测是否是移动端
const isMobile = computed(() => {
return window.innerWidth <= 768;
});
// 开始拖动元素
const startElementDrag = (e, index) => {
e.stopPropagation(); // 阻止事件冒泡到场景
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault(); // 仅在桌面端阻止默认行为,包括图片重影效果
}
// 检查是否点击在控制按钮上,如果是则不启动拖动
if (e.target.closest('.control-button')) {
return;
}
isElementDragging.value = true;
isSceneDragging.value = false;
draggedElementIndex.value = index;
cards.value[index].zIndex =getMaxZIndex(1);
// 支持触摸和鼠标事件
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
// 获取当前元素的偏移量(转换为数值)
const currentOffsetX = parseFloat(cards.value[index].offsetX) || 0;
const currentOffsetY = parseFloat(cards.value[index].offsetY) || 0;
// 考虑场景缩放比例,调整元素起始位置计算
elementStartX.value = clientX - (currentOffsetX * scale.value);
elementStartY.value = clientY - (currentOffsetY * scale.value);
cards.value[index].offsetX = `${currentOffsetX}px`;
cards.value[index].offsetY = `${currentOffsetY}px`;
};
// 拖动元素
const dragElement = (e) => {
if (isElementDragging.value && draggedElementIndex.value !== null) {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault(); // 阻止默认行为
}
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const currentTime = performance.now();
// 计算速度
if (lastTimestamp.value > 0) {
const deltaTime = currentTime - lastTimestamp.value;
if (deltaTime > 0) {
elementVelocityX.value = (clientX - lastMouseX.value) / deltaTime * 16.67; // 转换为每帧速度
elementVelocityY.value = (clientY - lastMouseY.value) / deltaTime * 16.67;
}
}
// 更新最后位置和时间
lastMouseX.value = clientX;
lastMouseY.value = clientY;
lastTimestamp.value = currentTime;
// 考虑场景缩放比例,调整元素移动速度
const newOffsetX = (clientX - elementStartX.value) / scale.value;
const newOffsetY = (clientY - elementStartY.value) / scale.value;
// 更新元素位置,使用像素值
cards.value[draggedElementIndex.value].offsetX = `${newOffsetX}px`;
cards.value[draggedElementIndex.value].offsetY = `${newOffsetY}px`;
// cards.value[index].zIndex =cards.value[index].zIndex;
}
};
// 停止拖动元素
const stopElementDrag = () => {
if (isElementDragging.value && draggedElementIndex.value !== null) {
// 启动惯性动画
applyInertiaToElement();
}
isElementDragging.value = false;
draggedElementIndex.value = null;
lastTimestamp.value = 0;
};
// 应用元素惯性动画
const applyInertiaToElement = () => {
if (Math.abs(elementVelocityX.value) < 0.1 && Math.abs(elementVelocityY.value) < 0.1) {
elementVelocityX.value = 0;
elementVelocityY.value = 0;
return;
}
const index = draggedElementIndex.value;
if (index === null) return;
// 应用摩擦力减速
const friction = 0.95;
elementVelocityX.value *= friction;
elementVelocityY.value *= friction;
// 更新元素位置
const currentOffsetX = parseFloat(cards.value[index].offsetX) || 0;
const currentOffsetY = parseFloat(cards.value[index].offsetY) || 0;
cards.value[index].offsetX = `${currentOffsetX + elementVelocityX.value / scale.value}px`;
cards.value[index].offsetY = `${currentOffsetY + elementVelocityY.value / scale.value}px`;
// 继续动画
animationFrameId.value = requestAnimationFrame(applyInertiaToElement);
};
// 开始拖动场景
const startSceneDrag = (e) => {
// 如果正在拖动元素,则不响应场景拖动
if (isElementDragging.value) return;
// 只有在主内容区域(不是侧边栏)才允许拖动场景
if (e.target.closest('.main-content') && !e.target.closest('.floating-sidebar')) {
// 检测是否为双指触摸
if (e.touches && e.touches.length === 2) {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
isPinchZooming.value = true;
isSceneDragging.value = false;
// 计算双指初始距离
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
initialPinchDistance.value = distance;
// 计算双指初始中心点
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
initialPinchCenterX.value = centerX;
initialPinchCenterY.value = centerY;
// 记录初始缩放比例和偏移量
initialScale.value = scale.value;
initialOffsetX.value = sceneOffsetX.value;
initialOffsetY.value = sceneOffsetY.value;
} else {
// 单指触摸,执行普通拖拽
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
isSceneDragging.value = true;
isPinchZooming.value = false;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
sceneStartX.value = clientX - sceneOffsetX.value;
sceneStartY.value = clientY - sceneOffsetY.value;
}
}
};
// 计算两点之间的距离
const calculateDistance = (touch1, touch2) => {
return Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
};
// 拖动场景
const dragScene = (e) => {
if (isPinchZooming.value) {
// 双指缩放处理
if (e.touches && e.touches.length === 2) {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
// 计算当前双指距离
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = calculateDistance(touch1, touch2);
// 计算缩放比例变化
const scaleFactor = currentDistance / initialPinchDistance.value;
const newScale = Math.min(Math.max(initialScale.value * scaleFactor, 0.1), 5);
if (newScale !== scale.value) {
// 计算当前双指中心点
const currentCenterX = (touch1.clientX + touch2.clientX) / 2;
const currentCenterY = (touch1.clientY + touch2.clientY) / 2;
// 获取场景容器元素
const container = e.currentTarget;
// 计算双指中心点相对于容器的坐标
const rect = container.getBoundingClientRect();
const mouseX = currentCenterX - rect.left;
const mouseY = currentCenterY - rect.top;
// 补偿偏移量向右偏移600像素向上偏移400像素
// 这意味着实际鼠标位置需要向左偏移600向下偏移400
// 移动端不需要调整偏差
const adjustedMouseX = mouseX - 600;
const adjustedMouseY = mouseY - 400;
// 计算当前场景中心点相对于调整后鼠标位置的偏移
const currentOffsetFromMouseX = adjustedMouseX - initialOffsetX.value;
const currentOffsetFromMouseY = adjustedMouseY - initialOffsetY.value;
// 计算缩放后的新偏移量,使缩放围绕调整后的鼠标位置进行
const newOffsetFromMouseX = currentOffsetFromMouseX * (newScale / initialScale.value);
const newOffsetFromMouseY = currentOffsetFromMouseY * (newScale / initialScale.value);
// 更新场景偏移量
sceneOffsetX.value = adjustedMouseX - newOffsetFromMouseX;
sceneOffsetY.value = adjustedMouseY - newOffsetFromMouseY;
// 更新缩放比例
scale.value = newScale;
}
}
} else if (isSceneDragging.value) {
// 单指拖拽处理
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
sceneOffsetX.value = clientX - sceneStartX.value;
sceneOffsetY.value = clientY - sceneStartY.value;
} else if (isElementDragging.value) {
// 如果正在拖动元素,则执行元素拖动
dragElement(e);
}
};
// 停止拖动场景
const stopSceneDrag = () => {
isSceneDragging.value = false;
isPinchZooming.value = false; // 重置双指缩放状态
isElementDragging.value = false;
draggedElementIndex.value = null;
};
// 处理鼠标滚轮事件,实现缩放和移动功能
const handleWheel = (e) => {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault(); // 阻止默认的滚轮行为(页面滚动)
}
// 如果按下了Ctrl键Windows/Linux或Cmd键Mac执行缩放操作
if (e.ctrlKey || e.metaKey) {
// 计算缩放比例增量
const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1;
// 限制缩放范围,防止缩放过大或过小
const newScale = scale.value * scaleFactor;
if (newScale >= 0.1 && newScale <= 5) {
// 获取场景容器元素
const container = e.currentTarget;
// 计算鼠标在容器中的相对位置(像素值)
const rect = container.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 补偿偏移量向右偏移600像素向上偏移400像素
// 这意味着实际鼠标位置需要向左偏移600向下偏移400
const adjustedMouseX = mouseX - 900;
const adjustedMouseY = mouseY - 400;
// 计算当前场景中心点相对于调整后鼠标位置的偏移
const currentOffsetFromMouseX = adjustedMouseX - sceneOffsetX.value;
const currentOffsetFromMouseY = adjustedMouseY - sceneOffsetY.value;
// 计算缩放后的新偏移量,使缩放围绕调整后的鼠标位置进行
const newOffsetFromMouseX = currentOffsetFromMouseX * scaleFactor;
const newOffsetFromMouseY = currentOffsetFromMouseY * scaleFactor;
// 更新场景偏移量
sceneOffsetX.value = adjustedMouseX - newOffsetFromMouseX;
sceneOffsetY.value = adjustedMouseY - newOffsetFromMouseY;
// 更新缩放比例
scale.value = newScale;
}
} else {
// 单纯滚轮滚动,实现场景上下移动
const moveAmount = e.deltaY * 0.5; // 调整移动速度
sceneOffsetY.value += (-moveAmount);
}
};
// 修改阻止鼠标滚轮缩放的函数,仅在侧边栏区域阻止
const preventZoom = (e) => {
// 检查事件是否来自侧边栏区域
if ((e.ctrlKey || e.metaKey) && e.target.closest('.floating-sidebar')) {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
}
};
// 监听触摸事件以防止缩放手势
const preventPinchZoom = (e) => {
// 仅在侧边栏区域阻止缩放手势,主内容区域允许缩放手势
if (e.touches && e.touches.length > 1 && e.target.closest('.floating-sidebar')) {
// 移动端不阻止默认行为,避免影响触摸事件
if (!isMobile.value) {
e.preventDefault();
}
}
};
// 导入被动事件监听器工具
import { addPassiveEventListener } from '@/utils/passiveEventListeners'
const init = ()=>{
const route = useRoute();
projectId.value = route.params.id;
series.value = route.params.series;
if(projectId.value === 'new'){
createProject();
return
}
getProjectInfo(projectId.value);
getGenerateCount();
}
// 组件挂载时添加事件监听器
onMounted(() => {
MeshyServer.pollingEnabled = true;
GiminiServer.pollingEnabled = true;
// 每次进入都显示引导弹窗
// showGuideModal.value = true;
init();
getCombinedPrompt();
// 使用优化的被动事件监听器
const removeWheelListener = addPassiveEventListener(document, 'wheel', preventZoom);
const removeTouchStartListener = addPassiveEventListener(document, 'touchstart', preventPinchZoom);
const removeTouchMoveListener = addPassiveEventListener(document, 'touchmove', preventPinchZoom);
// 存储清理函数以便组件卸载时使用
cleanupFunctions.value = {
wheel: removeWheelListener,
touchstart: removeTouchStartListener,
touchmove: removeTouchMoveListener
};
});
// 组件卸载时移除事件监听器,避免内存泄漏
onUnmounted(() => {// 禁用轮询
MeshyServer.pollingEnabled = false;
GiminiServer.pollingEnabled = false;
clearInterval(Project.timer);
if (cleanupFunctions.value) {
Object.values(cleanupFunctions.value).forEach(cleanup => {
if (typeof cleanup === 'function') {
cleanup();
}
});
}
});
</script>
<style scoped>
/* 头部组件容器样式 */
.header-wrapper {
position: fixed;
top:8px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
width: 98.6%;
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(229, 231, 235, 0.8);
transition: all 0.3s ease;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06);
}
/* 暗色主题下的头部组件容器 */
html.dark .header-wrapper {
background-color: rgba(31, 41, 55, 0.95);
border-bottom-color: rgba(75, 85, 99, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
}
.creative-zone {
color: #1f2937; /* 默认亮色主题文字颜色 */
height: 100vh; /* 固定高度为视口高度 */
overflow: hidden; /* 禁止溢出滚动 */
display: flex;
gap: 20px;
box-sizing: border-box; /* 确保内边距不增加总尺寸 */
position: relative;
transition: all 0.3s ease;
/* 背景渐变,移除网格 */
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 50%, #e5e7eb 100%);
}
/* 移动端适配 */
@media (max-width: 768px) {
.creative-zone {
height: 100vh; /* 移动端使用视口高度 */
min-height: -webkit-fill-available; /* iOS Safari 支持 */
overflow: hidden;
}
}
/* 暗色主题适配 */
html.dark .creative-zone {
color: #ffffff; /* 暗色主题文字颜色 */
background: linear-gradient(135deg, #1F2937 0%, #111827 50%, #030712 100%);
}
.sidebar-container{
position: absolute;
left: 12px;
top: 70px;
z-index: 30;
/* background-color: rgba(255, 255, 255, 0.95); */
border-radius: 12px;
/* border: 1px solid rgba(229, 231, 235, 0.8); */
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
/* 暗色主题下的侧边栏容器 */
html.dark .sidebar-container {
background-color: rgba(31, 41, 55, 0.95);
border-bottom-color: rgba(75, 85, 99, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 测试动画按钮样式 */
.test-animation-btn {
background: linear-gradient(135deg, #6B46C1 0%, #7C3AED 50%, #A78BFA 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 4px 12px rgba(107, 70, 193, 0.3),
0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.test-animation-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow:
0 8px 25px rgba(107, 70, 193, 0.4),
0 4px 12px rgba(0, 0, 0, 0.15);
}
.test-animation-btn:active {
transform: translateY(0) scale(0.98);
transition: all 0.1s ease;
}
html.dark .test-animation-btn {
background: linear-gradient(135deg, #A78BFA 0%, #8B5CF6 50%, #6B46C1 100%);
box-shadow:
0 4px 12px rgba(167, 139, 250, 0.3),
0 2px 4px rgba(0, 0, 0, 0.2);
}
html.dark .test-animation-btn:hover {
box-shadow:
0 8px 25px rgba(167, 139, 250, 0.4),
0 4px 12px rgba(0, 0, 0, 0.25);
}
/* 响应式设计 - 移动端适配 */
@media (max-width: 768px) {
.test-animation-btn {
padding: 10px 20px;
font-size: 12px;
bottom: 15px;
right: 15px;
}
.sidebar-overlay {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
}
.creative-zone::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(107, 70, 193, 0.05) 0%, transparent 50%, rgba(167, 139, 250, 0.05) 100%);
pointer-events: none;
z-index: 0;
}
/* 暗色主题下的背景叠加层 */
html.dark .creative-zone::before {
background: linear-gradient(45deg, rgba(167, 139, 250, 0.1) 0%, transparent 50%, rgba(107, 70, 193, 0.1) 100%);
}
/* 全局box-sizing设置确保所有元素的尺寸计算一致 */
* {
box-sizing: border-box;
touch-action: manipulation;
}
/* 防止在移动设备上的缩放手势 */
html {
touch-action: pan-x pan-y;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
/* 移动端适配 - 移除全局用户选择限制 */
@media (max-width: 768px) {
body {
user-select: auto;
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
}
}
/* 桌面端防止用户选择文本和右键菜单 */
@media (min-width: 769px) {
body {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
}
/* 主内容区域 */
.main-content {
flex: 1;
position: relative;
overflow: hidden;
user-select: none;
touch-action: none; /* 阻止触摸设备上的默认行为 */
}
/* 移动端主内容区域适配 */
@media (max-width: 768px) {
.main-content {
touch-action: pan-x pan-y; /* 移动端允许平移 */
user-select: auto; /* 移动端允许选择文本 */
}
}
.scene-container {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
background:
/* 简约画布背景 */
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.9) 0%, rgba(243, 244, 246, 0.9) 100%),
/* 方格子四个角点 */
radial-gradient(circle at 0 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px);
background-size: 100% 100%, 30px 30px, 30px 30px, 30px 30px, 30px 30px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
border-radius: 8px;
overflow: hidden;
box-shadow:
inset 0 1px 3px rgba(0, 0, 0, 0.03),
0 1px 3px rgba(0, 0, 0, 0.03);
}
/* 暗色主题下的场景容器 */
html.dark .scene-container {
background:
/* 简约画布背景 - 暗色主题 */
radial-gradient(ellipse at center, rgba(31, 41, 55, 0.9) 0%, rgba(17, 24, 39, 0.9) 100%),
/* 方格子四个角点 - 暗色主题 */
radial-gradient(circle at 0 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px);
background-size: 100% 100%, 30px 30px, 30px 30px, 30px 30px, 30px 30px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
box-shadow:
inset 0 1px 3px rgba(0, 0, 0, 0.2),
0 1px 3px rgba(0, 0, 0, 0.15);
}
.scene-content {
position: absolute;
top: 0%;
left: 0%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
will-change: transform; /* 提示浏览器提前准备渲染变换 */
contain: layout style;
}
/* 可拖拽元素样式 */
.draggable-element {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 初始居中 */
cursor: grab;
user-select: none;
-webkit-user-drag: none; /* 禁用图片拖拽的默认行为 */
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
/* 禁用图片重影效果 */
pointer-events: auto;
will-change: transform; /* 提示浏览器优化渲染 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 8px;
box-shadow:
0 4px 12px rgba(107, 70, 193, 0.15),
0 2px 6px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* 暗色主题下的可拖拽元素 */
html.dark .draggable-element {
background: rgba(31, 41, 55, 0.95);
box-shadow:
0 4px 12px rgba(167, 139, 250, 0.15),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.draggable-element:hover {
transform: translate(-50%, -50%) scale(1.02);
box-shadow:
0 8px 25px rgba(107, 70, 193, 0.25),
0 4px 12px rgba(107, 70, 193, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* 暗色主题下的悬停效果 */
html.dark .draggable-element:hover {
box-shadow:
0 8px 25px rgba(167, 139, 250, 0.25),
0 4px 12px rgba(167, 139, 250, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* 当鼠标悬停在可拖拽元素上时显示删除按钮 */
.draggable-element:hover .delete-button {
opacity: 1;
visibility: visible;
transform: scale(1);
}
/* 删除按钮样式 */
.delete-button {
position: absolute;
top: -12px;
right: -12px;
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
border: none;
border-radius: 50%;
background: linear-gradient(135deg, #6B46C1 0%, #553C9A 50%, #44337A 100%);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 4px 12px rgba(107, 70, 193, 0.4),
0 2px 4px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
opacity: 0;
visibility: hidden;
flex-shrink: 0;
transform: scale(0.8);
backdrop-filter: blur(10px);
overflow: hidden;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.delete-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.2) 0%, transparent 70%);
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
.delete-button:hover::before {
opacity: 1;
}
.delete-button:hover {
background: linear-gradient(135deg, #7C3AED 0%, #6B46C1 50%, #553C9A 100%);
transform: scale(1.15);
box-shadow:
0 8px 25px rgba(107, 70, 193, 0.6),
0 4px 12px rgba(107, 70, 193, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.delete-button:active {
transform: scale(1.05);
transition: all 0.1s ease;
}
.delete-button:focus {
outline: none;
box-shadow:
0 4px 12px rgba(255, 71, 87, 0.4),
0 2px 4px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 3px rgba(255, 71, 87, 0.3);
}
.delete-button svg {
width: 16px !important;
height: 16px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
stroke: currentColor;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
.delete-button:hover svg {
transform: scale(1.1);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.draggable-element * {
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
/* 拖动时的鼠标效果 */
.draggable-element:active {
cursor: grabbing;
}
.ip-display-section {
margin-top: 30px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
transition: all 0.3s ease;
}
.ip-display-section h2 {
margin-bottom: 20px;
color: #333;
font-size: 20px;
font-weight: 600;
}
/* 暗色主题下的IP展示区域 */
html.dark .ip-display-section {
background-color: rgba(31, 41, 55, 0.9);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
html.dark .ip-display-section h2 {
color: #E5E7EB;
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
p {
font-size: 1.2rem;
opacity: 0.9;
}
/* 响应式设计 */
@media (max-width: 768px) {
.creative-zone {
flex-direction: column;
gap: 10px;
padding: 5px;
}
.floating-sidebar {
width: 100%;
max-width: none;
margin: 0;
padding: 15px;
max-height: calc(100vh - 10px);
}
.main-content {
flex: 1;
min-height: 0; /* 允许主内容区域缩小 */
}
h1 {
font-size: 2rem;
}
p {
font-size: 1rem;
}
/* 移动端场景容器适配 */
.scene-container {
touch-action: pan-x pan-y pinch-zoom; /* 允许平移和缩放 */
}
/* 移动端可拖动元素适配 */
.draggable-element {
touch-action: none; /* 拖动元素时阻止默认触摸行为 */
}
/* 移动端头部适配 */
.header-wrapper {
width: 100%;
border-radius: 0;
}
/* 移动端删除按钮始终可见且更大 */
.delete-button {
opacity: 1 !important;
visibility: visible !important;
transform: scale(1) !important;
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
top: -14px !important;
right: -14px !important;
box-shadow:
0 4px 12px rgba(107, 70, 193, 0.6),
0 2px 6px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3) !important;
}
.delete-button svg {
width: 18px !important;
height: 18px !important;
}
}
</style>