1700 lines
53 KiB
Vue
1700 lines
53 KiB
Vue
<template>
|
||
<div class="creative-zone" @contextmenu.prevent>
|
||
<!-- 顶部固定头部组件 -->
|
||
<div class="header-wrapper">
|
||
<HeaderComponent ref="headerRef" @back="handleBack" :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
|
||
</div>
|
||
<!-- 导入的侧边栏组件 -->
|
||
<div class="sidebar-container">
|
||
<iPandCardLeft
|
||
:locale="locale"
|
||
:series="series"
|
||
ref="iPandCardLeftRef"
|
||
:Info="projectInfo.details"
|
||
@generate-requested="handleGenerateRequested"
|
||
@model-generated="handleModelGenerated"
|
||
@import-character="openImportModal"
|
||
@hook-selected="handleHookSelected"
|
||
/>
|
||
</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
|
||
@delete="handleDeleteCard(index)"
|
||
@delete-card="handleDeleteCard(index)"
|
||
:combinedPromptJson="combinedPromptJson"
|
||
:series="series"
|
||
@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)"
|
||
@download-image="downloadImage"
|
||
: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"
|
||
/>
|
||
<!-- 下载确认弹窗 -->
|
||
<DownloadConfirmModal
|
||
:show="showDownloadConfirm"
|
||
@confirm="handleDownloadConfirm"
|
||
@cancel="showDownloadConfirm = false"
|
||
/>
|
||
<DtCanvasEditor
|
||
:language="locale"
|
||
v-model:visible="canvasEditorVisible"
|
||
:image-url="canvasEditorImageUrl"
|
||
@add-prompt-card="handleCanvasSave"
|
||
/>
|
||
<!-- 定制到家弹窗 -->
|
||
<OrderProcessModal
|
||
:series="series"
|
||
:show="showOrderProcessModal"
|
||
:modelData="CustomizeModalData"
|
||
@close="showOrderProcessModal=false"
|
||
@acknowledge="handleBuyFromCustomize" />
|
||
<PurchaseModal
|
||
v-if="showPurchaseModal"
|
||
:series="series"
|
||
:show="showPurchaseModal"
|
||
:modelData="CustomizeModalData"
|
||
@close="showPurchaseModal=false" />
|
||
<el-tour v-model="openShow">
|
||
<el-tour-step :target="tourJson?.tour1">
|
||
<Tour1 />
|
||
</el-tour-step>
|
||
<el-tour-step :placement="'right-end'" :target="tourJson?.tour2">
|
||
<Tour2 />
|
||
</el-tour-step>
|
||
<el-tour-step :placement="'right-end'" :target="tourJson?.tour3" >
|
||
<Tour3 />
|
||
</el-tour-step>
|
||
<el-tour-step :placement="'right-end'" :target="tourJson?.tour4" title="参考图">
|
||
<Tour4 />
|
||
</el-tour-step>
|
||
<el-tour-step :placement="'right-end'" :target="tourJson?.tour5" title="创作按钮">
|
||
<Tour5 />
|
||
</el-tour-step>
|
||
</el-tour>
|
||
</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 DownloadConfirmModal from '../../components/DownloadConfirmModal/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 {Project} from './index';
|
||
import {ModernHome} from '../ModernHome/index.js'
|
||
import { useI18n } from 'vue-i18n';
|
||
import {ElTour,ElTourStep} from 'element-plus';
|
||
import Tour1 from '../../components/tours/tour1.vue';
|
||
import Tour2 from '../../components/tours/tour2.vue';
|
||
import Tour3 from '../../components/tours/tour3.vue';
|
||
import Tour4 from '../../components/tours/tour4.vue';
|
||
import Tour5 from '../../components/tours/tour5.vue';
|
||
const { locale } = useI18n()
|
||
const headerRef = ref(null);
|
||
const iPandCardLeftRef = ref(null);
|
||
const tourJson = ref({
|
||
tour1:''
|
||
})
|
||
const fileServer = new FileServer();
|
||
const modernHome = new ModernHome();
|
||
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 openShow = ref(false);
|
||
// 图片预览弹窗相关状态
|
||
const showImagePreview = ref(false);
|
||
const previewImages = ref([]);
|
||
const currentImageIndex = ref(0);
|
||
// 下载确认弹窗相关状态
|
||
const showDownloadConfirm = ref(false);
|
||
const pendingDownloadUrl = ref(null);
|
||
// 事件监听器清理函数存储
|
||
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 downloadImage = async (url) => {
|
||
pendingDownloadUrl.value = url;
|
||
showDownloadConfirm.value = true;
|
||
}
|
||
|
||
const handleDownloadConfirm = async () => {
|
||
showDownloadConfirm.value = false;
|
||
const url = pendingDownloadUrl.value;
|
||
pendingDownloadUrl.value = null;
|
||
|
||
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 setTourJson = ()=>{
|
||
const tourFlag = window.localStorage.getItem('tourFlag');
|
||
if(tourFlag){
|
||
return;
|
||
}
|
||
tourJson.value.tour1 = headerRef?.value?.RechargeRef;
|
||
tourJson.value.tour2 = iPandCardLeftRef?.value?.ipTypeSectionRef;
|
||
tourJson.value.tour3 = iPandCardLeftRef?.value?.textPromptSectionRef;
|
||
tourJson.value.tour4 = iPandCardLeftRef?.value?.referenceImageSectionRef;
|
||
tourJson.value.tour5 = iPandCardLeftRef?.value?.generateButtonRef;
|
||
openShow.value = true;
|
||
window.localStorage.setItem('tourFlag','1');
|
||
}
|
||
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',
|
||
addDiyPromptImg:true
|
||
// inspirationImage:cardData.inspirationImage||'',
|
||
});
|
||
// console.log(newCard,'newCardnewCard');
|
||
cards.value.push(newCard);
|
||
})
|
||
}
|
||
// 多个可拖动元素的数据
|
||
const cards = ref([
|
||
|
||
]);
|
||
//判断引导弹窗的弹出逻辑,需要每日弹出一次
|
||
const showGuideModalLogic = ()=>{
|
||
// const tourFlag = window.localStorage.getItem('tourFlag');
|
||
// if(!tourFlag){
|
||
// return;
|
||
// }
|
||
// 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)=>{
|
||
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 handleBack = ()=>{
|
||
router.replace(`/creation-workspace`);
|
||
}
|
||
const isHook = ref(true);//默认需要挂钩
|
||
|
||
const handleHookSelected = (value) => {
|
||
isHook.value = value;
|
||
getCombinedPrompt({
|
||
isHook:value,
|
||
});
|
||
}
|
||
const combinedPromptJson = ref({});
|
||
//获取动态提示词
|
||
const getCombinedPrompt = async (config={})=>{
|
||
try {
|
||
const data = await PluginProject.getCombinedPrompt(series.value,config);
|
||
combinedPromptJson.value = data;
|
||
console.log(data,'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 currentDate = new Date().toDateString();
|
||
localStorage.setItem('lastShowGuideModalDate', currentDate);
|
||
};
|
||
|
||
// 完成引导
|
||
const completeGuide = () => {
|
||
showGuideModal.value = false;
|
||
const currentDate = new Date().toDateString();
|
||
localStorage.setItem('lastShowGuideModalDate', currentDate);
|
||
};
|
||
|
||
// ==================== 定位计算器 ====================
|
||
/**
|
||
* 卡片定位计算系统
|
||
* 负责计算新卡片相对于最高层级卡片的确切位置
|
||
*/
|
||
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) => {
|
||
// 阻止默认的滚轮行为(页面滚动),以便处理触摸板的水平滚动
|
||
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 moveAmountY = e.deltaY * 0.5;
|
||
const moveAmountX = e.deltaX * 0.5;
|
||
|
||
// 更新场景偏移量
|
||
sceneOffsetY.value += (-moveAmountY);
|
||
sceneOffsetX.value += (-moveAmountX);
|
||
}
|
||
};
|
||
|
||
// 修改阻止鼠标滚轮缩放的函数,仅在侧边栏区域阻止
|
||
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(() => {
|
||
showGuideModalLogic();
|
||
MeshyServer.pollingEnabled = true;
|
||
GiminiServer.pollingEnabled = true;
|
||
// 每次进入都显示引导弹窗
|
||
// showGuideModal.value = true;
|
||
init();
|
||
getCombinedPrompt();
|
||
setTimeout(()=>{
|
||
setTourJson();
|
||
},200)
|
||
// 使用非被动事件监听器,以便能够阻止默认行为
|
||
const removeWheelListener = addPassiveEventListener(document, 'wheel', preventZoom, { passive: false });
|
||
const removeTouchStartListener = addPassiveEventListener(document, 'touchstart', preventPinchZoom, { passive: false });
|
||
const removeTouchMoveListener = addPassiveEventListener(document, 'touchmove', preventPinchZoom, { passive: false });
|
||
// 存储清理函数以便组件卸载时使用
|
||
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: pan-x pan-y; /* 允许触摸板左右和上下拖动 */
|
||
}
|
||
|
||
/* 移动端主内容区域适配 */
|
||
@media (max-width: 768px) {
|
||
.main-content {
|
||
touch-action: none; /* 移动端阻止默认行为,使用自定义触摸处理 */
|
||
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>
|