797 lines
19 KiB
Vue
797 lines
19 KiB
Vue
<template>
|
||
<div
|
||
class="ip-card-container"
|
||
@mouseenter="handleMouseEnter"
|
||
@mouseleave="handleMouseLeave"
|
||
>
|
||
<!-- 主卡片区域 - 替换为3D模型展示容器 -->
|
||
<div class="ip-card-wrapper" >
|
||
<div
|
||
class="ip-card"
|
||
:class="{ 'loading': isGenerating, 'initializing': isInitializing }"
|
||
:style="cardStyle"
|
||
>
|
||
<!-- 如果没有模型URL但有图片URL,显示图片 -->
|
||
<div v-if="!currentModelUrl && props.cardData.imageUrl" class="image-preview">
|
||
<img
|
||
:src="props.cardData.imageUrl"
|
||
:alt="altText"
|
||
class="preview-image"
|
||
@load="handleImageLoad"
|
||
@error="handleImageError"
|
||
/>
|
||
<!-- 生成模型按钮 -->
|
||
<button
|
||
v-if="!isGenerating"
|
||
class="generate-model-btn"
|
||
@click.stop="handleGenerateModel"
|
||
>
|
||
{{ t('modelCard.generateModelButton') }}
|
||
</button>
|
||
<!-- 生成进度指示器 -->
|
||
<div v-else class="generating-indicator">
|
||
<div class="spinner"></div>
|
||
<span>{{ t('modelCard.generatingIndicator') }}</span>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }">
|
||
|
||
</div>
|
||
<div
|
||
class="progress-background-text"
|
||
>
|
||
{{ t('modelCard.progressText', { percentage: Math.round(progressPercentage) }) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 否则显示3D模型 -->
|
||
<div style="position: relative;"
|
||
v-else>
|
||
<div style="position: absolute;width: 100%;height: 100%;z-index: 2;"></div>
|
||
<ThreeModelViewer
|
||
ref="threeModelViewer"
|
||
:model-url="currentModelUrl"
|
||
:container-width="cardWidth"
|
||
:show-border="false"
|
||
:border-radius="0"
|
||
:background-color="backgroundColor"
|
||
:enable-controls="!props.clickDisabled"
|
||
:auto-rotate="isRotating"
|
||
:rotation-speed="0.005"
|
||
:camera-distance="2.5"
|
||
/>
|
||
<!-- 放大镜按钮 - 右上角 -->
|
||
<!-- <button class="zoom-button" @click="handleCardClick" title="查看详情">
|
||
<span class="btn-icon">🔍</span>
|
||
</button> -->
|
||
</div>
|
||
</div>
|
||
<!-- 右侧控件区域 -->
|
||
<div class="right-controls-container" @click.stop>
|
||
<!-- 右侧圆形按钮控件 -->
|
||
<div class="right-circular-controls" v-if="generatedModelUrl">
|
||
<button class="control-button rotate-btn" title="detail" @click="handleCardClick">
|
||
<span class="btn-icon">🔍</span>
|
||
</button>
|
||
<!-- <button class="control-button export-btn" @click="exportModel">
|
||
<span class="btn-icon">📥</span>
|
||
</button>
|
||
-->
|
||
<!-- <button class="control-button reset-btn" @click="resetView">
|
||
<span class="btn-icon">🎯</span>
|
||
</button> -->
|
||
<!-- <button class="control-button more-btn" @click="toggleActions">
|
||
<span class="btn-icon">•••</span>
|
||
</button> -->
|
||
<!-- <div class="right-actions-controls" v-if="showRightControls">
|
||
<button class="control-button" @click="exportAsGLB" title="导出GLB格式">
|
||
<span class="btn-icon">📦</span>
|
||
</button>
|
||
<button class="control-button" @click="exportAsOBJ" title="导出OBJ格式">
|
||
<span class="btn-icon">📄</span>
|
||
</button>
|
||
<button class="control-button" @click="exportAsSTL" title="导出STL格式">
|
||
<span class="btn-icon">🔷</span>
|
||
</button>
|
||
<button class="control-button" @click="changeModelColor" title="改变模型颜色">
|
||
<span class="btn-icon">🎨</span>
|
||
</button>
|
||
</div> -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, onMounted } from 'vue';
|
||
import ThreeModelViewer from '../ThreeModelViewer/index.vue';
|
||
import { MeshyServer } from '@deotaland/utils';
|
||
import { useI18n } from 'vue-i18n';
|
||
|
||
// 初始化国际化函数
|
||
const { t } = useI18n();
|
||
|
||
// 定义组件事件
|
||
const emit = defineEmits(['clickModel','refineModel','save-project','delete']);
|
||
const Meshy = new MeshyServer();
|
||
// 控制右侧按钮显示状态
|
||
const showRightControls = ref(false);
|
||
const threeModelViewer = ref(null);
|
||
const isRotating = ref(true);
|
||
const isGenerating = ref(false);// 控制生成模型按钮和进度指示器的显示
|
||
const isInitializing = ref(true); // 控制初始化状态
|
||
const progressPercentage = ref(0); // 生成进度百分比
|
||
// 处理卡片点击事件 - 只通过放大镜按钮触发
|
||
const handleCardClick = () => {
|
||
// 创建包含所有必要信息的对象,包括正确的模型URL
|
||
const modelData = {
|
||
modelUrl: currentModelUrl.value,
|
||
imageUrl: props.cardData.imageUrl,
|
||
projectId:props.projectId,
|
||
altText: props.altText,
|
||
cardId: props.cardId,
|
||
cardWidth: props.cardWidth
|
||
};
|
||
emit('clickModel', modelData);
|
||
};
|
||
// 处理生成模型按钮点击
|
||
const handleGenerateModel = async () => {
|
||
isGenerating.value = true;
|
||
progressPercentage.value = 10;
|
||
let result;
|
||
if(props.cardData.taskId){
|
||
result = props.cardData.taskId;
|
||
TaskStatus(result,props.cardData.resultTask);
|
||
return
|
||
}
|
||
Meshy.createModelTask({
|
||
project_id: props.cardData.project_id,
|
||
image_url: props.cardData.imageUrl,
|
||
},(result,resultTask)=>{
|
||
if(result){
|
||
emit('save-project',{
|
||
...props.cardData,
|
||
resultTask:resultTask,
|
||
taskId:result
|
||
});
|
||
TaskStatus(result,resultTask);
|
||
}
|
||
},(error)=>{
|
||
emit('delete');
|
||
},{})
|
||
};
|
||
const TaskStatus = (result,resultTask)=>{
|
||
Meshy.getModelTaskStatus({
|
||
result:result,
|
||
resultTask:resultTask,
|
||
},(modelUrl)=>{
|
||
if(modelUrl){
|
||
// 模型生成完成
|
||
generatedModelUrl.value = modelUrl;
|
||
isGenerating.value = false;
|
||
progressPercentage.value = 100;
|
||
emit('save-project',{
|
||
...props.cardData,
|
||
modelUrl:modelUrl,
|
||
taskId:result,
|
||
status:'success'
|
||
});
|
||
}
|
||
},(error)=>{
|
||
console.error('模型生成失败:', error);
|
||
emit('delete');
|
||
},(progress)=>{
|
||
if (progress !== undefined) {
|
||
progressPercentage.value = progress;
|
||
}
|
||
})
|
||
}
|
||
// 组件挂载时,如果有图片URL但没有模型URL,自动生成模型
|
||
onMounted(() => {
|
||
// handleGenerateModel();
|
||
// return
|
||
switch (props.cardData.status) {
|
||
case 'loading':
|
||
handleGenerateModel();
|
||
break;
|
||
case 'success':
|
||
generatedModelUrl.value = props.cardData.modelUrl;
|
||
isGenerating.value = false;
|
||
progressPercentage.value = 100;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
});
|
||
|
||
// 处理鼠标移入事件(保留用于其他可能的交互)
|
||
const handleMouseEnter = () => {
|
||
// 现在主要通过CSS控制显示,这里可以添加其他交互逻辑
|
||
};
|
||
|
||
// 处理鼠标移出事件
|
||
const handleMouseLeave = () => {
|
||
// 鼠标离开时隐藏展开的操作按钮
|
||
showRightControls.value = false;
|
||
};
|
||
// 处理图片加载完成,获取图片实际比例
|
||
const handleImageLoad = (event) => {
|
||
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)})`);
|
||
}
|
||
};
|
||
|
||
// 处理图片加载错误
|
||
const handleImageError = (event) => {
|
||
console.warn('图片加载失败:', event.target.src);
|
||
};
|
||
// 定义组件属性
|
||
const props = defineProps({
|
||
projectId:{
|
||
type: String,
|
||
default: ''
|
||
},
|
||
cardData: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 背景颜色
|
||
backgroundColor: {
|
||
type: String,
|
||
default: '#1a1a1a'
|
||
},
|
||
// 卡片宽度(默认200px)
|
||
cardWidth: {
|
||
type: [Number, String], // 允许字符串或数字类型
|
||
default: 200
|
||
},
|
||
// 是否显示边框
|
||
showBorder: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 边框圆角
|
||
borderRadius: {
|
||
type: Number,
|
||
default: 8
|
||
},
|
||
// 是否禁用点击(用于拖动时)
|
||
clickDisabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
});
|
||
|
||
// 内部状态:生成的模型URL
|
||
const generatedModelUrl = ref('');
|
||
// 图片比例
|
||
const imageAspectRatio = ref(16 / 9); // 默认比例
|
||
|
||
// 计算属性:决定使用哪个模型URL
|
||
const currentModelUrl = computed(() => {
|
||
return generatedModelUrl.value || props.cardData.modelUrl;
|
||
});
|
||
|
||
|
||
|
||
// 计算卡片样式,根据是否有模型使用固定9:16比例或图片实际比例
|
||
const cardStyle = computed(() => {
|
||
const width = props.cardWidth;
|
||
// 如果有模型URL,使用固定9:16比例,否则使用图片实际比例
|
||
const height = currentModelUrl.value
|
||
? (width * 16) / 9 // 固定9:16比例
|
||
: width / imageAspectRatio.value; // 使用图片实际比例
|
||
|
||
return {
|
||
width: `${width}px`,
|
||
height: `${height}px`,
|
||
borderRadius: `${props.borderRadius}px`,
|
||
border: props.showBorder ? '1px solid #e0e0e0' : 'none'
|
||
};
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.ip-card-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
padding: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
/* 初始化状态 - 与IPCard组件保持一致 */
|
||
.ip-card.initializing {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 加载状态 - 添加脉冲效果 */
|
||
.ip-card.loading {
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
|
||
70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
|
||
}
|
||
|
||
/* 主卡片区域 */
|
||
.ip-card-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.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:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 32px rgba(167, 139, 250, 0.3);
|
||
border-color: rgba(167, 139, 250, 0.4);
|
||
}
|
||
|
||
/* 图片预览区域 */
|
||
.image-preview {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
border-radius: 8px;
|
||
background-color: #1a1a1a;
|
||
}
|
||
|
||
.preview-image {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* 生成模型按钮 */
|
||
.generate-model-btn {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 12px 24px;
|
||
background: linear-gradient(135deg, #7C3AED 0%, #6B46C1 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
z-index: 10;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.generate-model-btn:hover {
|
||
transform: translateX(-50%) translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
|
||
.generate-model-btn:active {
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
|
||
/* 生成进度指示器 */
|
||
.generating-indicator {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 15px 25px;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
border-radius: 20px;
|
||
color: white;
|
||
font-size: 14px;
|
||
z-index: 10;
|
||
min-width: 200px;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
/* 进度条样式 */
|
||
.progress-bar {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 24px;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.2));
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
margin-top: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.progress-fill {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4);
|
||
border-radius: 12px;
|
||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
padding-right: 8px;
|
||
box-shadow:
|
||
0 2px 8px rgba(59, 130, 246, 0.4),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||
animation: shimmer 2s infinite;
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% {
|
||
left: -100%;
|
||
}
|
||
100% {
|
||
left: 100%;
|
||
}
|
||
}
|
||
|
||
.progress-text {
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||
white-space: nowrap;
|
||
opacity: 1;
|
||
transition: opacity 0.3s ease;
|
||
z-index: 2;
|
||
position: relative;
|
||
}
|
||
|
||
.progress-text.low-progress {
|
||
justify-self: flex-start;
|
||
padding-left: 8px;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.progress-background-text {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
z-index: 1;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.spinner {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: white;
|
||
animation: spin 1s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 放大镜按钮 - 右上角 */
|
||
.zoom-button {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 12px;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
opacity: 0;
|
||
transform: scale(0.8);
|
||
z-index: 5;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
/* 鼠标悬停时显示放大镜按钮 */
|
||
.ip-card-container:hover .zoom-button {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
|
||
.zoom-button:hover {
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.zoom-button:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.zoom-button .btn-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
/* 右侧控件容器 - 使用绝对定位避免布局重排 */
|
||
.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;
|
||
}
|
||
|
||
.control-button {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.control-button:hover {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.control-button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
/* 卡片底部信息 */
|
||
.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.export-btn {
|
||
background-color: rgba(34, 197, 94, 0.2);
|
||
border-color: rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
.control-button.export-btn:hover {
|
||
background-color: rgba(34, 197, 94, 0.3);
|
||
border-color: rgba(34, 197, 94, 0.4);
|
||
}
|
||
|
||
/* 旋转按钮特殊样式 */
|
||
.control-button.rotate-btn {
|
||
background-color: rgba(59, 130, 246, 0.2);
|
||
border-color: rgba(59, 130, 246, 0.3);
|
||
}
|
||
|
||
.control-button.rotate-btn:hover {
|
||
background-color: rgba(59, 130, 246, 0.3);
|
||
border-color: rgba(59, 130, 246, 0.4);
|
||
}
|
||
|
||
/* 重置按钮特殊样式 */
|
||
.control-button.reset-btn {
|
||
background-color: rgba(249, 115, 22, 0.2);
|
||
border-color: rgba(249, 115, 22, 0.3);
|
||
}
|
||
|
||
.control-button.reset-btn:hover {
|
||
background-color: rgba(249, 115, 22, 0.3);
|
||
border-color: rgba(249, 115, 22, 0.4);
|
||
}
|
||
|
||
/* 操作按钮样式继承control-button,保持一致性 */
|
||
.right-actions-controls .control-button.export-format-btn {
|
||
background-color: rgba(168, 85, 247, 0.2);
|
||
border-color: rgba(168, 85, 247, 0.3);
|
||
}
|
||
|
||
.right-actions-controls .control-button.export-format-btn:hover {
|
||
background-color: rgba(168, 85, 247, 0.3);
|
||
border-color: rgba(168, 85, 247, 0.4);
|
||
}
|
||
|
||
/* GLB导出按钮特殊样式 */
|
||
.right-actions-controls .control-button:nth-child(1) {
|
||
background-color: rgba(34, 197, 94, 0.2);
|
||
border-color: rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
.right-actions-controls .control-button:nth-child(1):hover {
|
||
background-color: rgba(34, 197, 94, 0.3);
|
||
border-color: rgba(34, 197, 94, 0.4);
|
||
}
|
||
|
||
/* OBJ导出按钮特殊样式 */
|
||
.right-actions-controls .control-button:nth-child(2) {
|
||
background-color: rgba(59, 130, 246, 0.2);
|
||
border-color: rgba(59, 130, 246, 0.3);
|
||
}
|
||
|
||
.right-actions-controls .control-button:nth-child(2):hover {
|
||
background-color: rgba(59, 130, 246, 0.3);
|
||
border-color: rgba(59, 130, 246, 0.4);
|
||
}
|
||
|
||
/* STL导出按钮特殊样式 */
|
||
.right-actions-controls .control-button:nth-child(3) {
|
||
background-color: rgba(249, 115, 22, 0.2);
|
||
border-color: rgba(249, 115, 22, 0.3);
|
||
}
|
||
|
||
.right-actions-controls .control-button:nth-child(3):hover {
|
||
background-color: rgba(249, 115, 22, 0.3);
|
||
border-color: rgba(249, 115, 22, 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;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.ip-card-container {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.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> |