deotalandAi/apps/frontend/src/components/iPandCardLeft/index.vue

2481 lines
58 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>
<aside class="floating-sidebar">
<!-- IP类型选择 -->
<div class="form-section" >
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.ipType') }}
</span>
</div>
<div class="ip-type-grid">
<div
class="ip-type-card"
:class="{ active: ipType === 1 }"
@click="handleIpTypeSelect(1)"
>
<div class="ip-type-label">{{ $t('iPandCardLeft.character') }}</div>
</div>
<div
class="ip-type-card"
:class="{ active: ipType === 2 }"
@click="handleIpTypeSelect(2)"
>
<div class="ip-type-label">{{ $t('iPandCardLeft.animal') }}</div>
</div>
</div>
</div>
<!-- 表情选择 -->
<div class="form-section" v-if="false">
<!-- <label class="section-label">表情选择</label> -->
<div class="expression-info">
<span class="expression-description">
选择一个表情来丰富您的角色形象
</span>
</div>
<div class="expression-grid">
<div
v-for="expression in expressions"
:key="expression.id"
class="expression-card"
:class="{ active: selectedExpression?.id === expression.id }"
@click="handleExpressionSelect(expression)"
>
<div class="expression-preview">
<img :src="expression.imageUrl" :alt="expression.name">
</div>
<div class="expression-name">{{ expression.name }}</div>
<div v-if="selectedExpression?.id === expression.id" class="expression-selected">✓</div>
</div>
</div>
</div>
<!-- 文本提示输入 -->
<div class="form-section">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.textPrompt') }}
</span>
</div>
<div class="prompt-input-container">
<div class="textarea-wrapper">
<textarea
class="prompt-input custom-scrollbar"
:placeholder="$t('iPandCardLeft.placeholder.characterDescription')"
v-model="formData.prompt"
@input="autoResizeTextarea"
ref="textareaRef"
:disabled="isOptimizing"
></textarea>
<!-- 扫描线动画效果 -->
<div v-if="isOptimizing" class="scan-overlay">
<div class="scan-line"></div>
</div>
<!-- <button
class="optimizer-btn"
@click="handleOptimizePrompt"
:disabled="isOptimizing || !prompt.trim()"
>
<span class="btn-icon">🪄</span>
</button> -->
</div>
</div>
</div>
<!-- 参考图片上传 -->
<div class="form-section">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.addReferenceImage') }}
</span>
</div>
<div
class="image-upload-area"
@click="triggerFileUpload"
@dragover.prevent="handleDragOver"
@dragenter.prevent="handleDragEnter"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
:class="{ 'drag-over': isDragOver, 'uploading': isUploading }"
>
<!-- 图片预览 -->
<div v-if="formData.previewImage && !isUploading" class="image-preview">
<img :src="formData.previewImage" alt="Selected image">
<button class="remove-image" @click.stop="removeImage">×</button>
</div>
<!-- 上传进度 -->
<div v-else-if="isUploading" class="upload-progress">
<div class="upload-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<div class="upload-progress-text">loading...</div>
</div>
<!-- 上传提示 -->
<div v-else class="upload-prompt">
<div class="upload-icon">+</div>
<span class="upload-text">{{ $t('iPandCardLeft.uploadOrSelectImage') }}</span>
<span class="drag-text">{{ $t('iPandCardLeft.dragImageHere') }}</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="fileInput"
type="file"
accept="image/*"
@change="handleFileChange"
class="hidden-file-input"
>
</div>
</div>
<!-- 材质选择 -->
<div class="form-section" v-if="false">
<label class="section-label">{{ $t('iPandCardLeft.material.title') }}</label>
<div class="material-selection">
<div class="material-info">
<span class="material-description">
{{ $t('iPandCardLeft.material.description') }}
</span>
</div>
<div class="material-grid">
<div
v-for="material in materials"
:key="material.id"
class="material-card"
:class="{ active: selectedMaterial?.id === material.id }"
@click="handleMaterialSelect(material)"
>
<div class="material-preview">
<img v-if="material.imageUrl" :src="material.imageUrl" :alt="material.name">
<div v-else class="material-placeholder">
<div class="placeholder-icon">{{ material.icon }}</div>
<div class="placeholder-text">{{ material.name }}</div>
</div>
</div>
<div class="material-details">
<div class="material-name">{{ material.name }}</div>
<div class="material-type">{{ material.type }}</div>
</div>
<div v-if="selectedMaterial?.id === material.id" class="material-selected">✓</div>
</div>
</div>
</div>
</div>
<!-- 颜色选择 -->
<div class="form-section" v-if="selectedMaterial">
<label class="section-label">{{ $t('iPandCardLeft.color.title') }}</label>
<div class="color-selection">
<div class="color-info">
<span class="color-description">
{{ $t('iPandCardLeft.color.description', { material: selectedMaterial.name }) }}
</span>
</div>
<div class="color-grid">
<div
v-for="color in availableColors"
:key="color.id"
class="color-card"
:class="{ active: selectedColor?.id === color.id }"
@click="handleColorSelect(color)"
>
<div class="color-preview" :style="{ backgroundColor: color.hex }">
<div v-if="selectedColor?.id === color.id" class="color-selected">✓</div>
</div>
<div class="color-details">
<div class="color-name">{{ color.name }}</div>
<div class="color-hex">{{ color.hex }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 电子模块选择 -->
<div class="form-section" v-if="false">
<label class="section-label">{{ $t('iPandCardLeft.electronicModule') }}</label>
<div class="module-selection">
<div class="module-grid">
<div
v-for="module in electronicModules"
:key="module.id"
class="module-card"
:class="{ active: selectedModule?.id === module.id }"
@click="handleModuleSelect(module)"
>
<div class="module-icon">{{ module.icon }}</div>
<div class="module-info">
<div class="module-name">{{ module.name }}</div>
</div>
<div v-if="selectedModule?.id === module.id" class="module-selected">✓</div>
</div>
</div>
</div>
</div>
<!-- 草图选择系统 -->
<div class="form-section" v-if="selectedModule">
<label class="section-label">{{ $t('iPandCardLeft.sketch.title') }}</label>
<div class="sketch-selection">
<div class="sketch-info">
<span class="sketch-description">
{{ $t('iPandCardLeft.sketch.description', { module: selectedModule.name }) }}
</span>
</div>
<div class="sketch-grid">
<div
v-for="sketch in availableSketches"
:key="sketch.id"
class="sketch-card"
:class="{ active: selectedSketch?.id === sketch.id }"
@click="handleSketchSelect(sketch)"
>
<div class="sketch-preview">
<img v-if="sketch.imageUrl" :src="sketch.imageUrl" :alt="sketch.name">
</div>
<div class="sketch-details">
<div class="sketch-name">{{ sketch.name }}</div>
<div class="sketch-proportions">{{ sketch.proportions }}</div>
</div>
<div v-if="selectedSketch?.id === sketch.id" class="sketch-selected">✓</div>
</div>
</div>
</div>
</div>
<!-- 图片数量选择 -->
<div class="form-section" v-if="false">
<div class="expression-info">
<span class="expression-description">
选择图片数量
</span>
</div>
<div class="quantity-options">
<button
v-for="num in [1, 2, 3, 4]"
:key="num"
class="quantity-option"
:class="{ active: generateCount === num }"
@click="handleQuantitySelect(num)"
>
{{ num }}
</button>
</div>
</div>
<!-- 生成按钮 -->
<div class="generate-section">
<el-button
type="primary"
class="generate-btn"
@click="handleGenerate"
:disabled="isGenerateButtonDisabled"
>
{{ $t('common.generate') }}
</el-button>
</div>
</aside>
</template>
<script setup>
// import lts from '../../assets/sketches/lts.png'
// import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance, reactive } from 'vue';
import { ElMessage } from 'element-plus';
// import cz1 from '../../assets/material/cz1.jpg'
import { FileServer } from '@deotaland/utils';
const filePlug = new FileServer();
// 定义事件
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back', 'updateProjectInfo']);
const props = defineProps({
Info: {
type: Object,
default: () => ({})
}
})
// 获取 i18n 实例
const { proxy } = getCurrentInstance();
const $t = proxy.$t;
const formData = ref({
prompt: '',
previewImage:'',
})
const textareaRef = ref(null);
const fileInput = ref(null); // 文件输入引用
const generateCount = ref(4); // 生成数量默认为1
const isOptimizing = ref(false); // 优化状态
const isDragOver = ref(false); // 拖拽状态
const isUploading = ref(false); // 图片上传状态
// IP类型选择人物/动物),默认人物,并保存到本地
const ipType = ref(1);
const handleIpTypeSelect = (type) => {
ipType.value = type;
// try {
// localStorage.setItem('ipType', type);
// } catch (e) {
// // 忽略本地存储异常
// }
};
// 新增:电子模块和草图选择相关状态
const selectedModule = ref(null); // 选中的电子模块
const selectedSketch = ref(null); // 选中的草图
const selectedMaterial = ref(null); // 选中的材质
const selectedColor = ref(null); // 选中的颜色
const selectedExpression = ref(null); // 选中的表情
const selectedHairColor = ref(null); // 选中的发色
const selectedSkinColor = ref(null); // 选中的肤色
// 表情数据:改为动态遍历 assets/email 目录
const expressions = ref([]);
const init = () => {
}
onMounted(() => {
init()
// loadExpressions();
});
// 颜色数据库 - 基于材质类型提供不同的颜色选项
const colorDatabase = ref({
metal_brushed: [
{ id: 'silver', name: 'Silver', hex: '#C0C0C0', description: 'Classic silver metallic' },
{ id: 'gold', name: 'Gold', hex: '#FFD700', description: 'Luxurious gold finish' },
{ id: 'copper', name: 'Copper', hex: '#B87333', description: 'Warm copper tone' },
{ id: 'titanium', name: 'Titanium', hex: '#878681', description: 'Modern titanium gray' },
{ id: 'bronze', name: 'Bronze', hex: '#CD7F32', description: 'Antique bronze' },
{ id: 'steel', name: 'Steel', hex: '#71797E', description: 'Industrial steel gray' }
],
plastic_matte: [
{ id: 'white', name: 'White', hex: '#FFFFFF', description: 'Pure white' },
{ id: 'black', name: 'Black', hex: '#2C2C2C', description: 'Deep black' },
{ id: 'red', name: 'Red', hex: '#E53E3E', description: 'Vibrant red' },
{ id: 'blue', name: 'Blue', hex: '#3182CE', description: 'Ocean blue' },
{ id: 'green', name: 'Green', hex: '#38A169', description: 'Forest green' },
{ id: 'yellow', name: 'Yellow', hex: '#D69E2E', description: 'Sunny yellow' },
{ id: 'purple', name: 'Purple', hex: '#805AD5', description: 'Royal purple' },
{ id: 'orange', name: 'Orange', hex: '#DD6B20', description: 'Bright orange' }
],
carbon_fiber: [
{ id: 'carbon_black', name: 'Carbon Black', hex: '#1A1A1A', description: 'Classic carbon black' },
{ id: 'carbon_gray', name: 'Carbon Gray', hex: '#4A4A4A', description: 'Lighter carbon gray' },
{ id: 'carbon_blue', name: 'Carbon Blue', hex: '#1E3A8A', description: 'Blue carbon weave' },
{ id: 'carbon_red', name: 'Carbon Red', hex: '#991B1B', description: 'Red carbon accent' }
],
wood_natural: [
{ id: 'oak', name: 'Oak', hex: '#DEB887', description: 'Natural oak wood' },
{ id: 'walnut', name: 'Walnut', hex: '#8B4513', description: 'Rich walnut brown' },
{ id: 'maple', name: 'Maple', hex: '#F5DEB3', description: 'Light maple wood' },
{ id: 'cherry', name: 'Cherry', hex: '#A0522D', description: 'Cherry wood red' },
{ id: 'mahogany', name: 'Mahogany', hex: '#C04000', description: 'Deep mahogany' },
{ id: 'pine', name: 'Pine', hex: '#FFEAA7', description: 'Light pine wood' }
],
ceramic_glossy: [
{ id: 'ceramic_white', name: 'Ceramic White', hex: '#FAFAFA', description: 'Pure ceramic white' },
{ id: 'ceramic_cream', name: 'Cream', hex: '#FFF8DC', description: 'Warm cream ceramic' },
{ id: 'ceramic_blue', name: 'Ceramic Blue', hex: '#4FC3F7', description: 'Sky blue ceramic' },
{ id: 'ceramic_green', name: 'Ceramic Green', hex: '#81C784', description: 'Mint green ceramic' },
{ id: 'ceramic_pink', name: 'Ceramic Pink', hex: '#F48FB1', description: 'Soft pink ceramic' }
],
fabric_soft: [
{ id: 'fabric_beige', name: 'Beige', hex: '#F5F5DC', description: 'Natural beige fabric' },
{ id: 'fabric_gray', name: 'Gray', hex: '#808080', description: 'Neutral gray fabric' },
{ id: 'fabric_navy', name: 'Navy', hex: '#000080', description: 'Deep navy fabric' },
{ id: 'fabric_burgundy', name: 'Burgundy', hex: '#800020', description: 'Rich burgundy fabric' },
{ id: 'fabric_olive', name: 'Olive', hex: '#808000', description: 'Olive green fabric' },
{ id: 'fabric_cream', name: 'Cream', hex: '#FFFDD0', description: 'Soft cream fabric' }
]
});
// 材质数据
// const materials = ref([
// {
// id: 'metal_brushed',
// name: '白毛绒',
// type: 'Metal',
// icon: '🔩',
// imageUrl: cz1, // 可以添加实际的材质图片URL
// description: 'Industrial brushed metal finish',
// properties: {
// reflectivity: 0.8,
// roughness: 0.3,
// metallic: 1.0
// }
// }
// // ,
// // {
// // id: 'plastic_matte',
// // name: '材质2',
// // type: 'Plastic',
// // icon: '🧱',
// // imageUrl: '',
// // description: 'Non-reflective plastic surface',
// // properties: {
// // reflectivity: 0.1,
// // roughness: 0.9,
// // metallic: 0.0
// // }
// // }
// ]);
// 电子模块数据
const electronicModules = ref([
{
id: 'arduino_uno',
name: 'D1 Mini',
icon: '🔌',
category: 'microcontroller',
compatibility: ['basic', 'intermediate', 'advanced']
},
{
id: 'raspberry_pi',
name: 'D1 Plus Pi',
icon: '🔌',
category: 'computer',
compatibility: ['intermediate', 'advanced']
},
]);
// 草图数据 - 基于选中的电子模块动态生成
// const sketchDatabase = ref({
// arduino_uno: [
// {
// id: 'humanoid_basic',
// name: 'Basic Humanoid',
// proportions: 'Head:Body = 1:1',
// pose: 'standing',
// imageUrl: lts,
// description: 'Classic humanoid proportions suitable for Arduino-based robots',
// compatibility: ['arduino_uno', 'servo_motor'],
// headToBodyRatio: '5:5'
// }
// ],
// raspberry_pi: [
// {
// id: 'advanced_humanoid',
// name: 'Advanced Humanoid',
// proportions: 'Head:Body = 1:2.5',
// pose: 'dynamic',
// imageUrl: mk2dy,
// description: 'Sophisticated design leveraging Raspberry Pi capabilities',
// compatibility: ['raspberry_pi', 'camera_module'],
// headToBodyRatio: '6:4'
// },
// ],
// esp32: [
// {
// id: 'iot_character',
// name: 'IoT Character',
// proportions: 'Head:Body = 1:6',
// pose: 'connected',
// imageUrl: '/src/assets/sketches/esp32_compact.svg',
// description: 'IoT-enabled character design for ESP32',
// compatibility: ['esp32']
// }
// ],
// servo_motor: [
// {
// id: 'articulated_figure',
// name: 'Articulated Figure',
// proportions: 'Head:Body = 1:7',
// pose: 'articulated',
// imageUrl: '/src/assets/sketches/arduino_standing.svg',
// description: 'Design emphasizing joint movement and servo integration',
// compatibility: ['servo_motor', 'arduino_uno']
// }
// ],
// led_matrix: [
// {
// id: 'display_character',
// name: 'Display Character',
// proportions: 'Head:Body = 1:6',
// pose: 'expressive',
// imageUrl: '/src/assets/sketches/arduino_sitting.svg',
// description: 'Character design featuring LED matrix display integration',
// compatibility: ['led_matrix', 'arduino_uno']
// }
// ],
// camera_module: [
// {
// id: 'vision_robot',
// name: 'Vision Robot',
// proportions: 'Head:Body = 1:6',
// pose: 'observing',
// imageUrl: '/src/assets/sketches/raspberry_action.svg',
// description: 'Design optimized for camera module and computer vision',
// compatibility: ['camera_module', 'raspberry_pi']
// }
// ]
// });
// 计算属性:基于选中模块获取可用草图
const availableSketches = computed(() => {
if (!selectedModule.value) return [];
return sketchDatabase.value[selectedModule.value.id] || [];
});
// 计算属性:基于选中材质获取可用颜色
const availableColors = computed(() => {
if (!selectedMaterial.value) return [];
return colorDatabase.value[selectedMaterial.value.id] || [];
});
const characterProfile = ref(null); // 字符档案
// 新增:材质选择处理函数
const handleMaterialSelect = (material) => {
// 如果点击的是已选中的材质,则取消选择
if (selectedMaterial.value?.id === material.id) {
selectedMaterial.value = null;
selectedColor.value = null; // 同时重置颜色选择
} else {
selectedMaterial.value = material;
// 重置颜色选择
selectedColor.value = null;
}
};
// 新增:颜色选择处理函数
const handleColorSelect = (color) => {
// 如果点击的是已选中的颜色,则取消选择
if (selectedColor.value?.id === color.id) {
selectedColor.value = null;
} else {
selectedColor.value = color;
}
};
// 新增:电子模块选择处理函数
const handleModuleSelect = (module) => {
// 如果点击的是已选中的模块,则取消选择
if (selectedModule.value?.id === module.id) {
selectedModule.value = null;
selectedSketch.value = null; // 同时重置草图选择
} else {
selectedModule.value = module;
// 重置草图选择
selectedSketch.value = null;
}
};
// 新增:表情选择处理函数
const handleExpressionSelect = (expression) => {
// 如果点击的是已选中的表情,则取消选择
if (selectedExpression.value?.id === expression.id) {
selectedExpression.value = null;
} else {
selectedExpression.value = expression;
}
};
// 新增:草图选择处理函数
const handleSketchSelect = (sketch) => {
// 如果点击的是已选中的草图,则取消选择
if (selectedSketch.value?.id === sketch.id) {
selectedSketch.value = null;
} else {
selectedSketch.value = sketch;
}
};
// 综合输入验证函数
const validateGenerationInputs = () => {
const errors = []
const warnings = []
// 验证参考图像 - 现在必须上传参考图片
if (!(formData.value.previewImage || formData.value.prompt)) {
errors.push($t('common.validation.referenceImageRequired'))
}
return { errors, warnings }
}
// 计算属性:判断生成按钮是否应该禁用
const isGenerateButtonDisabled = computed(() => {
const validation = validateGenerationInputs();
return validation.errors.length > 0;
});
// 错误处理函数
const handleValidationError = (errors) => {
if (errors.length > 0) {
// 显示第一个错误消息(已经通过 i18n 翻译)
const errorMessage = typeof errors[0] === 'string' ? errors[0] : String(errors[0])
ElMessage.error(errorMessage)
}
}
const handleProcessingError = (error, context = '') => {
console.error(`处理错误 ${context}:`, error)
let userMessage = '操作失败'
if (error.message) {
if (error.message.includes('network') || error.message.includes('fetch')) {
userMessage = '网络连接失败,请检查网络后重试'
} else if (error.message.includes('timeout')) {
userMessage = '请求超时,请稍后重试'
} else if (error.message.includes('quota') || error.message.includes('limit')) {
userMessage = '已达到使用限制,请稍后重试'
} else if (error.message.includes('format') || error.message.includes('invalid')) {
userMessage = '输入格式不正确,请检查后重试'
} else {
userMessage = `操作失败: ${error.message}`
}
}
ElMessage.error(userMessage)
// 记录详细错误信息用于调试
if (process.env.NODE_ENV === 'development') {
console.error('详细错误信息:', {
context,
error: error.stack || error,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
})
}
}
// 处理数量选择
const handleQuantitySelect = (num) => {
generateCount.value = num;
};
// 处理生成按钮点击
const handleGenerate = async () => {
console.log('生成数量:', generateCount.value);
// 执行综合验证
const validation = validateGenerationInputs();
if (validation.errors.length > 0) {
handleValidationError(validation.errors);
return;
}
try {
await handleGenerateWithMultipleImages();
// // 检查是否选择了电子模块和草图(新功能)
// if (selectedModule.value && selectedSketch.value) {
// // 使用多图像生成模式
// } else {
// // 使用原有的单图像生成模式
// await handleGenerateWithSingleImage();
// }
} catch (error) {
console.error('生成过程出错:', error);
handleProcessingError(error, '图像生成');
}
};
// 新增:多图像生成处理函数(仅草图参考)
const handleGenerateWithMultipleImages = async () => {
try {
// 构建角色档案,只包含草图信息,不包含电子模块
const profile = {
appearance: formData.value.prompt,
};
// 传递参数给父组件,只包含草图信息
const params = {
profile: profile,
inspirationImage: formData.value.previewImage,
count: generateCount.value,
ipType:ipType.value,
}
emit('generate-requested', params);
} catch (error) {
console.error('创建多图像角色档案失败:', error);
handleProcessingError(error, '多图像角色档案创建');
}
};
// 自适应调整textarea高度
const autoResizeTextarea = () => {
if (textareaRef.value) {
// 重置高度以准确计算内容高度
textareaRef.value.style.height = 'auto';
// 设置高度限制
const minHeight = 40; // 最小高度,适合单行文本
const maxHeight = 300; // 最大高度与CSS保持一致
const scrollHeight = textareaRef.value.scrollHeight;
// 计算实际需要的高度
const newHeight = Math.max(minHeight, Math.min(maxHeight, scrollHeight));
textareaRef.value.style.height = newHeight + 'px';
// 如果内容超过最大高度,确保滚动条可见
if (scrollHeight > maxHeight) {
textareaRef.value.style.overflowY = 'auto';
} else {
textareaRef.value.style.overflowY = 'hidden';
}
}
};
// 触发文件上传
const triggerFileUpload = () => {
fileInput.value?.click();
};
// 处理文件选择
const handleFileChange = async (event) => {
const file = event.target.files?.[0];
if (file) {
isUploading.value = true;
try {
const imgUrl = await filePlug.uploadFile(file);
formData.value.previewImage = imgUrl;
} catch (error) {
console.error('图片上传失败:', error);
ElMessage.error('图片上传失败,请重试');
} finally {
isUploading.value = false;
}
}
};
// 移除已上传的图片
const removeImage = () => {
formData.value.previewImage = '';
if (fileInput.value) {
fileInput.value.value = ''; // 重置文件输入
}
};
// 拖拽事件处理函数
const handleDragOver = (event) => {
event.preventDefault();
event.stopPropagation();
};
const handleDragEnter = (event) => {
event.preventDefault();
event.stopPropagation();
isDragOver.value = true;
};
const handleDragLeave = (event) => {
event.preventDefault();
event.stopPropagation();
// 只有当拖拽离开整个上传区域时才设置为false
if (event.target === event.currentTarget) {
isDragOver.value = false;
}
};
const handleDrop = async (event) => {
event.preventDefault();
event.stopPropagation();
isDragOver.value = false;
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
// 检查文件类型是否为图片
if (file.type.startsWith('image/')) {
console.log('拖拽上传文件:', file);
isUploading.value = true;
try {
const imgUrl = await filePlug.uploadFile(file);
formData.value.previewImage = imgUrl;
} catch (error) {
console.error('拖拽上传失败:', error);
ElMessage.error('图片上传失败,请重试');
} finally {
isUploading.value = false;
}
} else {
ElMessage.error('请选择有效的图片文件');
}
}
};
// 监听 prompt 变化,自动调整 textarea 高度
watch(() => formData.value.prompt, () => {
nextTick(() => {
autoResizeTextarea();
});
});
// 组件挂载后初始化
onMounted(() => {
autoResizeTextarea();
});
</script>
<style scoped>
.import-btn {
width: 100%;
background-color: #381e16 !important;
border-color: #381e16 !important;
color: #ffffff !important;
}
.import-btn:hover,
.import-btn:focus {
filter: brightness(1.1);
}
.prompt-input-container {
position: relative;
width: 100%;
}
.textarea-wrapper {
position: relative;
width: 100%;
}
/* 扫描线动画效果 */
.scan-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 6px;
overflow: hidden;
z-index: 5;
pointer-events: none;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
rgba(99, 102, 241, 0.8) 20%,
rgba(99, 102, 241, 1) 50%,
rgba(99, 102, 241, 0.8) 80%,
transparent 100%);
animation: scan 2s linear infinite;
box-shadow: 0 0 10px 2px rgba(99, 102, 241, 0.5);
}
@keyframes scan {
0% {
top: 0;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
top: calc(100% - 2px);
opacity: 0;
}
}
/* 浮动侧边栏样式 */
.floating-sidebar {
max-width: calc(100vw - 20px); /* 确保不超过视口宽度减去外边距 */
border-radius: 12px;
padding: 20px;
/* box-shadow: 0 4px 20px rgba(107, 70, 193, 0.3); */
border: 1px solid rgba(167, 139, 250, 0.2);
overflow-y: auto; /* 仅在侧边栏内容过多时允许垂直滚动 */
max-height: calc(100vh - 20px); /* 限制侧边栏最大高度 */
position: relative;
background-color: #293140;
min-width: 300px;
}
.floating-sidebar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(167, 139, 250, 0.05) 0%, transparent 50%, rgba(107, 70, 193, 0.05) 100%);
pointer-events: none;
border-radius: 12px;
}
/* 侧边栏头部样式 */
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(167, 139, 250, 0.2);
position: relative;
z-index: 2;
}
.header-left {
display: flex;
align-items: center;
}
/* 返回按钮样式 */
.back-button {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(167, 139, 250, 0.15);
border: 1px solid rgba(167, 139, 250, 0.3);
border-radius: 8px;
color: #ffffff;
font-size: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
font-weight: 500;
}
.back-button:hover {
background: rgba(167, 139, 250, 0.25);
border-color: #A78BFA;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
}
.back-button:active {
transform: translateY(0);
transition: all 0.1s ease;
}
.back-button svg {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.back-button:hover svg {
transform: translateX(-2px);
}
.back-text {
font-weight: 500;
letter-spacing: 0.5px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.back-button {
padding: 6px 10px;
font-size: 13px;
}
.back-button svg {
width: 18px;
height: 18px;
}
.back-text {
display: none; /* 移动端隐藏文字,只显示图标 */
}
}
/* IP类型图片选择样式 */
.ip-type-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.ip-type-card {
position: relative;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
background-color: var(--bg-color, rgba(255, 255, 255, 0.05));
height: 34px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.ip-type-card:hover {
border-color: var(--border-hover-color, rgba(255, 255, 255, 0.2));
transform: translateY(-2px);
}
.ip-type-card.active {
background-color: rgba(167, 139, 250, 0.2);
border-color: #A78BFA;
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.4);
}
.ip-type-label {
color: var(--text-color, #fff);
font-size: 16px;
font-weight: 500;
}
/* 适应亮色主题 */
:root {
--border-color: rgba(0, 0, 0, 0.1);
--bg-color: rgba(0, 0, 0, 0.05);
--border-hover-color: rgba(0, 0, 0, 0.2);
--text-color: #333;
}
/* 深色主题特定样式 */
.dark-theme {
--border-color: rgba(255, 255, 255, 0.1);
--bg-color: rgba(255, 255, 255, 0.05);
--border-hover-color: rgba(255, 255, 255, 0.2);
--text-color: #fff;
}
/* 确保当前主题下的样式正确应用 */
.ip-type-label {
color: var(--el-text-color-regular);
}
.ip-type-card {
border-color: var(--el-border-color-light);
background-color: var(--el-bg-color-light);
}
.ip-type-card:hover {
border-color: var(--el-border-color);
}
.ip-type-card.active:hover {
border-color: #A78BFA;
}
.logo-container {
display: flex;
align-items: center;
gap: 8px;
}
.logo {
font-size: 24px;
}
.logo-text {
font-size: 18px;
font-weight: bold;
}
.credits {
display: flex;
align-items: center;
gap: 6px;
}
.credits-icon {
font-size: 16px;
}
.credits-amount {
font-size: 16px;
font-weight: bold;
color: #ffcc00;
}
.credits-label {
font-size: 12px;
opacity: 0.8;
}
/* 表单区域样式 */
.form-section {
margin-bottom: 20px;
}
.section-label {
display: block;
font-size: 14px;
margin-bottom: 8px;
margin-top: 16px;
padding: 6px 10px;
background: linear-gradient(135deg, rgba(167, 139, 250, 0.1) 0%, rgba(107, 70, 193, 0.05) 100%);
border-radius: 6px;
border-left: 3px solid #A78BFA;
font-weight: 500;
color: #E9D5FF;
}
/* 选择框样式 */
.model-select {
width: 100%;
padding: 10px 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #ffffff;
font-size: 14px;
cursor: pointer;
appearance: none;
position: relative;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-5 h-5'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19.5 8.25l-7.5 7.5-7.5-7.5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
}
.model-select:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.3);
}
/* 图片上传区域样式 */
.image-upload-area {
position: relative;
width: 100%;
height: 180px;
border: 2px dashed rgba(221, 221, 221, 0.5);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
margin-bottom: 12px;
}
.image-upload-area:hover {
border-color: #675b5c;
background-color: rgba(99, 102, 241, 0.05);
}
/* 拖拽状态样式 */
.image-upload-area.drag-over {
border-color: #6366f1;
background-color: rgba(99, 102, 241, 0.1);
transform: scale(1.02);
box-shadow: 0 0 15px rgba(99, 102, 241, 0.3);
}
/* 上传状态样式 */
.image-upload-area.uploading {
border-color: #6B46C1;
background-color: rgba(107, 70, 193, 0.05);
pointer-events: none;
}
/* 上传进度样式 */
.upload-progress {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 16px;
}
/* 旋转加载动画 */
.upload-spinner {
position: relative;
width: 48px;
height: 48px;
}
.spinner-ring {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-top: 3px solid #6B46C1;
border-radius: 50%;
animation: spin 1.2s linear infinite;
}
.spinner-ring:nth-child(2) {
width: 36px;
height: 36px;
top: 6px;
left: 6px;
border-top-color: #A78BFA;
animation-duration: 1s;
animation-direction: reverse;
}
.spinner-ring:nth-child(3) {
width: 24px;
height: 24px;
top: 12px;
left: 12px;
border-top-color: #C4B5FD;
animation-duration: 0.8s;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.upload-progress-text {
font-size: 14px;
color: #6B46C1;
font-weight: 500;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
.upload-prompt {
text-align: center;
color: #666;
}
.upload-icon {
font-size: 32px;
color: #999;
margin-bottom: 8px;
display: block;
}
.upload-text {
font-size: 14px;
color: #666;
}
.drag-text {
font-size: 14px;
color: #6366f1;
margin-top: 8px;
display: block;
opacity: 0.8;
}
/* 隐藏文件输入 */
.hidden-file-input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
cursor: pointer;
}
/* 图片预览样式 */
.image-preview {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 移除图片按钮 */
.remove-image {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
font-size: 18px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.remove-image:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.upload-icon {
font-size: 32px;
opacity: 0.6;
margin-bottom: 8px;
}
.upload-text {
font-size: 14px;
opacity: 0.8;
}
/* 按钮样式 */
.generate-worldview-btn {
width: 100%;
padding: 10px 16px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: #ffffff;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
}
.generate-worldview-btn:hover {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
/* 文本输入区域 */
.prompt-input {
width: 100%;
min-height: 40px; /* 最小高度,适合单行文本 */
max-height: 300px; /* 设置最大高度 */
padding: 12px 12px 40px 12px; /* 增加底部内边距,为按钮留出空间 */
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #ffffff;
font-size: 14px;
resize: none; /* 禁用手动调整大小 */
overflow-y: hidden; /* 初始隐藏滚动条内容多时通过JS控制 */
margin-bottom: 0; /* 移除底部边距,因为按钮现在是绝对定位 */
line-height: 1.5; /* 设置行高,便于计算高度 */
transition: border-color 0.3s ease;
}
.prompt-input:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.prompt-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.prompt-input:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.3);
}
.textarea-wrapper:has(.prompt-input:disabled) .prompt-input {
border-color: rgba(99, 102, 241, 0.5);
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
}
.optimizer-btn {
position: absolute;
bottom: 10px;
right: 8px;
padding: 6px 12px;
background-color: #6366f1;
border: none;
border-radius: 6px;
color: white;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background-color 0.2s;
z-index: 10;
}
.optimizer-btn:hover:not(:disabled) {
background-color: #4f46e5;
}
.optimizer-btn:disabled {
background-color: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
}
/* 风格选项 */
.style-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.style-option {
padding: 10px 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #ffffff;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
}
.style-option:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.style-option.active {
background-color: rgba(255, 204, 0, 0.2);
border-color: #ffcc00;
color: #ffcc00;
}
/* 数量选项 */
.quantity-options {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.quantity-option {
padding: 10px 12px;
background-color: rgba(167, 139, 250, 0.1);
border: 1px solid rgba(167, 139, 250, 0.2);
border-radius: 6px;
color: #A78BFA;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.quantity-option:hover {
background-color: rgba(167, 139, 250, 0.2);
border-color: rgba(167, 139, 250, 0.4);
}
.quantity-option.active {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.3) 0%, rgba(124, 58, 237, 0.3) 100%);
border-color: #A78BFA;
color: #A78BFA;
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.3);
}
/* 生成按钮区域 */
.generate-section {
margin-top: 32px;
}
/* 横向控制布局 */
.generate-controls {
display: flex;
align-items: center;
}
/* 生成按钮样式 */
.generate-btn {
width: 100%;
--el-button-bg-color: #7C3AED;
--el-button-hover-bg-color: #8B5CF6;
--el-button-active-bg-color: #6B46C1;
--el-button-font-weight: 500;
--el-button-font-size: 16px;
--el-button-border-radius: 8px;
--el-button-border-color: #7C3AED;
--el-button--base-line-height: 2.5;
height: 40px;
}
.generate-3d-btn {
flex: 1;
--el-button-bg-color: #10b981;
--el-button-hover-bg-color: #059669;
--el-button-active-bg-color: #047857;
--el-button-font-weight: 500;
--el-button-font-size: 16px;
--el-button-border-radius: 8px;
--el-button-border-color: #10b981;
--el-button--base-line-height: 2.5;
height: 40px;
}
/* 下拉菜单项样式优化 */
:deep(.el-dropdown-menu) {
--el-dropdown-menu-bg-color: #fff;
--el-dropdown-menu-border-color: #e0e0e0;
--el-dropdown-menu-item-hover-bg-color: #fff2f0;
--el-dropdown-menu-item-hover-color: #ff4d4f;
--el-dropdown-menu-item-active-color: #ff4d4f;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
:deep(.el-dropdown-menu__item) {
padding: 10px 20px;
transition: all 0.2s ease;
}
:deep(.el-dropdown-menu__item:hover) {
transform: translateX(4px);
}
.btn-icon {
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.floating-sidebar {
width: 100%;
max-width: none;
margin: 0;
padding: 15px;
max-height: calc(100vh - 10px);
}
}
/* 表情选择样式 */
.expression-selection {
width: 100%;
}
.expression-info {
margin-bottom: 12px;
padding: 8px 12px;
background: linear-gradient(135deg, rgba(167, 139, 250, 0.1) 0%, rgba(107, 70, 193, 0.05) 100%);
border-radius: 6px;
border-left: 3px solid #A78BFA;
}
.expression-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.expression-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.expression-card {
position: relative;
padding: 8px;
background: linear-gradient(135deg, rgba(167, 139, 250, 0.08) 0%, rgba(107, 70, 193, 0.05) 100%);
border: 1px solid rgba(167, 139, 250, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.expression-card:hover {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.15) 0%, rgba(107, 70, 193, 0.1) 100%);
border-color: rgba(167, 139, 250, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.2);
}
.expression-card.active {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.25) 0%, rgba(124, 58, 237, 0.2) 100%);
border-color: #A78BFA;
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.4);
}
.expression-preview {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
}
.expression-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.expression-name {
font-size: 11px;
color: #ffffff;
margin-bottom: 2px;
}
.expression-selected {
position: absolute;
top: 4px;
right: 4px;
width: 16px;
height: 16px;
background: linear-gradient(135deg, #A78BFA, #7C3AED);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(167, 139, 250, 0.3);
}
/* 发色选择样式 */
.hair-color-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: rgba(59, 130, 246, 0.1);
border-radius: 6px;
border-left: 3px solid #3b82f6;
}
.hair-color-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.hair-color-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.hair-color-card {
position: relative;
padding: 8px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.hair-color-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.hair-color-card.active {
background-color: rgba(59, 130, 246, 0.2);
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
.hair-color-preview {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
margin-bottom: 6px;
position: relative;
}
.hair-color-selected {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.9);
color: #3b82f6;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
}
.hair-color-name {
font-size: 11px;
color: #ffffff;
margin-bottom: 2px;
}
/* 肤色选择样式 */
.skin-color-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: rgba(236, 72, 153, 0.1);
border-radius: 6px;
border-left: 3px solid #ec4899;
}
.skin-color-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.skin-color-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.skin-color-card {
position: relative;
padding: 8px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.skin-color-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.skin-color-card.active {
background-color: rgba(236, 72, 153, 0.2);
border-color: #ec4899;
box-shadow: 0 0 0 2px rgba(236, 72, 153, 0.3);
}
.skin-color-preview {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
margin-bottom: 6px;
position: relative;
}
.skin-color-selected {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.9);
color: #ec4899;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
}
.skin-color-name {
font-size: 11px;
color: #ffffff;
margin-bottom: 2px;
}
/* 响应式调整 */
@media (max-width: 480px) {
.expression-grid, .hair-color-grid, .skin-color-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* 材质选择样式 */
.material-selection {
width: 100%;
}
.material-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: rgba(168, 85, 247, 0.1);
border-radius: 6px;
border-left: 3px solid #a855f7;
}
.material-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.material-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.material-card {
position: relative;
padding: 10px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-height: 100px;
}
.material-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.material-card.active {
background-color: rgba(168, 85, 247, 0.2);
border-color: #a855f7;
box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.3);
}
.material-preview {
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 6px;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.material-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.material-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
}
.material-placeholder .placeholder-icon {
font-size: 18px;
margin-bottom: 2px;
opacity: 0.8;
}
.material-placeholder .placeholder-text {
font-size: 8px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.2;
}
.material-details {
flex: 1;
min-width: 0;
}
.material-name {
font-size: 12px;
font-weight: 500;
color: #ffffff;
margin-bottom: 2px;
}
.material-type {
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
}
.material-selected {
position: absolute;
top: 6px;
right: 6px;
width: 18px;
height: 18px;
background-color: #a855f7;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: bold;
}
/* 颜色选择样式 */
.color-selection {
width: 100%;
}
.color-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: rgba(34, 197, 94, 0.1);
border-radius: 6px;
}
.color-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.color-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.color-card {
position: relative;
padding: 8px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-width: 80px;
}
.color-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.color-card.active {
background-color: rgba(34, 197, 94, 0.2);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
.color-preview {
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
margin-bottom: 6px;
border: 2px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.color-selected {
color: white;
font-size: 14px;
font-weight: bold;
text-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
}
.color-details {
flex: 1;
min-width: 0;
}
.color-name {
font-size: 11px;
font-weight: 500;
color: #ffffff;
margin-bottom: 2px;
line-height: 1.2;
}
.color-hex {
font-size: 9px;
color: rgba(255, 255, 255, 0.6);
font-family: 'Courier New', monospace;
}
/* 电子模块选择样式 */
.module-selection {
width: 100%;
}
.module-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.module-card {
position: relative;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.module-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.module-card.active {
background-color: rgba(99, 102, 241, 0.2);
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}
.module-icon {
font-size: 24px;
flex-shrink: 0;
}
.module-info {
flex: 1;
min-width: 0;
}
.module-name {
font-size: 13px;
font-weight: 500;
color: #ffffff;
margin-bottom: 2px;
}
.module-size {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
}
.module-selected {
position: absolute;
top: 6px;
right: 6px;
width: 18px;
height: 18px;
background-color: #6366f1;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
/* 草图选择样式 */
.sketch-selection {
width: 100%;
}
.sketch-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: rgba(99, 102, 241, 0.1);
border-radius: 6px;
border-left: 3px solid #6366f1;
}
.sketch-description {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.sketch-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.sketch-card {
position: relative;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
gap: 12px;
}
.sketch-card:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.sketch-card.active {
background-color: rgba(34, 197, 94, 0.2);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
.sketch-preview {
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 6px;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.sketch-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.sketch-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
}
.placeholder-icon {
font-size: 20px;
margin-bottom: 4px;
opacity: 0.7;
}
.placeholder-text {
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.2;
}
.sketch-details {
flex: 1;
min-width: 0;
}
.sketch-name {
font-size: 13px;
font-weight: 500;
color: #ffffff;
margin-bottom: 4px;
}
.sketch-proportions {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 2px;
}
.sketch-selected {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
/* 响应式调整 */
@media (max-width: 480px) {
.module-grid {
grid-template-columns: 1fr;
}
.module-card {
padding: 10px;
}
.sketch-card {
flex-direction: column;
text-align: center;
}
.sketch-preview {
width: 50px;
height: 50px;
margin: 0 auto;
}
.color-grid {
grid-template-columns: repeat(3, 1fr);
}
.color-card {
min-height: 70px;
padding: 6px;
}
.color-preview {
width: 28px;
height: 28px;
}
}
/* 亮色主题样式 */
:global(:root:not(.dark)) {
.floating-sidebar {
background-color: #FFFFFF;
border-right: 1px solid #E5E7EB;
color: #374151;
}
.sidebar-header {
background-color: #F9FAFB;
border-bottom: 1px solid #E5E7EB;
}
.back-button {
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
color: #374151;
}
.back-button:hover {
background-color: #F3F4F6;
border-color: #D1D5DB;
}
.ip-type-card {
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
color: #374151;
}
.ip-type-card:hover {
background-color: #F9FAFB;
border-color: #D1D5DB;
}
.ip-type-card.active {
background-color: rgba(99, 102, 241, 0.1);
border-color: #6366f1;
}
.image-upload-area {
background-color: #FFFFFF;
border: 2px dashed #D1D5DB;
color: #6B7280;
}
.image-upload-area:hover {
background-color: #F9FAFB;
border-color: #9CA3AF;
}
.image-upload-area.drag-over {
background-color: #F3F4F6;
border-color: #6366f1;
}
.remove-image {
background-color: rgba(0, 0, 0, 0.6);
color: white;
}
.remove-image:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.prompt-input {
background-color: #FFFFFF;
border: 1px solid #D1D5DB;
color: #374151;
}
.prompt-input:focus {
border-color: #6366f1;
}
.prompt-input::placeholder {
color: #9CA3AF;
}
.optimizer-btn {
background-color: #6366f1;
color: white;
}
.optimizer-btn:hover:not(:disabled) {
background-color: #4f46e5;
}
.style-option {
background-color: #FFFFFF;
border: 1px solid #D1D5DB;
color: #374151;
}
.style-option:hover {
background-color: #F9FAFB;
border-color: #9CA3AF;
}
.style-option.active {
background-color: rgba(255, 204, 0, 0.1);
border-color: #ffcc00;
color: #b45309;
}
.quantity-option {
background-color: rgba(167, 139, 250, 0.05);
border: 1px solid rgba(167, 139, 250, 0.2);
color: #6B46C1;
}
.quantity-option:hover {
background-color: rgba(167, 139, 250, 0.1);
border-color: rgba(167, 139, 250, 0.3);
}
.quantity-option.active {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.2) 0%, rgba(124, 58, 237, 0.2) 100%);
border-color: #6B46C1;
color: #6B46C1;
}
.generate-btn {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
}
.generate-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
}
.generate-btn:disabled {
background-color: #9CA3AF;
}
.expression-card {
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
color: #374151;
}
.expression-card:hover {
background-color: #F9FAFB;
border-color: #D1D5DB;
}
.expression-card.active {
background-color: rgba(34, 197, 94, 0.1);
}
.hair-color-card,
.skin-color-card,
.material-card,
.color-card,
.module-card,
.sketch-card {
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
color: #374151;
}
.hair-color-card:hover,
.skin-color-card:hover,
.material-card:hover,
.color-card:hover,
.module-card:hover,
.sketch-card:hover {
background-color: #F9FAFB;
border-color: #D1D5DB;
}
.hair-color-card.active,
.skin-color-card.active,
.material-card.active,
.color-card.active,
.module-card.active,
.sketch-card.active {
background-color: rgba(34, 197, 94, 0.1);
}
.color-preview,
.hair-color-preview,
.skin-color-preview {
border: 1px solid rgba(0, 0, 0, 0.1);
}
.material-preview,
.module-preview {
background-color: #F9FAFB;
}
.upload-icon,
.placeholder-icon {
opacity: 0.6;
color: #6B7280;
}
.upload-text,
.drag-text,
.expression-name,
.hair-color-name,
.skin-color-name,
.material-name,
.material-type,
.color-name,
.module-name,
.module-size,
.sketch-name,
.sketch-proportions,
.placeholder-text {
color: #374151;
}
.expression-description,
.hair-color-description,
.skin-color-description,
.material-description {
color: #6B7280;
}
.scan-overlay {
background-color: rgba(255, 255, 255, 0.8);
}
.scan-line {
background: linear-gradient(90deg, transparent, #6366f1, transparent);
}
.section-label,
.form-section label {
/* color: #374151; */
}
.generate-worldview-btn {
background-color: #FFFFFF;
border: 1px solid #D1D5DB;
color: #374151;
}
.generate-worldview-btn:hover {
background-color: #F9FAFB;
border-color: #9CA3AF;
}
.sketch-info {
background-color: rgba(99, 102, 241, 0.05);
border-left: 3px solid #6366f1;
}
.sketch-description {
color: #6B7280;
}
}
</style>