deotalandAi/apps/FrontendDesigner/src/views/admin/AdminDisassemblyDetail/AdminDisassemblyDetail.vue

1341 lines
33 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="disassembly-detail">
<div class="order-info-container">
<div class="order-info-header">
<div class="header-left">
<el-button circle @click="goBack" class="back-button">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<h2 class="order-title">拆件详情</h2>
</div>
<el-tag :type="getStatusType(orderDetail.status)" class="order-status">
{{ getStatusText(orderDetail.status) }}
</el-tag>
</div>
<div class="order-info-content">
<div class="info-item">
<span class="info-label">订单号</span>
<span class="info-value">{{ orderDetail.orderNumber }}</span>
</div>
<div class="info-item">
<span class="info-label">创作者</span>
<span class="info-value">{{ orderDetail.customerName }}</span>
</div>
<div class="info-item">
<span class="info-label">模型名称</span>
<span class="info-value">{{ orderDetail.modelName }}</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ orderDetail.createTime }}</span>
</div>
</div>
</div>
<div class="timeline-section">
<div class="timeline-header">
<div class="timeline-title-container">
<h2 class="timeline-title">处理流程</h2>
<div class="timeline-progress">
<span class="progress-text">进度</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${(currentStep / 3) * 100}%` }"></div>
</div>
<span class="progress-step">{{ currentStep }}/3</span>
</div>
</div>
</div>
<div class="timeline-container">
<el-timeline>
<!-- 第一步预览图和模型展示 -->
<el-timeline-item
:color="currentStep >= 1 ? '#6B46C1' : '#e4e7ed'"
size="large"
>
<div class="timeline-content">
<h3>{{ $t('admin.disassemblyOrders.detail.step1') }}</h3>
<div v-if="currentStep >= 1" class="step-content">
<div class="preview-container-horizontal">
<div class="preview-images-horizontal">
<div v-if="thumbnailUrl" class="image-item-horizontal" @click="previewImage(thumbnailUrl)">
<img :src="thumbnailUrl" alt="缩略图" />
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }}</div>
<div class="image-actions">
<el-button
type="primary"
size="small"
@click.stop="changePreviewImage"
>
{{ $t('admin.disassemblyOrders.detail.changePreview') }}
</el-button>
</div>
</div>
<div class="image-item-horizontal" @click="previewModel(modelUrl)">
<div class="model-preview-horizontal">
<el-icon size="48"><View /></el-icon>
<span>{{ $t('admin.disassemblyOrders.detail.previewDialog') }}</span>
</div>
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.previewDialog') }}</div>
</div>
</div>
<div class="prompt-template-container">
<div class="prompt-template-label">拆件提示词模板</div>
<el-input
v-model="cjtsc"
type="textarea"
:rows="4"
placeholder="请输入拆件提示词模板"
class="prompt-template-textarea"
></el-input>
</div>
<div class="prompt-template-container">
<div class="prompt-template-label">合并提示词模板</div>
<el-input
v-model="hbtsc"
type="textarea"
:rows="4"
placeholder="请输入合并提示词模板"
class="prompt-template-textarea"
></el-input>
</div>
<div class="disassembly-button-container">
<el-button
type="primary"
size="large"
:loading="disassemblyLoading"
@click="startDisassembly"
>
{{ $t('admin.disassemblyOrders.detail.disassembly') }}
</el-button>
</div>
</div>
</div>
</div>
</el-timeline-item>
<!-- 第二步:展示已拆件图片 -->
<el-timeline-item
:color="currentStep >= 2 ? '#6B46C1' : '#e4e7ed'"
size="large"
>
<div class="timeline-content" style="z-index: 3;">
<div class="step-header">
<h3>{{ $t('admin.disassemblyOrders.detail.step2') }}</h3>
<el-button
v-if="currentStep >= 2"
type="primary"
size="small"
@click="toggleMergeMode"
>
{{ mergeMode ? '取消合并' : '合并图片' }}
</el-button>
</div>
<div v-if="currentStep >= 2&&disassembledImages.length>0" class="step-content">
<div class="disassembled-images">
<div
v-for="(item, index) in disassembledImages"
:key="item.id"
class="image-item"
:class="{ 'selectable': mergeMode, 'selected': selectedImages.includes(index) }"
>
<ImageWrapper
:prompt="cjtsc"
:card-data="item"
:selected="selectedImages.includes(index)"
@preview="previewImage"
@delete="deleteImage(index)"
@generate-model="generateModelFromImage"
@show-actions="showImageActions"
@hide-actions="hideImageActions"
@save="(result)=>saveImage(index,result)"
@edit="(result)=>editImage(index,result)"
@partial-edit="(imageUrl)=>handlePartialEdit(imageUrl, index)"
/>
<!-- <div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div> -->
<!-- 选择框 -->
<!-- <div v-if="mergeMode" class="image-checkbox">
<el-checkbox v-model="selectedImages" :label="index" @change="handleCheckboxChange"></el-checkbox>
</div> -->
<div v-if="mergeMode" @click="mergeMode && toggleImageSelection(index)" style="position: absolute;width: 100%;height: 100%;z-index: 2;"></div>
</div>
</div>
<!-- 确定合并按钮 -->
<div v-if="showMergeConfirm" class="merge-confirm-button">
<el-button type="success" size="large" @click="confirmMerge">
确定合并
</el-button>
</div>
</div>
</div>
<div style="height: 80px;width: 100%;"></div>
</el-timeline-item>
<!-- 第三步:展示生成的模型 -->
<el-timeline-item
:color="currentStep >= 3 ? '#6B46C1' : '#e4e7ed'"
size="large"
>
<div class="timeline-content">
<h3>{{ $t('admin.disassemblyOrders.detail.step3') }}</h3>
<div class="step-content" >
<div class="model-upload-section">
<el-button
type="primary"
@click="showModelUploadModal = true"
class="upload-model-button"
>
<el-icon><Upload /></el-icon>
{{ $t('modelUpload.uploadModel') }}
</el-button>
<span class="upload-hint-text">{{ $t('modelUpload.uploadHint') }}</span>
<div class="warning-hint" style="margin-left: auto; display: flex; align-items: center; gap: 6px;">
<el-icon color="#f2ac34"><WarningFilled /></el-icon>
<span style="color: #f2ac34; font-size: 13px; line-height: 1.5;">{{ $t('modelUpload.saveToOrderHint') }}</span>
</div>
</div>
<div class="generated-models">
<div
v-for="(item, index) in generatedModels"
:key="item.id"
class="model-item"
>
<ModelCom
@preview="previewModel"
:img-url="item.image"
:model-url="item.modelUrl"
:task-id="item.taskId"
:result-task="item.resultTask"
width="100%"
height="150px"
:project-id="orderDetail.projectId"
@save="(result,resultTask,modelUrl='')=>saveModel(index,result,resultTask,modelUrl)"
:show-delete="true"
@delete="deleteModel(index)"
/>
<div class="image-label">模型 {{ index + 1 }}</div>
</div>
</div>
<!-- <div class="step-actions">
<el-button
type="primary"
@click="handleProcessingComplete"
:loading="processingCompleteLoading"
>
加工完成
</el-button>
</div> -->
</div>
</div>
</el-timeline-item>
<!-- 第四步:工期信息 -->
<!-- <el-timeline-item
:color="currentStep >= 4 ? '#6B46C1' : '#e4e7ed'"
size="large"
>
<div class="timeline-content">
<h3>工期信息</h3>
<div v-if="currentStep >= 4" class="step-content">
<div class="work-period-info">
<div class="info-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Calendar /></el-icon>
</div>
<div class="info-content">
<div class="info-label">开始工期</div>
<div class="info-value">{{ formatDate(orderDetail.createTime) }}</div>
</div>
</div>
</div>
<div class="info-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Check /></el-icon>
</div>
<div class="info-content">
<div class="info-label">完成工期</div>
<div class="info-value">{{ formatDate(orderDetail.processingCompleteTime) }}</div>
</div>
</div>
</div>
<div class="info-card total-days-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="info-content">
<div class="info-label">共计天数</div>
<div class="info-value">{{ calculateTotalDays() }} 天</div>
</div>
</div>
</div>
</div>
<div class="step-actions">
<el-button @click="goBack">返回列表</el-button>
</div>
</div>
</div>
</el-timeline-item> -->
</el-timeline>
</div>
</div>
<!-- 图片预览对话框 -->
<el-dialog v-model="imagePreviewVisible" :title="$t('admin.disassemblyOrders.detail.preview')" width="50%" >
<img :src="previewImageUrl" style="width:100%;object-fit: contain;" />
</el-dialog>
<!-- 模型预览对话框 -->
<el-dialog v-model="modelPreviewVisible" :title="$t('admin.disassemblyOrders.detail.previewDialog')" width="70%">
<ModelViewer
:model-url="previewModelUrl"
:width="'100%'"
/>
<template #footer>
<el-button @click="modelPreviewVisible = false">{{ $t('admin.disassemblyOrders.detail.close') }}</el-button>
</template>
</el-dialog>
<!-- 更换预览图对话框 -->
<el-dialog v-model="showImageUpload" :title="$t('admin.disassemblyOrders.detail.changePreview')" width="30%">
<div class="upload-container">
<el-upload
class="image-uploader"
:before-upload="handleImageUpload"
:disabled="uploadLoading"
>
<div class="upload-trigger">
<el-icon size="32" v-if="!uploadLoading"><Plus /></el-icon>
<el-icon size="32" v-else><Loading /></el-icon>
<div class="upload-text">
{{ uploadLoading ? $t('common.loading') : $t('admin.disassemblyOrders.detail.uploadImage') }}
</div>
</div>
</el-upload>
</div>
<template #footer>
<el-button @click="cancelImageUpload">{{ $t('common.cancel') }}</el-button>
</template>
</el-dialog>
<!-- 画布编辑器对话框 -->
<DtCanvasEditor
v-model:visible="canvasEditorVisible"
:image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave"
/>
<!-- 模型上传弹窗 -->
<ModelUploadModal
v-model="showModelUploadModal"
@confirm="handleModelUpload"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch,computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import {AdminOrders} from '../AdminOrders/AdminOrders.js'
import { ElMessage, ElMessageBox } from 'element-plus'
import {prompt} from '@deotaland/utils'
import {
ArrowLeft,
View,
Close,
Calendar,
Check,
Timer,
Plus,
Loading,
Upload
} from '@element-plus/icons-vue'
import ModelViewer from '@/components/common/ModelViewer.vue'
import ModelCom from '@/components/modelCom/index.vue'
import ImageWrapper from '@/components/disassembly/ImageWrapper.vue'
import ModelUploadModal from '@/components/ModelUploadModal/index.vue'
import { AdminDisassemblyDetail } from './AdminDisassemblyDetail.js';
import { MeshyServer,GiminiServer,FileServer } from '@deotaland/utils';
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const Plug = new AdminDisassemblyDetail();
const adminOrders = new AdminOrders();
const fileServer = new FileServer();
const cjtsc = ref(prompt.Hairseparation+'');
const hbtsc = ref('将图中元素并排放在一张图中');
const editIndex = ref(-1);
const editContent = ref('');
const canvasEditorVisible = ref(false);
const canvasEditorImageUrl = ref('');
const currentEditImageIndex = ref(-1);
const showModelUploadModal = ref(false);
// 订单详情
const orderDetail = ref({
orderNumber: '',
customerName: '',
modelName: '',
createTime: '',
status: 'pending',
processingCompleteTime: null
})
const saveModel = (index,result,resultTask,modelUrl='')=>{
generatedModels.value[index].taskId = result;
generatedModels.value[index].resultTask = resultTask;
generatedModels.value[index].modelUrl = modelUrl;
}
const saveImage = (index,result)=>{
disassembledImages.value[index].taskId = result.taskId;
disassembledImages.value[index].taskQueue = result.taskQueue;
disassembledImages.value[index].imageUrl = result.imageUrl;
}
const editImage = (index,result)=>{
const imgurl = disassembledImages.value[index].imageUrl;
const newItem = {
id: new Date().getTime(),
thumbnailUrl:imgurl,
imageUrl: '',
taskID: '',
taskQueue: '',
prompt: result,
type:'edit',
project_id: orderDetail.value.projectId,
};
disassembledImages.value.push(newItem);
}
const handlePartialEdit = (imageUrl, index) => {
canvasEditorImageUrl.value = imageUrl;
currentEditImageIndex.value = index;
canvasEditorVisible.value = true;
}
const handleCanvasSave = (editedImageUrl,editContent) => {
fileServer.uploadFile(editedImageUrl).then((url) => {
const newItem = {
id: new Date().getTime(),
thumbnailUrl: url,
taskID: '',
taskQueue: '',
prompt: editContent,
type:'edit',
project_id: orderDetail.value.projectId,
};
disassembledImages.value.push(newItem);
})
}
canvasEditorVisible.value = false;
// 当前步骤
const currentStep = ref(3)
// 加载状态
const disassemblyLoading = ref(false)
const processingCompleteLoading = ref(false)
// 预览相关
const imagePreviewVisible = ref(false)
const modelPreviewVisible = ref(false)
const previewImageUrl = ref('')
const previewModelUrl = ref('')
// 图片删除相关
const hoveredImageIndex = ref(-1)
// 更换预览图相关
const showImageUpload = ref(false)
const uploadLoading = ref(false)
// 合并图片相关
const mergeMode = ref(false)
const selectedImages = ref([])
// 计算属性:是否显示确定合并按钮
const showMergeConfirm = computed(() => {
return mergeMode.value && selectedImages.value.length >= 2
})
// 资源路径
const thumbnailUrl = ref('');
const modelUrl = ref('')
const generatedModelUrl = ref('')
// 存储多个生成的模型
const generatedModels = ref([])
// 拆件后的图片(模拟)
const disassembledImages = ref([
])
watch(()=>[generatedModels.value,disassembledImages.value],()=>{
saveDisassemblyProgress()
}, {deep: true})
// 预览模型
const previewModel = (url) => {
previewModelUrl.value = url
modelPreviewVisible.value = true
}
// 处理模型上传
const handleModelUpload = (files) => {
if (files.length === 0) {
ElMessage.warning(t('admin.disassemblyOrders.detail.messages.uploadModelWarning') || '请选择要上传的模型文件')
return
}
const file = files[0]
fileServer.uploadFile(file).then((url) => {
generatedModels.value.push({
id: new Date().getTime(),
modelUrl: url,
taskId: '',
resultTask: ''
})
})
}
//保存拆件进度
const saveDisassemblyProgress = () => {
let project_details = {
...orderData.value.project_details,
disassembledImages: disassembledImages.value,
generatedModels: generatedModels.value
};
let params = {
id:orderData.value.id,
project_details:project_details
}
adminOrders.saveDisassemblyProgress(params);
}
// 获取状态标签类型
const getStatusType = (status) => {
const statusMap = {
pending: 'warning',
processing: 'primary',
'awaiting-shipment': 'success',
completed: 'success',
failed: 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 'pending':
return '待处理'
case 'processing':
return '处理中'
case 'awaiting-shipment':
return '待发货'
case 'completed':
return '已完成'
case 'failed':
return '失败'
default:
return '未知'
}
}
// 返回上一页
const goBack = () => {
router.back();
}
// 预览图片
const previewImage = (url) => {
previewImageUrl.value = url
imagePreviewVisible.value = true
}
// 更换预览图
const changePreviewImage = () => {
showImageUpload.value = true
}
// 处理图片上传
const handleImageUpload = (file) => {
uploadLoading.value = true
// 更新预览图URL
// thumbnailUrl.value = e.target.result
fileServer.uploadFile(file).then((url) => {
thumbnailUrl.value = url
uploadLoading.value = false
showImageUpload.value = false
ElMessage.success(t('admin.disassemblyOrders.detail.messages.uploadSuccess') || '图片上传成功')
})
}
// 取消图片上传
const cancelImageUpload = () => {
showImageUpload.value = false
}
const startDisassembly = () => {
for(let i = 0;i<1;i++){
handleDisassembly();
}
}
// 执行拆件
const handleDisassembly = () => {
const newItem = {
id: new Date().getTime(),
thumbnailUrl: thumbnailUrl.value,
imageUrl: '',
taskID: '',
taskQueue: '',
project_id: orderDetail.value.projectId,
};
// disassembledImages.value = []
// return
// 使用展开运算符创建新数组以确保响应式更新
disassembledImages.value = [...disassembledImages.value, newItem];
// if(currentStep.value<=2){
// currentStep.value = 2;
// }
}
const deleteModel = (index) => {
generatedModels.value.splice(index, 1)
}
const orderId = ref('');
const orderData = ref({});
// 组件挂载时获取订单信息
onMounted(async () => {
MeshyServer.pollingEnabled = true;
GiminiServer.pollingEnabled = true;
orderId.value = route.params.id
const result = await adminOrders.getOrderDetail({
id:orderId.value
})
let data = result.data;
orderDetail.value = {
orderNumber: data.order_no,
customerName: data.order_info.shipping.firstName+' '+data.order_info.shipping.lastName,
modelName: data.order_info.ipName,
createTime: data.created_at,
status: 'pending',
processingCompleteTime: null,
projectId:data.project_id,
}
thumbnailUrl.value = data.order_info.modelData.imageUrl;
modelUrl.value = data.order_info.modelData.modelUrl;
orderData.value = data;
disassembledImages.value = data.project_details.disassembledImages || [];
generatedModels.value = data.project_details.generatedModels || [];
// 根据数组长度自动判断当前步骤
if (generatedModels.value.length > 0) {
// 如果有生成的模型直接进入第3步
// currentStep.value = 3;
} else if (disassembledImages.value.length > 0) {
// 如果有拆件图片但没有模型进入第2步
// currentStep.value = 2;
} else {
// 否则保持在第1步
// currentStep.value = 1;
}
})
// 组件卸载时清理资源
onUnmounted(() => {
MeshyServer.pollingEnabled = false;
GiminiServer.pollingEnabled = false;
// 清理预览状态
imagePreviewVisible.value = false
modelPreviewVisible.value = false
})
// 删除图片
const deleteImage = (index) => {
disassembledImages.value.splice(index, 1)
}
// 显示图片操作按钮
const showImageActions = (index) => {
hoveredImageIndex.value = index
}
// 隐藏图片操作按钮
const hideImageActions = () => {
hoveredImageIndex.value = -1
}
// 切换合并模式
const toggleMergeMode = () => {
mergeMode.value = !mergeMode.value
if (!mergeMode.value) {
selectedImages.value = []
}
}
// 切换图片选择状态
const toggleImageSelection = (index) => {
const indexInSelected = selectedImages.value.indexOf(index)
if (indexInSelected > -1) {
selectedImages.value.splice(indexInSelected, 1)
} else {
selectedImages.value.push(index)
}
}
// 处理复选框变化
const handleCheckboxChange = () => {
// 复选框变化由v-model自动处理
}
// 确认合并图片
const confirmMerge = () => {
const imagesToMerge = selectedImages.value.map(index => {
return {
index,
...disassembledImages.value[index]
}
})
let imgArr = [];
// console.log('Selected images to merge:', imagesToMerge)
imagesToMerge.forEach((item,index) => {
imgArr.push(item.imageUrl)
})
let parms = {
id:new Date().getTime(),
project_id:orderDetail.value.projectId,
imgArr,
type:'merge',//合并类型
prompt:hbtsc.value//使用合并提示词模板
}
console.log(parms,'parmsparms');
disassembledImages.value.push(parms)
// 合并完成后退出合并模式
mergeMode.value = false
selectedImages.value = []
}
// 从单张图片生成模型并进入第三步
const generateModelFromImage = async (image) => {
// console.log('generateModelFromImage',image);
const newModel = {
id: new Date().getTime(),
image: image,
taskId: ''
};
// 使用展开运算符创建新数组以确保响应式更新
generatedModels.value = [...generatedModels.value, newModel];
console.log('generatedModels.value',generatedModels.value);
// 如果是第一次生成模型,进入第三步
// if (currentStep.value < 3) {
// currentStep.value = 3
// }
}
</script>
<style scoped>
.page-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
}
/* 订单信息容器 */
.order-info-container {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.order-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.back-button {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
color: #6b7280;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.back-button:hover {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #374151;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.order-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #1f2937;
}
.order-status {
font-weight: 500;
}
.order-info-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: 14px;
color: #6b7280;
font-weight: 500;
}
.info-value {
font-size: 15px;
color: #1f2937;
font-weight: 400;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
}
.page-subtitle {
color: #6b7280;
margin: 0;
font-size: 14px;
}
.timeline-section {
margin-top: 24px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
overflow: hidden;
}
.timeline-header {
padding: 20px;
border-bottom: 1px solid #f3f4f6;
background: linear-gradient(135deg, #6B46C1 0%, #8B5CF6 100%);
}
.timeline-title-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-title {
margin: 0;
font-size: 22px;
font-weight: 600;
color: #ffffff;
}
.timeline-progress {
display: flex;
align-items: center;
gap: 12px;
}
.progress-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.progress-bar {
width: 120px;
height: 6px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #ffffff;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-step {
font-size: 14px;
color: #ffffff;
font-weight: 600;
}
.timeline-container {
margin-top: 0;
padding: 20px;
}
.timeline-content {
padding-left: 16px;
}
.timeline-content h3 {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0 0 16px 0;
display: flex;
align-items: center;
/* position: relative; */
/* top: -1px; */
}
/* 调整Element Plus时间轴组件的节点样式 */
:deep(.el-timeline-item__wrapper) {
padding-left: 28px;
position: relative;
}
:deep(.el-timeline-item__tail) {
left: 13px;
border-width: 2px;
}
:deep(.el-timeline-item__node) {
left: 7px;
width: 12px;
height: 12px;
background-color: #ffffff;
border: 2px solid #e4e7ed;
border-radius: 50%;
}
:deep(.el-timeline-item__node--primary) {
border-color: #6B46C1;
background-color: #6B46C1;
}
:deep(.el-timeline-item__timestamp) {
padding-left: 28px;
}
/* 调整时间轴内容的位置 */
:deep(.el-timeline-item__content) {
padding-left: 0;
position: relative;
top: -2px;
}
.step-content {
background-color: #f9fafb;
border-radius: 8px;
padding: 16px;
margin-top: 12px;
}
.model-upload-section {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background-color: #f3f4f6;
border-radius: 8px;
border: 1px dashed #d1d5db;
}
.upload-model-button {
display: flex;
align-items: center;
gap: 6px;
}
.upload-hint-text {
font-size: 13px;
color: #6b7280;
line-height: 1.5;
}
/* 横板预览容器样式 */
.preview-container-horizontal {
display: flex;
flex-direction: column;
position: relative;
border-radius: 8px;
overflow: hidden;
}
.preview-images-horizontal {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.image-item-horizontal {
flex: 1;
cursor: pointer;
transition: transform 0.2s;
max-width: 300px;
}
.image-item-horizontal:hover {
transform: scale(1.02);
}
.image-item-horizontal img {
width: 100%;
height: 180px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.model-preview-horizontal {
width: 100%;
height: 180px;
background-color: #f3f4f6;
border-radius: 8px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #6b7280;
}
.disassembly-button-container {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
/* 提示词模板在预览容器中的样式调整 */
.preview-container-horizontal .prompt-template-container {
margin-bottom: 0;
}
/* 原有样式保持不变 */
.preview-container,
.disassembled-images,
.generated-models {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
position: relative;
}
.image-item,
.model-item {
position: relative;
width: 200px;
cursor: pointer;
transition: transform 0.2s;
display: flex;
flex-direction: column;
align-items: center;
border: 1px dashed #d9d9d9;
border-radius: 8px;
padding: 8px;
}
.image-item:hover,
.model-item:hover {
transform: scale(1.02);
}
.model-wrapper {
position: relative;
width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.delete-model-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
}
.model-item:hover {
border-color: #6B46C1;
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.1);
}
.model-actions {
position: absolute;
bottom: 4px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 10;
}
.image-item img {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.model-preview {
width: 100%;
height: 150px;
background-color: #f3f4f6;
border-radius: 8px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #6b7280;
}
.image-label {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #6b7280;
}
.image-actions {
display: flex;
justify-content: center;
margin-top: 8px;
gap: 8px;
}
.step-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.shipping-form {
max-width: 600px;
}
.work-period-info {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-bottom: 24px;
}
.info-card {
flex: 1;
min-width: 280px;
background-color: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.info-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-content {
display: flex;
align-items: center;
gap: 16px;
}
.total-days-card {
background: linear-gradient(135deg, #6B46C1 0%, #9333EA 100%);
color: white;
border: none;
}
.info-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background-color: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
color: #6B46C1;
font-size: 20px;
flex-shrink: 0;
}
.total-days-card .info-icon {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.info-content {
flex: 1;
}
.info-label {
font-size: 14px;
color: #6b7280;
margin-bottom: 4px;
font-weight: 500;
}
.total-days-card .info-label {
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: 16px;
color: #1f2937;
font-weight: 600;
}
.total-days-card .info-value {
color: white;
font-size: 24px;
font-weight: 700;
}
.model-preview-dialog {
display: flex;
justify-content: center;
align-items: center;
}
/* 上传容器样式 */
.upload-container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.image-uploader {
width: 100%;
}
.upload-trigger {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 200px;
height: 150px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 auto;
}
.upload-trigger:hover {
border-color: #6B46C1;
background-color: #f9fafb;
}
.upload-text {
margin-top: 8px;
font-size: 14px;
color: #6b7280;
}
.prompt-template-container {
margin-bottom: 16px;
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
border: 1px solid #e5e7eb;
}
.prompt-template-label {
font-size: 14px;
font-weight: 500;
color: #6b7280;
margin-bottom: 8px;
display: block;
}
.prompt-template-textarea {
width: 100%;
font-size: 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 8px;
resize: vertical;
}
.prompt-template-textarea:focus {
outline: none;
border-color: #6B46C1;
box-shadow: 0 0 0 3px rgba(107, 70, 193, 0.1);
}
@media (max-width: 768px) {
.disassembly-detail {
padding: 12px;
}
.page-title {
font-size: 24px;
}
.preview-container,
.disassembled-images,
.generated-model {
flex-direction: column;
}
.preview-images-horizontal {
flex-direction: column;
}
.image-item,
.model-item,
.image-item-horizontal {
width: 100%;
max-width: none;
}
.step-actions {
flex-direction: column;
}
.disassembly-button-container {
justify-content: center;
}
.shipping-form {
max-width: 100%;
}
.work-period-info {
flex-direction: column;
gap: 16px;
}
.info-card {
min-width: auto;
}
}
</style>