360 lines
8.8 KiB
Markdown
360 lines
8.8 KiB
Markdown
# Spec: 第三步 - 模型生成和展示功能
|
||
|
||
## ADDED Requirements
|
||
|
||
### 生成模型展示
|
||
- **Requirement**: 实现已生成模型的展示区域,支持点击放大预览功能
|
||
- **Scenario**: 用户可以看到通过第二步生成的3D模型,模型以缩略图形式展示,点击后可以使用ModelViewer组件进行全屏预览
|
||
|
||
### 重新生成功能
|
||
- **Requirement**: 添加重新生成模型按钮,支持更新模型内容
|
||
- **Scenario**: 用户对当前生成的模型不满意时,点击重新生成按钮,系统重新执行模型生成算法
|
||
|
||
### 导出功能
|
||
- **Requirement**: 实现导出功能,提供不同格式的模型文件导出选项
|
||
- **Scenario**: 用户可以选择不同的文件格式(如GLB、OBJ、FBX等)进行模型导出,下载到本地
|
||
|
||
### 发货功能
|
||
- **Requirement**: 添加发货按钮,点击后进入第四步物流发货环节
|
||
- **Scenario**: 用户满意模型结果后,点击发货按钮,系统进入物流信息填写流程并进入第四步
|
||
|
||
### 模型预览集成
|
||
- **Requirement**: 集成现有的ModelViewer组件进行全屏模型预览
|
||
- **Scenario**: 点击模型缩略图后弹出全屏预览对话框,用户可以旋转、缩放、查看模型详情
|
||
|
||
## 技术实现细节
|
||
|
||
### 布局结构
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
### 模型预览对话框
|
||
```javascript
|
||
const modelPreviewVisible = ref(false)
|
||
|
||
const openModelPreview = () => {
|
||
modelPreviewVisible.value = true
|
||
}
|
||
```
|
||
|
||
```html
|
||
<!-- 模型预览对话框 -->
|
||
<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>
|
||
```
|
||
|
||
### 重新生成逻辑
|
||
```javascript
|
||
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
|
||
}
|
||
}
|
||
```
|
||
|
||
### 导出功能实现
|
||
```javascript
|
||
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
|
||
}
|
||
```
|
||
|
||
### 发货流程
|
||
```javascript
|
||
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
|
||
}
|
||
}
|
||
```
|
||
|
||
### 样式实现
|
||
```css
|
||
.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%;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 数据模拟
|
||
```javascript
|
||
// 模拟不同模型数据
|
||
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组件在预览对话框中正常工作
|
||
- 重新生成按钮能够触发模型重新生成流程
|
||
- 导出功能能够正常下载不同格式的文件
|
||
- 发货按钮能够正常进入第四步
|
||
- 响应式布局在各种设备上表现良好 |