deotalandAi/apps/frontend/src/components/IPCard/index copy.vue

1055 lines
28 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="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>