1055 lines
28 KiB
Vue
1055 lines
28 KiB
Vue
<template>
|
||
<div class="ip-card-container" @touchstart="handleTouchStart" @touchend="handleTouchEnd" :class="{ 'controls-visible': isControlsVisible }">
|
||
<!-- 主卡片区域 -->
|
||
<div class="ip-card-wrapper">
|
||
<!-- 文本输入框弹出层 -->
|
||
<div v-if="showTextInput" class="text-input-overlay" role="dialog" aria-modal="true" aria-label="文本输入">
|
||
<div class="text-input-container">
|
||
<div class="text-input-header">
|
||
<div class="text-input-title">文本输入</div>
|
||
<button class="text-input-close" @click="handleTextInputCancel" @touchend.prevent="handleTextInputCancel" aria-label="关闭">
|
||
<el-icon class="close-icon"><CloseBold /></el-icon>
|
||
</button>
|
||
</div>
|
||
<textarea
|
||
ref="textInputRef"
|
||
v-model="textInputValue"
|
||
class="text-input-area"
|
||
:placeholder="t('modelModal.textInputPlaceholder')"
|
||
rows="4"
|
||
@keydown.enter.ctrl="handleTextInputConfirm"
|
||
@keydown.esc="handleTextInputCancel"
|
||
@click.stop
|
||
@mousedown.stop
|
||
></textarea>
|
||
<div class="text-input-actions">
|
||
<button class="text-input-btn cancel-btn" @click="handleTextInputCancel" @touchend.prevent="handleTextInputCancel">
|
||
取消
|
||
</button>
|
||
<button class="text-input-btn confirm-btn" @click="handleTextInputConfirm" @touchend.prevent="handleTextInputConfirm">
|
||
确定
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ip-card" :style="cardStyle">
|
||
<el-image
|
||
v-if="formData.internalImageUrl"
|
||
:src="formData.internalImageUrl"
|
||
@contextmenu.prevent
|
||
alt="IP"
|
||
class="ip-card-image"
|
||
:teleported="true"
|
||
@error="handleImageError"
|
||
@load="handleImageLoad"
|
||
/>
|
||
<!-- <div v-else class="generating-placeholder">
|
||
<div class="generating-spinner"></div>
|
||
<div class="generating-text">正在生成图片...</div>
|
||
</div> -->
|
||
<el-skeleton v-else style="width:100%;height: 100%;" animated>
|
||
<template #template>
|
||
<el-skeleton-item variant="image" style="width:100%;height: 100%;" />
|
||
<!-- <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>
|
||
<!-- 右侧控件区域 -->
|
||
<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>
|
||
</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>
|
||
<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>
|
||
</template>
|
||
|
||
<script setup>
|
||
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'
|
||
// import anTypeImg from '@/assets/sketches/dwww.webp';
|
||
// 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 { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus'
|
||
const { t } = useI18n();
|
||
const formData = ref({
|
||
internalImageUrl: '',//内部图片URL
|
||
status:'loading',//状态
|
||
});
|
||
// 触摸事件状态
|
||
const isTouching = ref(false);
|
||
const isControlsVisible = ref(false);
|
||
const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f';
|
||
const anTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e82b2b6-0e5d-4a62-b65f-098952eb2f67';
|
||
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e3e60cc7-9777-41ba-9d1e-f5ffc92e4fac.webp';
|
||
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/61770f50-4b87-40a0-9297-cabda0ec6317.webp'
|
||
const humanTypeImg = ''
|
||
const giminiServer = new GiminiServer();
|
||
// 图片比例
|
||
const imageAspectRatio = ref(9/16); // 默认比例
|
||
// 控制文本输入框显示状态
|
||
const showTextInput = ref(false);
|
||
// 文本输入框内容
|
||
const textInputValue = ref('');
|
||
// 文本输入框引用
|
||
const textInputRef = ref(null);
|
||
|
||
const handleImageClick = () => {
|
||
// 点击图片时触发预览
|
||
emit('preview-image', formData.value.internalImageUrl);
|
||
}
|
||
|
||
// 切换文本输入框显示/隐藏
|
||
const toggleTextInput = () => {
|
||
showTextInput.value = !showTextInput.value;
|
||
if (showTextInput.value) {
|
||
// 显示时清空输入内容
|
||
textInputValue.value = '';
|
||
// 使用nextTick确保DOM更新后再聚焦
|
||
nextTick(() => {
|
||
if (textInputRef.value) {
|
||
textInputRef.value.focus();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
const handleTextcjt = ()=>{
|
||
emit('create-prompt-card', {
|
||
img: formData.value.internalImageUrl||'',
|
||
imgyt:props?.cardData?.inspirationImage||'',
|
||
diyPromptText:cjt,
|
||
cardData: props.cardData
|
||
});
|
||
}
|
||
const handleCustomizeToHome = () => {
|
||
emit('customize-to-home', {
|
||
imageUrl: formData.value.internalImageUrl,
|
||
cardData: props.cardData
|
||
});
|
||
}
|
||
// 处理文本输入确认
|
||
const handleTextInputConfirm = () => {
|
||
// 触发创建新卡片事件,传递用户输入的文本内容
|
||
if (textInputValue.value.trim()) {
|
||
emit('create-prompt-card', {
|
||
img: formData.value.internalImageUrl,
|
||
diyPromptText:textInputValue.value,
|
||
generateFourView:props.generateFourView,
|
||
cardData: props.cardData
|
||
});
|
||
showTextInput.value = false;
|
||
textInputValue.value = '';
|
||
}
|
||
};
|
||
// 处理文本输入取消
|
||
const handleTextInputCancel = () => {
|
||
showTextInput.value = false;
|
||
textInputValue.value = '';
|
||
};
|
||
// 处理触摸开始事件
|
||
const handleTouchStart = () => {
|
||
isTouching.value = true;
|
||
if (isTouching.value) {
|
||
isControlsVisible.value = true;
|
||
}
|
||
};
|
||
// 处理触摸结束事件
|
||
const handleTouchEnd = () => {
|
||
isTouching.value = false;
|
||
setTimeout(() => {
|
||
isControlsVisible.value = false;
|
||
}, 3000); // 3秒后自动隐藏控件
|
||
};
|
||
// 定义组件属性
|
||
const props = defineProps({
|
||
combinedPromptJson:{//动态提示词
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 图片URL
|
||
imageUrl: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 卡片宽度(默认200px,横版图片自动调整为400px)
|
||
cardWidth: {
|
||
type: Number,
|
||
default: 200
|
||
},
|
||
// 是否显示边框
|
||
showBorder: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 边框圆角
|
||
borderRadius: {
|
||
type: Number,
|
||
default: 8
|
||
},
|
||
// 新增:卡片数据对象,包含所有生图相关参数
|
||
cardData: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 新增:四视图生成标识参数
|
||
generateFourView: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
});
|
||
|
||
// 定义事件
|
||
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;
|
||
try {
|
||
// 使用多图参考生成
|
||
let referenceImages = [];
|
||
// 如果有灵感图片,也添加到参考图片列表
|
||
if (props?.cardData?.inspirationImage) {
|
||
referenceImages.push(props.cardData.inspirationImage);
|
||
}
|
||
if(iscjt){
|
||
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
|
||
referenceImages.push(props.cardData.diyPromptImg);
|
||
referenceImages.push(cjimg);
|
||
}
|
||
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,
|
||
aspect_ratio:iscjt?'16:9':'9:16',
|
||
});
|
||
saveProject(taskResult);
|
||
getImageTask(taskResult.taskId,taskResult.taskQueue);
|
||
// 组件内部自己赋值,不通知父组件
|
||
return
|
||
formData.value.internalImageUrl = imageUrl;
|
||
formData.value.status = 'success';
|
||
saveProject();
|
||
} catch (error) {
|
||
console.log(error);
|
||
emit('delete');
|
||
}
|
||
};
|
||
//查询图片任务
|
||
const getImageTask = async (taskId,taskQueue)=>{
|
||
giminiServer.getTaskGinimi(taskId,taskQueue,(imgurls)=>{
|
||
let imgItem = imgurls.splice(0,1)[0]
|
||
formData.value.internalImageUrl=imgItem.url;
|
||
saveProject({
|
||
taskId:taskId,
|
||
taskQueue:taskQueue,
|
||
});
|
||
// createRemainingImageData(imgurls);
|
||
},()=>{
|
||
ElMessage.error('Failed to generate image, please try again later.');
|
||
emit('delete');
|
||
});
|
||
}
|
||
//创建剩余图片数据
|
||
const createRemainingImageData = (imageUrl)=>{
|
||
if(imageUrl.length>0){
|
||
imageUrl.forEach((item)=>{
|
||
emit('create-remaining-image-data',{
|
||
...props.cardData,
|
||
imageUrl:item.url,
|
||
})
|
||
})
|
||
}
|
||
}
|
||
const init = ()=>{
|
||
if(props.cardData.imageUrl){
|
||
formData.value.internalImageUrl = props.cardData.imageUrl;
|
||
}else if(props.cardData.taskId&&props.cardData.taskQueue){
|
||
getImageTask(props.cardData.taskId,props.cardData.taskQueue);
|
||
}else{
|
||
handleGenerateImage();
|
||
}
|
||
// let status = props.cardData.status;
|
||
// switch (status) {
|
||
// case 'loading':
|
||
// handleGenerateImage();
|
||
// break;
|
||
// default:
|
||
// formData.value.internalImageUrl = props.cardData.imageUrl;
|
||
// break;
|
||
// }
|
||
}
|
||
//保存到项目
|
||
const saveProject = (taskResult)=>{
|
||
emit('save-project', {
|
||
imageUrl:formData.value.internalImageUrl,
|
||
taskId:taskResult?.taskId||'',
|
||
taskQueue:taskResult?.taskQueue||'',
|
||
// status:formData.value.status,
|
||
status:'success',
|
||
});
|
||
}
|
||
|
||
// 组件挂载时的逻辑
|
||
onMounted(async () => {
|
||
// formData.value.internalImageUrl = demoImage;
|
||
// return
|
||
// return
|
||
init();
|
||
});
|
||
// 处理生成模型按钮点击
|
||
const handleGenerateModel = async () => {
|
||
try {
|
||
emit('generate-model-requested', {
|
||
cardId: props.cardData.id,
|
||
imageUrl:formData.value.internalImageUrl,
|
||
generateFourView: props.generateFourView,
|
||
});
|
||
} catch (error) {
|
||
console.error('生成3D模型失败:', error);
|
||
}
|
||
};
|
||
// 计算卡片样式,根据图片实际比例调整
|
||
const cardStyle = computed(() => {
|
||
// 如果图片宽度比例大于高度比例(横版图片),使用400px,否则使用props传入的宽度
|
||
const width = imageAspectRatio.value > 1 ? 400 : props.cardWidth;
|
||
// 使用图片实际比例,调整高度
|
||
const height = width / imageAspectRatio.value;
|
||
|
||
return {
|
||
width: `${width}px`,
|
||
height: `${height}px`,
|
||
borderRadius: `${props.borderRadius}px`,
|
||
border: props.showBorder ? '1px solid #e0e0e0' : 'none'
|
||
};
|
||
});
|
||
// 处理图片加载错误
|
||
const handleImageError = (event) => {
|
||
console.warn('图片加载失败:', event.target.src);
|
||
};
|
||
// 处理图片加载完成,获取图片实际比例
|
||
const handleImageLoad = (event) => {
|
||
// console.log(event,'eventeventevent');
|
||
const img = event.target;
|
||
const width = img.naturalWidth;
|
||
const height = img.naturalHeight;
|
||
if (width > 0 && height > 0) {
|
||
imageAspectRatio.value = width / height;
|
||
// console.log(`图片比例: ${width}:${height} (${imageAspectRatio.value.toFixed(2)})`);
|
||
}
|
||
};
|
||
</script>
|
||
<style scoped>
|
||
.ip-card-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
padding: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
/* 左侧附件卡片样式 */
|
||
.left-attachment-card {
|
||
position: absolute;
|
||
right: 100%;
|
||
top: 50%;
|
||
/* 初始状态:从主卡片位置开始 */
|
||
transform: translateY(-50%) translateX(calc(100% + 24px));
|
||
margin-right: 24px;
|
||
z-index: 5;
|
||
opacity: 0;
|
||
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
background-color: #1a1a1a;
|
||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.left-attachment-card.show {
|
||
opacity: 1;
|
||
/* 最终状态:移动到左侧位置 */
|
||
transform: translateY(-50%) translateX(0);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.left-card-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.left-card-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #2a2a2a;
|
||
color: #9e9e9e;
|
||
}
|
||
|
||
.left-card-placeholder .placeholder-icon {
|
||
font-size: 28px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.left-card-placeholder .placeholder-text {
|
||
font-size: 12px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* 左侧白模加载动画样式 */
|
||
.left-card-loading {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #2a2a2a;
|
||
color: white;
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.white-model-spinner {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: #fff;
|
||
animation: white-model-spin 1s ease-in-out infinite;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.white-model-loading-text {
|
||
font-size: 11px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
opacity: 0.9;
|
||
text-align: center;
|
||
}
|
||
|
||
@keyframes white-model-spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 主卡片区域 */
|
||
.ip-card-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
/* 文本输入框弹出层样式 */
|
||
.text-input-overlay {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
margin-top: 16px;
|
||
z-index: 1000;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
color: var(--text-color);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||
min-width: 320px;
|
||
max-width: 480px;
|
||
width: max(320px, 34vw);
|
||
backdrop-filter: blur(10px);
|
||
animation: fadeInDown 0.3s ease-out;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
:global(.dark) .text-input-overlay {
|
||
background: rgba(17, 24, 39, 0.95);
|
||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.4);
|
||
border: 1px solid rgba(139, 92, 246, 0.35);
|
||
}
|
||
|
||
.text-input-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.text-input-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.text-input-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.text-input-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||
background: transparent;
|
||
color: var(--text-color);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.text-input-close:hover {
|
||
background: rgba(139, 92, 246, 0.1);
|
||
border-color: rgba(139, 92, 246, 0.35);
|
||
}
|
||
|
||
.close-icon {
|
||
font-size: 16px;
|
||
color: #6B46C1;
|
||
}
|
||
|
||
.text-input-area {
|
||
width: 100%;
|
||
min-height: 100px;
|
||
padding: 12px;
|
||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||
border-radius: 10px;
|
||
background-color: rgba(139, 92, 246, 0.06);
|
||
color: var(--text-color);
|
||
font-size: 14px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
resize: vertical;
|
||
outline: none;
|
||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||
pointer-events: auto;
|
||
user-select: text;
|
||
-webkit-user-select: text;
|
||
-moz-user-select: text;
|
||
-ms-user-select: text;
|
||
}
|
||
|
||
.text-input-area:focus {
|
||
border-color: var(--el-color-primary);
|
||
background-color: rgba(139, 92, 246, 0.1);
|
||
}
|
||
|
||
.text-input-area::placeholder {
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:global(.dark) .text-input-area {
|
||
background-color: rgba(139, 92, 246, 0.08);
|
||
border-color: rgba(139, 92, 246, 0.35);
|
||
}
|
||
|
||
:global(.dark) .text-input-area::placeholder {
|
||
color: #d1d5db;
|
||
}
|
||
|
||
.text-input-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.text-input-btn {
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||
background-color: rgba(139, 92, 246, 0.1);
|
||
color: var(--text-color);
|
||
font-size: 12px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.text-input-btn:hover {
|
||
background-color: rgba(139, 92, 246, 0.15);
|
||
border-color: rgba(139, 92, 246, 0.35);
|
||
}
|
||
|
||
.text-input-btn.confirm-btn {
|
||
background-color: var(--el-color-primary);
|
||
border-color: var(--el-color-primary);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.text-input-btn.confirm-btn:hover {
|
||
background-color: var(--el-color-primary-light-3);
|
||
border-color: var(--el-color-primary-light-3);
|
||
}
|
||
|
||
.text-input-btn.cancel-btn {
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(139, 92, 246, 0.25);
|
||
}
|
||
|
||
.text-input-btn.cancel-btn:hover {
|
||
background-color: rgba(255, 255, 255, 0.12);
|
||
border-color: rgba(139, 92, 246, 0.35);
|
||
}
|
||
|
||
@keyframes fadeInDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
}
|
||
|
||
.ip-card {
|
||
overflow: hidden;
|
||
background: linear-gradient(135deg, rgba(107, 70, 193, 0.1) 0%, rgba(68, 51, 122, 0.05) 100%);
|
||
/* box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); */
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
position: relative;
|
||
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;
|
||
left: 100%;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
margin-left: 24px;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* 右侧圆形按钮控件 */
|
||
.right-circular-controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
opacity: 0;
|
||
transform: translateX(-20px);
|
||
transition: all 0.3s ease;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 当鼠标悬停在卡片容器上时显示控件 */
|
||
.ip-card-container:hover .right-circular-controls {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* 在 iPad 及更大触控屏上,按下即显示右侧圆形控件 */
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.ip-card-container:active .right-circular-controls,
|
||
.ip-card-container.controls-visible .right-circular-controls {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
pointer-events: auto;
|
||
}
|
||
}
|
||
|
||
/* 非触控设备保持 hover 显示 */
|
||
@media (hover: hover) {
|
||
.ip-card-container:hover .right-circular-controls {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
pointer-events: auto;
|
||
}
|
||
}
|
||
/* 移动端适配:点击卡片后显示功能按钮 */
|
||
@media (max-width: 1024px) {
|
||
/* 点击卡片容器时显示功能按钮 */
|
||
.ip-card-container:active .right-circular-controls,
|
||
.ip-card-container.controls-visible .right-circular-controls {
|
||
opacity: 1;
|
||
transform: translateX(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;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.control-button:hover {
|
||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.25) 0%, rgba(107, 70, 193, 0.2) 100%);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
|
||
}
|
||
|
||
.control-button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.ip-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 32px rgba(167, 139, 250, 0.3);
|
||
border-color: rgba(167, 139, 250, 0.4);
|
||
}
|
||
|
||
.ip-card-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
/* 确保el-image内部图片正确显示 */
|
||
.ip-card-image :deep(img) {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.image-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #2a2a2a;
|
||
color: #9e9e9e;
|
||
}
|
||
|
||
.placeholder-icon {
|
||
font-size: 32px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.placeholder-text {
|
||
font-size: 14px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* 生成状态样式 */
|
||
.generating-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #2a2a2a;
|
||
color: white;
|
||
}
|
||
|
||
.generating-spinner {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: #fff;
|
||
animation: spin 1s ease-in-out infinite;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.generating-text {
|
||
font-size: 14px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 卡片底部信息 */
|
||
.card-info {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 12px;
|
||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||
color: white;
|
||
}
|
||
|
||
.card-name {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.card-series {
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 右侧操作按钮控件 */
|
||
.right-actions-controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
padding-top: 12px;
|
||
margin-top: 12px;
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
transition: all 0.3s ease 0.1s;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 当显示右侧控件时,同时显示操作按钮 */
|
||
.ip-card-container:hover .right-actions-controls {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* more按钮激活状态 */
|
||
.more-btn.active {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
border-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* 操作按钮样式继承control-button,保持一致性 */
|
||
.right-actions-controls .control-button.generate-btn {
|
||
background-color: rgba(239, 68, 68, 0.2);
|
||
border-color: rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.right-actions-controls .control-button.generate-btn:hover {
|
||
background-color: rgba(239, 68, 68, 0.3);
|
||
border-color: rgba(239, 68, 68, 0.4);
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 18px;
|
||
width: 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 14px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* 生成四视图按钮特殊样式 */
|
||
.generate-btn {
|
||
background-color: #ff4d4f;
|
||
border-color: #ff4d4f;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.generate-btn:hover {
|
||
background-color: #ff7875;
|
||
border-color: #ff7875;
|
||
}
|
||
|
||
/* 发型脱离按钮特殊样式 */
|
||
.hair-detach-btn {
|
||
background-color: rgba(147, 51, 234, 0.2);
|
||
border-color: rgba(147, 51, 234, 0.3);
|
||
}
|
||
|
||
.hair-detach-btn:hover {
|
||
background-color: rgba(147, 51, 234, 0.3);
|
||
border-color: rgba(147, 51, 234, 0.4);
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 1024px) {
|
||
.ip-card-container {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
padding: 8px;
|
||
}
|
||
.text-input-overlay {
|
||
width: 92vw;
|
||
max-width: none;
|
||
min-width: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
/* 移动端左侧卡片调整 */
|
||
.left-attachment-card {
|
||
position: static;
|
||
transform: none;
|
||
margin-right: 0;
|
||
margin-bottom: 16px;
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.left-attachment-card.show {
|
||
transform: none;
|
||
}
|
||
|
||
.right-controls-container {
|
||
position: static;
|
||
transform: none;
|
||
margin-left: 0;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.right-circular-controls {
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
opacity: 1;
|
||
transform: none;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.right-actions-controls {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
margin-left: 0;
|
||
opacity: 1;
|
||
transform: none;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.action-button {
|
||
padding: 8px 16px;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style> |