deotalandAi/openspec/changes/create-disassembly-workflow/specs/model-step/spec.md

8.8 KiB
Raw Blame History

Spec: 第三步 - 模型生成和展示功能

ADDED Requirements

生成模型展示

  • Requirement: 实现已生成模型的展示区域,支持点击放大预览功能
  • Scenario: 用户可以看到通过第二步生成的3D模型模型以缩略图形式展示点击后可以使用ModelViewer组件进行全屏预览

重新生成功能

  • Requirement: 添加重新生成模型按钮,支持更新模型内容
  • Scenario: 用户对当前生成的模型不满意时,点击重新生成按钮,系统重新执行模型生成算法

导出功能

  • Requirement: 实现导出功能,提供不同格式的模型文件导出选项
  • Scenario: 用户可以选择不同的文件格式如GLB、OBJ、FBX等进行模型导出下载到本地

发货功能

  • Requirement: 添加发货按钮,点击后进入第四步物流发货环节
  • Scenario: 用户满意模型结果后,点击发货按钮,系统进入物流信息填写流程并进入第四步

模型预览集成

  • Requirement: 集成现有的ModelViewer组件进行全屏模型预览
  • Scenario: 点击模型缩略图后弹出全屏预览对话框,用户可以旋转、缩放、查看模型详情

技术实现细节

布局结构

<div class="model-step-content">
  <div class="model-display">
    <div class="model-header">
      <h3>{{ t('disassembly.steps.model') }}</h3>
      <el-tag type="success">{{ t('disassembly.status.completed') }}</el-tag>
    </div>
    
    <div class="model-preview-container">
      <div 
        class="model-thumbnail"
        @click="openModelPreview"
      >
        <ModelViewer 
          :model-url="generatedModelUrl"
          :show-controls="false"
          :loading-text="t('modelViewer.loadingModel')"
          style="height: 300px; width: 100%;"
        />
        <div class="preview-overlay">
          <el-icon><View /></el-icon>
          <span>{{ t('disassembly.actions.previewModel') }}</span>
        </div>
      </div>
    </div>
  </div>
  
  <div class="step-actions">
    <el-dropdown @command="handleExport">
      <el-button type="info">
        {{ t('disassembly.actions.export') }}
        <el-icon class="el-icon--right"><ArrowDown /></el-icon>
      </el-button>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item command="glb">GLB格式</el-dropdown-item>
          <el-dropdown-item command="obj">OBJ格式</el-dropdown-item>
          <el-dropdown-item command="fbx">FBX格式</el-dropdown-item>
          <el-dropdown-item command="gltf">GLTF格式</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
    
    <el-button 
      type="warning" 
      @click="regenerateModel"
      :loading="regenerateLoading"
    >
      {{ t('disassembly.actions.regenerate') }}
    </el-button>
    <el-button 
      type="primary" 
      @click="proceedToShipping"
      :loading="shippingLoading"
    >
      {{ t('disassembly.actions.ship') }}
    </el-button>
  </div>
</div>

模型预览对话框

const modelPreviewVisible = ref(false)

const openModelPreview = () => {
  modelPreviewVisible.value = true
}
<!-- 模型预览对话框 -->
<el-dialog 
  :title="t('disassembly.modelPreview')" 
  v-model="modelPreviewVisible"
  width="90%"
  top="5vh"
  :z-index="3000"
  :append-to-body="true"
  class="model-preview-dialog"
>
  <div class="model-preview-content">
    <ModelViewer 
      ref="modelViewerRef"
      :model-url="generatedModelUrl"
      :show-controls="true"
      :loading-text="t('modelViewer.loadingModel')"
      style="height: 70vh; width: 100%;"
    />
  </div>
</el-dialog>

重新生成逻辑

const regenerateModel = async () => {
  // 检查是否有第四步需要覆盖
  if (workflowState.currentStep >= 4) {
    const confirmed = await ElMessageBox.confirm(
      t('disassembly.confirm.regenerateModelOverride'),
      t('common.confirm'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    )
    
    if (!confirmed) return
  }
  
  try {
    regenerateLoading.value = true
    
    // 重新生成模型(用户后续实现)
    // const newModel = await regenerateModelAPI(contentId)
    
    // 模拟重新生成过程
    await new Promise(resolve => setTimeout(resolve, 4000))
    
    // 更新模型数据使用不同的demo模型或随机生成
    generatedModelUrl.value = generateMockModel()
    
    // 清空第四步
    if (workflowState.currentStep >= 4) {
      workflowState.contentData.shippingInfo = {}
      workflowState.stepStatus[4] = 'pending'
      workflowState.currentStep = 3
    }
    
    ElMessage.success(t('disassembly.success.modelRegenerateCompleted'))
    
  } catch (error) {
    ElMessage.error(t('disassembly.errors.modelRegenerateFailed'))
  } finally {
    regenerateLoading.value = false
  }
}

导出功能实现

const handleExport = async (format) => {
  try {
    ElMessage.info(t('disassembly.info.exporting'))
    
    // 调用导出API用户后续实现
    // await exportModelAPI(contentId, format)
    
    // 模拟导出过程
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    // 创建下载链接
    const downloadUrl = generateDownloadUrl(format)
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = `model-${contentId}.${format}`
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    
    ElMessage.success(t('disassembly.success.exportCompleted'))
    
  } catch (error) {
    ElMessage.error(t('disassembly.errors.exportFailed'))
  }
}

const generateDownloadUrl = (format) => {
  // 模拟不同格式的下载链接
  const mockUrls = {
    glb: '/src/assets/demo/model.glb',
    obj: '/src/assets/demo/model.obj', // 假设存在
    fbx: '/src/assets/demo/model.fbx', // 假设存在
    gltf: '/src/assets/demo/model.gltf' // 假设存在
  }
  
  return mockUrls[format] || mockUrls.glb
}

发货流程

const proceedToShipping = async () => {
  try {
    shippingLoading.value = true
    
    // 更新步骤状态
    updateStepStatus(3, 'completed')
    updateStepStatus(4, 'current')
    workflowState.currentStep = 4
    
    ElMessage.success(t('disassembly.success.proceedToShipping'))
    
  } catch (error) {
    ElMessage.error(t('disassembly.errors.proceedToShippingFailed'))
  } finally {
    shippingLoading.value = false
  }
}

样式实现

.model-display {
  background: #ffffff;
  border-radius: 12px;
  padding: 24px;
  margin-bottom: 32px;
  box-shadow: 0 2px 8px rgba(107, 70, 193, 0.1);
}

.model-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}

.model-header h3 {
  margin: 0;
  color: #1f2937;
  font-size: 18px;
  font-weight: 600;
}

.model-preview-container {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f8fafc;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 24px;
}

.model-thumbnail {
  position: relative;
  cursor: pointer;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 200ms ease;
}

.model-thumbnail:hover {
  transform: scale(1.02);
}

.preview-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(107, 70, 193, 0.8);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 200ms ease;
  color: white;
}

.preview-overlay .el-icon {
  font-size: 32px;
  margin-bottom: 8px;
}

.model-thumbnail:hover .preview-overlay {
  opacity: 1;
}

.step-actions {
  display: flex;
  gap: 16px;
  justify-content: flex-end;
  padding-top: 24px;
  border-top: 1px solid #e5e7eb;
}

.step-actions .el-dropdown {
  margin-right: auto;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .model-display {
    padding: 16px;
  }
  
  .model-preview-container {
    padding: 12px;
  }
  
  .step-actions {
    flex-direction: column;
    gap: 12px;
  }
  
  .step-actions .el-dropdown {
    margin-right: 0;
    width: 100%;
  }
  
  .step-actions .el-button {
    width: 100%;
  }
}

数据模拟

// 模拟不同模型数据
const generateMockModel = () => {
  const models = [
    '/src/assets/demo/model.glb',
    '/src/assets/demo/model2.glb', // 假设存在
    '/src/assets/demo/model3.glb'  // 假设存在
  ]
  
  return models[Math.floor(Math.random() * models.length)]
}

验证标准

  • 生成模型能够正确加载和显示
  • 点击模型能够弹出全屏预览对话框
  • ModelViewer组件在预览对话框中正常工作
  • 重新生成按钮能够触发模型重新生成流程
  • 导出功能能够正常下载不同格式的文件
  • 发货按钮能够正常进入第四步
  • 响应式布局在各种设备上表现良好