Răsfoiți Sursa

feat(project-detail): 添加交付执行板块的横向折叠面板功能

实现交付执行板块的横向折叠面板布局,包括:
1. 新增交付流程数据结构接口
2. 添加流程和空间的展开/折叠功能
3. 支持空间的新增和删除操作
4. 实现空间图片上传和管理功能
5. 添加响应式设计适配移动端
6. 移除个人看板中不必要的后期阶段进度显示
0235711 2 zile în urmă
părinte
comite
d012fe0b28

+ 0 - 1
src/app/pages/designer/personal-board/personal-board.ts

@@ -322,7 +322,6 @@ export class PersonalBoard implements OnInit, AfterViewInit {
       '建模': 30,
       '软装': 0,
       '渲染': 50,
-      '后期': 20,
       '尾款结算': 0,
       '客户评价': 0,
       '投诉处理': 0

+ 218 - 217
src/app/pages/designer/project-detail/project-detail.html

@@ -732,234 +732,235 @@
                             <div class="empty-tip" style="color:#888;">暂无色彩分析结果</div>
                           }
                         </div>
-                      } @else if (stage === '建模') {
-                        <div class="upload-section">
-                          <div class="upload-header">
-                            <h4>上传白模图片</h4>
-                            <span class="hint">支持:JPG/PNG,不强制4K</span>
-                          </div>
-                          @if (canEditSection('delivery')) {
-                            <div class="upload-dropzone" 
-                                 (click)="whiteModelImages.length === 0 ? triggerFileInput('whiteModel') : null"
-                                 (dragover)="whiteModelImages.length === 0 ? onDragOver($event) : null"
-                                 (dragleave)="whiteModelImages.length === 0 ? onDragLeave($event) : null"
-                                 (drop)="whiteModelImages.length === 0 ? onFileDrop($event, 'whiteModel') : null"
-                                 [class.drag-over]="isDragOver && whiteModelImages.length === 0"
-                                 [class.has-images]="whiteModelImages.length > 0">
-                              @if (whiteModelImages.length === 0) {
-                                <div class="upload-icon"></div>
-                                <div class="upload-text">点击此处或拖拽文件到此处上传</div>
-                                <div class="upload-hint">支持 JPG、PNG 格式,单个文件最大 10MB</div>
-                              } @else {
-                                <div class="uploaded-images-grid">
-                                  @for (img of whiteModelImages; track img.id) {
-                                    <div class="uploaded-image-item" (click)="previewImage(img)">
-                                      <img [src]="img.url" [alt]="img.name" />
-                                      <div class="image-overlay">
-                                        <div class="image-name">{{ img.name }}</div>
-                                        <div class="image-actions">
-                                          <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                              <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                              <circle cx="12" cy="12" r="3"></circle>
-                                            </svg>
-                                          </button>
-                                          <button class="remove-btn" (click)="$event.stopPropagation(); removeWhiteModelImage(img.id)">
-                                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                      } @else if (stage === '建模' || stage === '软装' || stage === '渲染') {
+                        <!-- 横向折叠面板布局 -->
+                        <div class="delivery-execution-panel">
+                          <div class="delivery-processes-container">
+                            @for (process of deliveryProcesses; track process.name) {
+                              @if ((stage === '建模' && process.type === 'modeling') || 
+                                   (stage === '软装' && process.type === 'softDecor') || 
+                                   (stage === '渲染' && process.type === 'rendering')) {
+                                <div class="delivery-process-card" [class.expanded]="process.isExpanded">
+                                  <!-- 流程标题 -->
+                                  <div class="process-header" (click)="toggleProcess(process.id)">
+                                    <h4>{{ process.name }}</h4>
+                                    <div class="expand-icon" [class.rotated]="process.isExpanded">
+                                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                        <polyline points="6,9 12,15 18,9"></polyline>
+                                      </svg>
+                                    </div>
+                                  </div>
+                                  
+                                  <!-- 空间列表 -->
+                                  <div class="spaces-list">
+                                    @for (space of process.spaces; track space.id) {
+                                      <div class="space-item" 
+                                           [class.active]="space.isExpanded"
+                                           (click)="toggleSpace(process.id, space.id)">
+                                        <span class="space-name">{{ space.name }}</span>
+                                        @if (canEditSection('delivery')) {
+                                          <button class="remove-space-btn" 
+                                                  (click)="$event.stopPropagation(); removeSpace(process.id, space.id)"
+                                                  title="删除空间">
+                                            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                                               <line x1="18" y1="6" x2="6" y2="18"></line>
                                               <line x1="6" y1="6" x2="18" y2="18"></line>
                                             </svg>
                                           </button>
-                                        </div>
+                                        }
                                       </div>
-                                    </div>
-                                  }
-                                  <div class="add-more-btn" (click)="triggerFileInput('whiteModel')">
-                                    <div class="add-icon">+</div>
-                                    <div class="add-text">添加更多</div>
-                                  </div>
-                                </div>
-                              }
-                              <input type="file" 
-                                     id="whiteModelFileInput"
-                                     accept="{{allowedImageTypes}}" 
-                                     multiple 
-                                     (change)="onWhiteModelSelected($event)" 
-                                     style="display: none;" />
-                            </div>
-                          }
-                          <div class="upload-actions">
-                            @if (canEditSection('delivery')) {
-                              <button class="primary-btn" [disabled]="whiteModelImages.length===0" (click)="confirmWhiteModelUpload()">确认上传</button>
-                            }
-                            @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('white')">同步图片信息</button> }
-                            @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
-                          </div>
-                        </div>
-                        <div class="model-check-section">
-                          <h4>模型差异检查清单</h4>
-                          <div class="checklist">
-                            @for (item of modelCheckItems; track item.id) {
-                              <div class="checklist-item">
-                                <label class="checklist-label">
-                                  <input type="checkbox" class="custom-checkbox" [checked]="item.isPassed" (change)="updateModelCheckItem(item.id, $any($event.target).checked)" [disabled]="!canEditSection('delivery')">
-                                  <span class="checklist-text">{{ item.name }}</span>
-                                </label>
-                                <span class="check-status">{{ item.isPassed ? '已通过' : '待处理' }}</span>
-                              </div>
-                            }
-                          </div>
-                        </div>
-                      } @else if (stage === '软装') {
-                        <div class="softdecor-section">
-                          <h4>软装清单素材</h4>
-                          <div class="upload-section">
-                            <div class="upload-header">
-                              <h4>上传软装小图</h4>
-                              <span class="hint">建议 ≤1MB 的 JPG/PNG 小图</span>
-                            </div>
-                            @if (canEditSection('delivery')) {
-                              <div class="upload-dropzone" 
-                                   (click)="softDecorImages.length === 0 ? triggerFileInput('softDecor') : null"
-                                   (dragover)="softDecorImages.length === 0 ? onDragOver($event) : null"
-                                   (dragleave)="softDecorImages.length === 0 ? onDragLeave($event) : null"
-                                   (drop)="softDecorImages.length === 0 ? onFileDrop($event, 'softDecor') : null"
-                                   [class.drag-over]="isDragOver && softDecorImages.length === 0"
-                                   [class.has-images]="softDecorImages.length > 0">
-                                @if (softDecorImages.length === 0) {
-                                  <div class="upload-icon"></div>
-                                  <div class="upload-text">点击此处或拖拽文件到此处上传</div>
-                                  <div class="upload-hint">支持 JPG、PNG 格式,建议文件大小 ≤1MB</div>
-                                } @else {
-                                  <div class="uploaded-images-grid">
-                                    @for (img of softDecorImages; track img.id) {
-                                      <div class="uploaded-image-item" (click)="previewImage(img)">
-                                        <img [src]="img.url" [alt]="img.name" />
-                                        <div class="image-overlay">
-                                          <div class="image-name">{{ img.name }}</div>
-                                          <div class="image-actions">
-                                            <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                <circle cx="12" cy="12" r="3"></circle>
-                                              </svg>
-                                            </button>
-                                            <button class="remove-btn" (click)="$event.stopPropagation(); removeSoftDecorImage(img.id)">
-                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                <line x1="6" y1="6" x2="18" y2="18"></line>
-                                              </svg>
-                                            </button>
-                                          </div>
+                                    }
+                                    
+                                    <!-- 添加空间按钮 -->
+                                    @if (canEditSection('delivery')) {
+                                      @if (showAddSpaceInput[process.id]) {
+                                        <div class="add-space-input">
+                                          <input type="text" 
+                                                 [(ngModel)]="newSpaceName[process.id]"
+                                                 placeholder="输入空间名称"
+                                                 (keyup.enter)="addSpace(process.id)"
+                                                 (keyup.escape)="cancelAddSpace(process.id)"
+                                                 #spaceInput>
+                                          <button class="confirm-btn" (click)="addSpace(process.id)">确认</button>
+                                          <button class="cancel-btn" (click)="cancelAddSpace(process.id)">取消</button>
                                         </div>
-                                      </div>
+                                      } @else {
+                                        <button class="add-space-btn" (click)="showAddSpaceForm(process.id)">
+                                          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                            <line x1="12" y1="5" x2="12" y2="19"></line>
+                                            <line x1="5" y1="12" x2="19" y2="12"></line>
+                                          </svg>
+                                          添加空间
+                                        </button>
+                                      }
                                     }
-                                    <div class="add-more-btn" (click)="triggerFileInput('softDecor')">
-                                      <div class="add-icon">+</div>
-                                      <div class="add-text">添加更多</div>
-                                    </div>
                                   </div>
-                                }
-                                <input type="file" 
-                                       id="softDecorFileInput"
-                                       accept="{{allowedImageTypes}}" 
-                                       multiple 
-                                       (change)="onSoftDecorSmallPicsSelected($event)" 
-                                       style="display: none;" />
-                              </div>
-                            }
-                            <div class="upload-actions">
-                              @if (canEditSection('delivery')) {
-                                <button class="primary-btn" [disabled]="softDecorImages.length===0" (click)="confirmSoftDecorUpload()">确认上传</button>
-                              }
-                              @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('soft')">同步图片信息</button> }
-                              @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
-                            </div>
-                          </div>
-                        </div>
-                      } @else if (stage === '渲染') {
-                        <div class="render-progress-section">
-                          @if (isLoadingRenderProgress) {
-                            <div class="loading-state">
-                              <div class="loading-spinner"></div>
-                              <div>正在加载渲染进度...</div>
-                            </div>
-                          }
-                          @if (errorLoadingRenderProgress) {
-                            <div class="error-state">
-                              <div>渲染进度加载失败</div>
-                              <button class="secondary-btn" (click)="retryLoadRenderProgress()">重试</button>
-                            </div>
-                          }
-                          @if (!isLoadingRenderProgress && !errorLoadingRenderProgress && renderProgress) {
-                            <div class="progress-info" style="display:flex;gap:16px;align-items:center;margin:12px 0;">
-                              <span>状态:{{ renderProgress.status }}</span>
-                              <span>完成度:{{ renderProgress.completionRate }}%</span>
-                              <span>预计剩余:{{ renderProgress.estimatedTimeRemaining }} 小时</span>
-                            </div>
-                          }
-                          <div class="upload-section">
-                            <div class="upload-header">
-                              <h4>上传渲染大图</h4>
-                              <span class="hint">需满足4K标准(最长边 ≥ 4000px)</span>
-                            </div>
-                            @if (canEditSection('delivery')) {
-                              <div class="upload-dropzone" 
-                                   (click)="renderLargeImages.length === 0 ? triggerFileInput('render') : null"
-                                   (dragover)="renderLargeImages.length === 0 ? onDragOver($event) : null"
-                                   (dragleave)="renderLargeImages.length === 0 ? onDragLeave($event) : null"
-                                   (drop)="renderLargeImages.length === 0 ? onFileDrop($event, 'render') : null"
-                                   [class.drag-over]="isDragOver && renderLargeImages.length === 0"
-                                   [class.has-images]="renderLargeImages.length > 0">
-                                @if (renderLargeImages.length === 0) {
-                                  <div class="upload-icon"></div>
-                                  <div class="upload-text">点击此处或拖拽文件到此处上传</div>
-                                  <div class="upload-hint">支持 JPG、PNG 格式,需满足4K标准(最长边 ≥ 4000px)</div>
-                                } @else {
-                                  <div class="uploaded-images-grid">
-                                    @for (img of renderLargeImages; track img.id) {
-                                      <div class="uploaded-image-item" (click)="previewImage(img)">
-                                        <img [src]="img.url" [alt]="img.name" />
-                                        <div class="image-overlay">
-                                          <div class="image-name">{{ img.name }}</div>
-                                          <div class="image-actions">
-                                            <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                <circle cx="12" cy="12" r="3"></circle>
-                                              </svg>
-                                            </button>
-                                            <button class="remove-btn" (click)="$event.stopPropagation(); removeRenderLargeImage(img.id)">
-                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                <line x1="6" y1="6" x2="18" y2="18"></line>
-                                              </svg>
-                                            </button>
+                                  
+                                  <!-- 展开的内容区域 -->
+                                  @if (process.isExpanded) {
+                                    <div class="process-content">
+                                      @for (space of process.spaces; track space.id) {
+                                        @if (space.isExpanded) {
+                                          <div class="space-content">
+                                            <div class="space-content-header">
+                                              <h5>{{ space.name }} - {{ process.name }}</h5>
+                                            </div>
+                                            
+                                            @if (process.type === 'modeling') {
+                                              <!-- 建模内容 -->
+                                              <div class="modeling-content">
+                                                <div class="upload-section">
+                                                  <div class="upload-header">
+                                                    <h6>上传白模图片</h6>
+                                                    <span class="hint">支持:JPG/PNG,不强制4K</span>
+                                                  </div>
+                                                  @if (canEditSection('delivery')) {
+                                                    <div class="upload-dropzone" 
+                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
+                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
+                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
+                                                        <div class="upload-icon"></div>
+                                                        <div class="upload-text">点击此处上传白模图片</div>
+                                                        <div class="upload-hint">支持 JPG、PNG 格式,单个文件最大 10MB</div>
+                                                      } @else {
+                                                        <div class="uploaded-images-grid">
+                                                          @for (img of process.content[space.id].images; track img.id) {
+                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
+                                                              <img [src]="img.url" [alt]="img.name" />
+                                                              <div class="image-overlay">
+                                                                <div class="image-name">{{ img.name }}</div>
+                                                                <div class="image-actions">
+                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
+                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                                                                      <circle cx="12" cy="12" r="3"></circle>
+                                                                    </svg>
+                                                                  </button>
+                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
+                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
+                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
+                                                                    </svg>
+                                                                  </button>
+                                                                </div>
+                                                              </div>
+                                                            </div>
+                                                          }
+                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
+                                                            <div class="add-icon">+</div>
+                                                            <div class="add-text">添加更多</div>
+                                                          </div>
+                                                        </div>
+                                                      }
+                                                    </div>
+                                                  }
+                                                </div>
+                                                
+                                                <!-- 模型检查清单 -->
+                                                <div class="model-check-section">
+                                                  <h6>模型差异检查清单</h6>
+                                                  <div class="checklist">
+                                                    @for (item of modelCheckItems; track item.id) {
+                                                      <div class="checklist-item">
+                                                        <label class="checklist-label">
+                                                          <input type="checkbox" class="custom-checkbox" 
+                                                                 [checked]="item.isPassed" 
+                                                                 (change)="updateModelCheckItem(item.id, $any($event.target).checked)" 
+                                                                 [disabled]="!canEditSection('delivery')">
+                                                          <span class="checklist-text">{{ item.name }}</span>
+                                                        </label>
+                                                        <span class="check-status">{{ item.isPassed ? '已通过' : '待处理' }}</span>
+                                                      </div>
+                                                    }
+                                                  </div>
+                                                </div>
+                                              </div>
+                                            } @else if (process.type === 'softDecor') {
+                                              <!-- 软装内容 -->
+                                              <div class="softdecor-content">
+                                                <div class="upload-section">
+                                                  <div class="upload-header">
+                                                    <h6>上传软装小图</h6>
+                                                    <span class="hint">建议 ≤1MB 的 JPG/PNG 小图</span>
+                                                  </div>
+                                                  @if (canEditSection('delivery')) {
+                                                    <div class="upload-dropzone" 
+                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
+                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
+                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
+                                                        <div class="upload-icon"></div>
+                                                        <div class="upload-text">点击此处上传软装图片</div>
+                                                        <div class="upload-hint">支持 JPG、PNG 格式,建议文件大小 ≤1MB</div>
+                                                      } @else {
+                                                        <div class="uploaded-images-grid">
+                                                          @for (img of process.content[space.id].images; track img.id) {
+                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
+                                                              <img [src]="img.url" [alt]="img.name" />
+                                                              <div class="image-overlay">
+                                                                <div class="image-name">{{ img.name }}</div>
+                                                                <div class="image-actions">
+                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
+                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                                                                      <circle cx="12" cy="12" r="3"></circle>
+                                                                    </svg>
+                                                                  </button>
+                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
+                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
+                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
+                                                                    </svg>
+                                                                  </button>
+                                                                </div>
+                                                              </div>
+                                                            </div>
+                                                          }
+                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
+                                                            <div class="add-icon">+</div>
+                                                            <div class="add-text">添加更多</div>
+                                                          </div>
+                                                        </div>
+                                                      }
+                                                    </div>
+                                                  }
+                                                </div>
+                                              </div>
+                                            } @else if (process.type === 'rendering') {
+                                              <!-- 渲染内容 -->
+                                              <div class="rendering-content">
+                                                <div class="render-progress-info">
+                                                  <h6>渲染进度</h6>
+                                                  <div class="progress-details">
+                                                    <div class="progress-item">
+                                                      <span class="label">状态:</span>
+                                                      <span class="value">{{ process.content[space.id]?.status || '待开始' }}</span>
+                                                    </div>
+                                                    <div class="progress-item">
+                                                      <span class="label">完成度:</span>
+                                                      <span class="value">{{ process.content[space.id]?.progress || 0 }}%</span>
+                                                    </div>
+                                                    <div class="progress-item">
+                                                      <span class="label">更新时间:</span>
+                                                      <span class="value">{{ process.content[space.id]?.lastUpdated || '暂无' }}</span>
+                                                    </div>
+                                                  </div>
+                                                  @if (process.content[space.id]?.notes) {
+                                                    <div class="progress-notes">
+                                                      <span class="label">备注:</span>
+                                                      <span class="value">{{ process.content[space.id].notes }}</span>
+                                                    </div>
+                                                  }
+                                                </div>
+                                              </div>
+                                            }
                                           </div>
-                                        </div>
-                                      </div>
-                                    }
-                                    <div class="add-more-btn" (click)="triggerFileInput('render')">
-                                      <div class="add-icon">+</div>
-                                      <div class="add-text">添加更多</div>
+                                        }
+                                      }
                                     </div>
-                                  </div>
-                                }
-                                <input type="file" 
-                                       id="renderFileInput"
-                                       accept="{{allowedImageTypes}}" 
-                                       multiple 
-                                       (change)="onRenderLargePicsSelected($event)" 
-                                       style="display: none;" />
-                              </div>
-                            }
-                            <div class="upload-actions">
-                              @if (canEditSection('delivery')) {
-                                <button class="primary-btn" [disabled]="renderLargeImages.length===0" (click)="confirmRenderUpload()">确认上传</button>
+                                  }
+                                </div>
                               }
-                              @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('render')">同步图片信息</button> }
-                              @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
-                            </div>
+                            }
                           </div>
                         </div>
                       } @else if (stage === '尾款结算') {

+ 900 - 0
src/app/pages/designer/project-detail/project-detail.scss

@@ -2658,4 +2658,904 @@
     height: 250px;
     padding: 8px;
   }
+}
+
+/* 横向折叠面板样式 */
+.horizontal-accordion {
+  display: flex;
+  gap: 16px;
+  margin: 20px 0;
+  overflow-x: auto;
+  padding-bottom: 8px;
+  
+  /* 自定义滚动条样式 */
+  &::-webkit-scrollbar {
+    height: 6px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+    
+    &:hover {
+      background: #a8a8a8;
+    }
+  }
+  
+  .process-panel {
+    flex: 0 0 auto;
+    min-width: 280px;
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 12px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+    
+    &.expanded {
+      min-width: 400px;
+      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+    }
+    
+    .process-header {
+      padding: 16px 20px;
+      border-bottom: 1px solid #f0f0f0;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      transition: background-color 0.2s ease;
+      
+      &:hover {
+        background-color: #f8f9fa;
+      }
+      
+      .process-title {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        
+        .process-icon {
+          width: 32px;
+          height: 32px;
+          border-radius: 8px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 16px;
+          font-weight: 600;
+          color: white;
+          
+          &.modeling {
+            background: linear-gradient(135deg, #007aff 0%, #5ac8fa 100%);
+          }
+          
+          &.decoration {
+            background: linear-gradient(135deg, #ff9500 0%, #ffcc02 100%);
+          }
+          
+          &.rendering {
+            background: linear-gradient(135deg, #34c759 0%, #30d158 100%);
+          }
+        }
+        
+        .process-info {
+          .process-name {
+            font-size: 16px;
+            font-weight: 600;
+            color: #1d1d1f;
+            margin-bottom: 4px;
+          }
+          
+          .process-status {
+            font-size: 13px;
+            color: #666;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            
+            .status-dot {
+              width: 6px;
+              height: 6px;
+              border-radius: 50%;
+              
+              &.pending {
+                background: #8e8e93;
+              }
+              
+              &.in-progress {
+                background: #ff9500;
+              }
+              
+              &.completed {
+                background: #34c759;
+              }
+            }
+          }
+        }
+      }
+      
+      .expand-icon {
+        font-size: 18px;
+        color: #8e8e93;
+        transition: transform 0.3s ease;
+        
+        &.expanded {
+          transform: rotate(180deg);
+        }
+      }
+    }
+    
+    .process-content {
+      max-height: 0;
+      overflow: hidden;
+      transition: max-height 0.3s ease;
+      
+      &.expanded {
+        max-height: 600px;
+      }
+      
+      .space-list {
+        padding: 16px 20px 0;
+        
+        .space-list-header {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          margin-bottom: 12px;
+          
+          .space-count {
+            font-size: 14px;
+            color: #666;
+            font-weight: 500;
+          }
+          
+          .add-space-btn {
+            padding: 6px 12px;
+            background: #f0f6ff;
+            color: #007aff;
+            border: 1px solid #007aff;
+            border-radius: 6px;
+            font-size: 13px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            
+            &:hover {
+              background: #007aff;
+              color: white;
+            }
+          }
+        }
+        
+        .space-item {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          padding: 10px 12px;
+          margin-bottom: 8px;
+          background: #f8f9fa;
+          border-radius: 8px;
+          cursor: pointer;
+          transition: all 0.2s ease;
+          
+          &:hover {
+            background: #e9ecef;
+          }
+          
+          &.active {
+            background: #e3f2fd;
+            border: 1px solid #007aff;
+          }
+          
+          .space-name {
+            font-size: 14px;
+            font-weight: 500;
+            color: #1d1d1f;
+          }
+          
+          .space-actions {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            
+            .remove-space-btn {
+              width: 20px;
+              height: 20px;
+              border-radius: 4px;
+              background: #ff3b30;
+              color: white;
+              border: none;
+              cursor: pointer;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              font-size: 12px;
+              transition: all 0.2s ease;
+              
+              &:hover {
+                background: #d70015;
+              }
+            }
+          }
+        }
+        
+        .add-space-form {
+          display: flex;
+          gap: 8px;
+          margin-bottom: 12px;
+          
+          .space-name-input {
+            flex: 1;
+            padding: 8px 12px;
+            border: 1px solid #d1d5db;
+            border-radius: 6px;
+            font-size: 14px;
+            
+            &:focus {
+              outline: none;
+              border-color: #007aff;
+              box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
+            }
+          }
+          
+          .form-actions {
+            display: flex;
+            gap: 6px;
+            
+            .confirm-btn, .cancel-btn {
+              padding: 8px 12px;
+              border-radius: 6px;
+              font-size: 13px;
+              font-weight: 500;
+              cursor: pointer;
+              transition: all 0.2s ease;
+            }
+            
+            .confirm-btn {
+              background: #007aff;
+              color: white;
+              border: 1px solid #007aff;
+              
+              &:hover {
+                background: #0056b3;
+              }
+            }
+            
+            .cancel-btn {
+              background: #f8f9fa;
+              color: #666;
+              border: 1px solid #d1d5db;
+              
+              &:hover {
+                background: #e9ecef;
+              }
+            }
+          }
+        }
+      }
+      
+      .space-content {
+        padding: 16px 20px 20px;
+        border-top: 1px solid #f0f0f0;
+        
+        .content-section {
+          margin-bottom: 20px;
+          
+          &:last-child {
+            margin-bottom: 0;
+          }
+          
+          .section-title {
+            font-size: 14px;
+            font-weight: 600;
+            color: #1d1d1f;
+            margin-bottom: 12px;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            
+            &::before {
+              content: '';
+              width: 3px;
+              height: 14px;
+              background: #007aff;
+              border-radius: 2px;
+            }
+          }
+          
+          .upload-area {
+            border: 2px dashed #d1d5db;
+            border-radius: 8px;
+            padding: 20px;
+            text-align: center;
+            background: #fafbfc;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            
+            &:hover {
+              border-color: #007aff;
+              background: #f0f6ff;
+            }
+            
+            .upload-icon {
+              font-size: 24px;
+              color: #8e8e93;
+              margin-bottom: 8px;
+            }
+            
+            .upload-text {
+              font-size: 14px;
+              color: #666;
+              margin-bottom: 4px;
+            }
+            
+            .upload-hint {
+              font-size: 12px;
+              color: #8e8e93;
+            }
+          }
+          
+          .image-preview {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-top: 12px;
+            
+            .preview-item {
+              position: relative;
+              width: 80px;
+              height: 80px;
+              border-radius: 6px;
+              overflow: hidden;
+              border: 1px solid #e9ecef;
+              
+              img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+              }
+              
+              .remove-btn {
+                position: absolute;
+                top: 4px;
+                right: 4px;
+                width: 20px;
+                height: 20px;
+                background: rgba(255, 59, 48, 0.9);
+                color: white;
+                border: none;
+                border-radius: 50%;
+                cursor: pointer;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                font-size: 12px;
+                
+                &:hover {
+                  background: #ff3b30;
+                }
+              }
+            }
+          }
+          
+          .progress-info {
+            .progress-item {
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              padding: 8px 0;
+              border-bottom: 1px solid #f0f0f0;
+              
+              &:last-child {
+                border-bottom: none;
+              }
+              
+              .progress-label {
+                font-size: 14px;
+                color: #666;
+              }
+              
+              .progress-value {
+                font-size: 14px;
+                font-weight: 500;
+                color: #1d1d1f;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+/* 交付执行面板样式 */
+.delivery-execution-panel {
+  width: 100%;
+  
+  .delivery-processes-container {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    
+    .delivery-process-card {
+      background: white;
+      border: 1px solid #e9ecef;
+      border-radius: 12px;
+      overflow: hidden;
+      transition: all 0.3s ease;
+      
+      &:hover {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+      }
+      
+      .process-header {
+        padding: 16px 20px;
+        background: #f8f9fa;
+        border-bottom: 1px solid #e9ecef;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        transition: background-color 0.2s ease;
+        
+        &:hover {
+          background: #e9ecef;
+        }
+        
+        h4 {
+          margin: 0;
+          font-size: 16px;
+          font-weight: 600;
+          color: #1d1d1f;
+        }
+        
+        .expand-icon {
+          transition: transform 0.3s ease;
+          color: #666;
+          
+          &.rotated {
+            transform: rotate(180deg);
+          }
+        }
+      }
+      
+      .spaces-list {
+        padding: 0;
+        max-height: 0;
+        overflow: hidden;
+        transition: max-height 0.3s ease, padding 0.3s ease;
+        
+        .space-item {
+          padding: 12px 20px;
+          border-bottom: 1px solid #f0f0f0;
+          cursor: pointer;
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          transition: background-color 0.2s ease;
+          
+          &:hover {
+            background: #f8f9fa;
+          }
+          
+          &.active {
+            background: #e6f3ff;
+            border-left: 3px solid #007aff;
+          }
+          
+          &:last-child {
+            border-bottom: none;
+          }
+          
+          .space-name {
+            font-size: 14px;
+            color: #333;
+            font-weight: 500;
+          }
+          
+          .remove-space-btn {
+            background: none;
+            border: none;
+            color: #ff3b30;
+            cursor: pointer;
+            padding: 4px;
+            border-radius: 4px;
+            transition: background-color 0.2s ease;
+            
+            &:hover {
+              background: rgba(255, 59, 48, 0.1);
+            }
+          }
+        }
+        
+        .add-space-input {
+          padding: 12px 20px;
+          border-top: 1px solid #f0f0f0;
+          
+          input {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 6px;
+            font-size: 14px;
+            
+            &:focus {
+              outline: none;
+              border-color: #007aff;
+              box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
+            }
+          }
+          
+          .input-actions {
+            display: flex;
+            gap: 8px;
+            margin-top: 8px;
+            
+            button {
+              padding: 6px 12px;
+              border: none;
+              border-radius: 4px;
+              font-size: 12px;
+              cursor: pointer;
+              transition: all 0.2s ease;
+              
+              &.confirm-btn {
+                background: #007aff;
+                color: white;
+                
+                &:hover {
+                  background: #0056b3;
+                }
+              }
+              
+              &.cancel-btn {
+                background: #f0f0f0;
+                color: #666;
+                
+                &:hover {
+                  background: #e0e0e0;
+                }
+              }
+            }
+          }
+        }
+        
+        .add-space-btn {
+          padding: 12px 20px;
+          border: none;
+          background: none;
+          color: #007aff;
+          cursor: pointer;
+          font-size: 14px;
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          width: 100%;
+          text-align: left;
+          transition: background-color 0.2s ease;
+          
+          &:hover {
+            background: #f8f9fa;
+          }
+        }
+      }
+      
+      &.expanded {
+        .spaces-list {
+          max-height: 500px;
+          padding: 8px 0;
+        }
+        
+        .process-content {
+          padding: 20px;
+          background: #fafbfc;
+          border-top: 1px solid #e9ecef;
+          
+          .space-content {
+            background: white;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 16px;
+            border: 1px solid #e9ecef;
+            
+            &:last-child {
+              margin-bottom: 0;
+            }
+            
+            .space-content-header {
+              margin-bottom: 16px;
+              padding-bottom: 12px;
+              border-bottom: 1px solid #f0f0f0;
+              
+              h5 {
+                margin: 0;
+                font-size: 16px;
+                font-weight: 600;
+                color: #1d1d1f;
+              }
+            }
+            
+            .modeling-content,
+            .soft-decor-content,
+            .rendering-content {
+              .upload-section {
+                margin-bottom: 20px;
+                
+                .upload-header {
+                  display: flex;
+                  align-items: center;
+                  justify-content: space-between;
+                  margin-bottom: 12px;
+                  
+                  h6 {
+                    margin: 0;
+                    font-size: 14px;
+                    font-weight: 600;
+                    color: #333;
+                  }
+                  
+                  .hint {
+                    font-size: 12px;
+                    color: #666;
+                  }
+                }
+                
+                .uploaded-images-grid {
+                  display: grid;
+                  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+                  gap: 12px;
+                  
+                  .uploaded-image-item {
+                    position: relative;
+                    aspect-ratio: 1;
+                    border-radius: 8px;
+                    overflow: hidden;
+                    cursor: pointer;
+                    transition: transform 0.2s ease;
+                    
+                    &:hover {
+                      transform: scale(1.02);
+                      
+                      .image-overlay {
+                        opacity: 1;
+                      }
+                    }
+                    
+                    img {
+                      width: 100%;
+                      height: 100%;
+                      object-fit: cover;
+                    }
+                    
+                    .image-overlay {
+                      position: absolute;
+                      top: 0;
+                      left: 0;
+                      right: 0;
+                      bottom: 0;
+                      background: rgba(0, 0, 0, 0.7);
+                      display: flex;
+                      flex-direction: column;
+                      justify-content: space-between;
+                      padding: 8px;
+                      opacity: 0;
+                      transition: opacity 0.2s ease;
+                      
+                      .image-name {
+                        color: white;
+                        font-size: 11px;
+                        font-weight: 500;
+                        text-overflow: ellipsis;
+                        overflow: hidden;
+                        white-space: nowrap;
+                      }
+                      
+                      .image-actions {
+                        display: flex;
+                        gap: 4px;
+                        justify-content: flex-end;
+                        
+                        button {
+                          background: rgba(255, 255, 255, 0.2);
+                          border: none;
+                          color: white;
+                          padding: 4px;
+                          border-radius: 4px;
+                          cursor: pointer;
+                          transition: background-color 0.2s ease;
+                          
+                          &:hover {
+                            background: rgba(255, 255, 255, 0.3);
+                          }
+                          
+                          &.remove-btn:hover {
+                            background: rgba(255, 59, 48, 0.8);
+                          }
+                        }
+                      }
+                    }
+                  }
+                  
+                  .add-more-btn {
+                    aspect-ratio: 1;
+                    border: 2px dashed #d0d7de;
+                    border-radius: 8px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    cursor: pointer;
+                    transition: all 0.2s ease;
+                    background: #fafbfc;
+                    
+                    &:hover {
+                      border-color: #007aff;
+                      background: #f0f6ff;
+                    }
+                    
+                    .add-icon {
+                      font-size: 24px;
+                      color: #666;
+                      font-weight: 300;
+                    }
+                  }
+                }
+              }
+              
+              .progress-section {
+                .progress-header {
+                  display: flex;
+                  align-items: center;
+                  justify-content: space-between;
+                  margin-bottom: 12px;
+                  
+                  h6 {
+                    margin: 0;
+                    font-size: 14px;
+                    font-weight: 600;
+                    color: #333;
+                  }
+                  
+                  .progress-value {
+                    font-size: 14px;
+                    font-weight: 600;
+                    color: #007aff;
+                  }
+                }
+                
+                .progress-bar {
+                  width: 100%;
+                  height: 8px;
+                  background: #f0f0f0;
+                  border-radius: 4px;
+                  overflow: hidden;
+                  margin-bottom: 12px;
+                  
+                  .progress-fill {
+                    height: 100%;
+                    background: linear-gradient(90deg, #007aff 0%, #4a9eff 100%);
+                    transition: width 0.3s ease;
+                  }
+                }
+                
+                .status-info {
+                  display: flex;
+                  align-items: center;
+                  gap: 8px;
+                  font-size: 13px;
+                  color: #666;
+                  
+                  .status-dot {
+                    width: 8px;
+                    height: 8px;
+                    border-radius: 50%;
+                    background: #34c759;
+                    
+                    &.pending {
+                      background: #ff9500;
+                    }
+                    
+                    &.in-progress {
+                      background: #007aff;
+                    }
+                    
+                    &.completed {
+                      background: #34c759;
+                    }
+                  }
+                }
+              }
+              
+              .notes-section {
+                margin-top: 16px;
+                
+                .notes-header {
+                  margin-bottom: 8px;
+                  
+                  h6 {
+                    margin: 0;
+                    font-size: 14px;
+                    font-weight: 600;
+                    color: #333;
+                  }
+                }
+                
+                .notes-content {
+                  background: #f8f9fa;
+                  border: 1px solid #e9ecef;
+                  border-radius: 6px;
+                  padding: 12px;
+                  font-size: 13px;
+                  line-height: 1.5;
+                  color: #666;
+                  min-height: 60px;
+                  
+                  &:empty::before {
+                    content: '暂无备注';
+                    color: #999;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .horizontal-accordion {
+    flex-direction: column;
+    gap: 12px;
+    
+    .process-panel {
+      min-width: unset;
+      width: 100%;
+      
+      &.expanded {
+        min-width: unset;
+      }
+    }
+  }
+  
+  .delivery-execution-panel {
+    .delivery-processes-container {
+      gap: 12px;
+      
+      .delivery-process-card {
+        .process-header {
+          padding: 12px 16px;
+          
+          h4 {
+            font-size: 15px;
+          }
+        }
+        
+        .spaces-list {
+          .space-item {
+            padding: 10px 16px;
+            
+            .space-name {
+              font-size: 13px;
+            }
+          }
+          
+          .add-space-input {
+            padding: 10px 16px;
+          }
+          
+          .add-space-btn {
+            padding: 10px 16px;
+            font-size: 13px;
+          }
+        }
+      }
+    }
+  }
 }

+ 263 - 0
src/app/pages/designer/project-detail/project-detail.ts

@@ -185,6 +185,32 @@ interface ProposalAnalysis {
   };
 }
 
+// 交付执行板块数据结构
+interface DeliverySpace {
+  id: string;
+  name: string; // 空间名称:卧室、餐厅、厨房等
+  isExpanded: boolean; // 是否展开
+  order: number; // 排序
+}
+
+interface DeliveryProcess {
+  id: string;
+  name: string; // 流程名称:建模、软装、渲染
+  type: 'modeling' | 'softDecor' | 'rendering';
+  spaces: DeliverySpace[]; // 该流程下的空间列表
+  isExpanded: boolean; // 是否展开
+  content: {
+    [spaceId: string]: {
+      // 每个空间的具体内容
+      images: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected' }>;
+      progress: number; // 进度百分比
+      status: 'pending' | 'in_progress' | 'completed' | 'approved';
+      notes: string; // 备注
+      lastUpdated: Date;
+    };
+  };
+}
+
 @Component({
   selector: 'app-project-detail',
   standalone: true,
@@ -301,6 +327,50 @@ export class ProjectDetail implements OnInit, OnDestroy {
   // 视图上下文:根据路由前缀识别角色视角(客服/设计师/组长)
   roleContext: 'customer-service' | 'designer' | 'team-leader' = 'designer';
 
+  // ============ 交付执行板块数据 ============
+  deliveryProcesses: DeliveryProcess[] = [
+    {
+      id: 'modeling',
+      name: '建模',
+      type: 'modeling',
+      isExpanded: false,
+      spaces: [
+        { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
+        { id: 'living', name: '客厅', isExpanded: false, order: 2 },
+        { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
+      ],
+      content: {}
+    },
+    {
+      id: 'softDecor',
+      name: '软装',
+      type: 'softDecor',
+      isExpanded: false,
+      spaces: [
+        { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
+        { id: 'living', name: '客厅', isExpanded: false, order: 2 },
+        { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
+      ],
+      content: {}
+    },
+    {
+      id: 'rendering',
+      name: '渲染',
+      type: 'rendering',
+      isExpanded: false,
+      spaces: [
+        { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
+        { id: 'living', name: '客厅', isExpanded: false, order: 2 },
+        { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
+      ],
+      content: {}
+    }
+  ];
+
+  // 新增空间输入框状态
+  newSpaceName: { [processId: string]: string } = {};
+  showAddSpaceInput: { [processId: string]: boolean } = {};
+
   constructor(
     private route: ActivatedRoute,
     private projectService: ProjectService,
@@ -3048,4 +3118,197 @@ export class ProjectDetail implements OnInit, OnDestroy {
       });
     }, 2000);
   }
+
+  // 横向折叠面板相关方法
+  
+  // 切换流程展开状态
+  toggleProcess(processId: string): void {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (process) {
+      process.isExpanded = !process.isExpanded;
+      // 如果展开,关闭其他流程
+      if (process.isExpanded) {
+        this.deliveryProcesses.forEach(p => {
+          if (p.id !== processId) {
+            p.isExpanded = false;
+          }
+        });
+      }
+    }
+  }
+
+  // 切换空间展开状态
+  toggleSpace(processId: string, spaceId: string): void {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (process) {
+      const space = process.spaces.find(s => s.id === spaceId);
+      if (space) {
+        space.isExpanded = !space.isExpanded;
+        // 如果展开,关闭同流程下的其他空间
+        if (space.isExpanded) {
+          process.spaces.forEach(s => {
+            if (s.id !== spaceId) {
+              s.isExpanded = false;
+            }
+          });
+        }
+      }
+    }
+  }
+
+  // 显示添加空间表单
+  showAddSpaceForm(processId: string): void {
+    this.showAddSpaceInput[processId] = true;
+    this.newSpaceName[processId] = '';
+  }
+
+  // 添加新空间
+  addSpace(processId: string): void {
+    if (!this.newSpaceName[processId]?.trim()) {
+      return;
+    }
+
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (process) {
+      const newSpace: DeliverySpace = {
+        id: 'space-' + Date.now(),
+        name: this.newSpaceName[processId].trim(),
+        isExpanded: false,
+        order: process.spaces.length + 1
+      };
+      
+      process.spaces.push(newSpace);
+      
+      // 初始化空间内容
+      process.content[newSpace.id] = {
+        images: [],
+        progress: 0,
+        status: 'pending',
+        notes: '',
+        lastUpdated: new Date()
+      };
+      
+      // 重置表单
+      this.newSpaceName[processId] = '';
+      this.showAddSpaceInput[processId] = false;
+    }
+  }
+
+  // 取消添加空间
+  cancelAddSpace(processId: string): void {
+    this.showAddSpaceInput[processId] = false;
+    this.newSpaceName[processId] = '';
+  }
+
+  // 删除空间
+  removeSpace(processId: string, spaceId: string): void {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (process) {
+      // 确认删除
+      if (confirm('确定要删除这个空间吗?')) {
+        process.spaces = process.spaces.filter(s => s.id !== spaceId);
+        delete process.content[spaceId];
+      }
+    }
+  }
+
+  // 触发空间文件上传
+  triggerSpaceFileInput(processId: string, spaceId: string): void {
+    const inputId = `space-file-input-${processId}-${spaceId}`;
+    const input = document.getElementById(inputId) as HTMLInputElement;
+    if (input) {
+      input.click();
+    }
+  }
+
+  // 处理空间文件选择
+  onSpaceFileSelected(event: Event, processId: string, spaceId: string): void {
+    const input = event.target as HTMLInputElement;
+    const files = input.files;
+    
+    if (files && files.length > 0) {
+      const process = this.deliveryProcesses.find(p => p.id === processId);
+      if (process && process.content[spaceId]) {
+        Array.from(files).forEach(file => {
+          // 验证文件类型
+          if (!file.type.startsWith('image/')) {
+            alert(`文件 ${file.name} 不是有效的图片格式`);
+            return;
+          }
+          
+          // 验证文件大小(限制为10MB)
+          if (file.size > 10 * 1024 * 1024) {
+            alert(`文件 ${file.name} 大小超过10MB限制`);
+            return;
+          }
+          
+          const imageItem = {
+            id: 'img-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
+            name: file.name,
+            url: URL.createObjectURL(file),
+            size: this.formatFileSize(file.size),
+            reviewStatus: 'pending' as const
+          };
+          
+          process.content[spaceId].images.push(imageItem);
+          process.content[spaceId].lastUpdated = new Date();
+        });
+      }
+    }
+    
+    // 重置input
+    input.value = '';
+  }
+
+  // 删除空间图片
+  removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (process && process.content[spaceId]) {
+      const imageIndex = process.content[spaceId].images.findIndex(img => img.id === imageId);
+      if (imageIndex > -1) {
+        const image = process.content[spaceId].images[imageIndex];
+        // 释放URL对象
+        URL.revokeObjectURL(image.url);
+        // 从数组中移除
+        process.content[spaceId].images.splice(imageIndex, 1);
+        process.content[spaceId].lastUpdated = new Date();
+      }
+    }
+  }
+
+  // 获取流程状态文本
+  getProcessStatusText(processId: string): string {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (!process) return '';
+    
+    const totalSpaces = process.spaces.length;
+    const completedSpaces = process.spaces.filter(space => {
+      const content = process.content[space.id];
+      return content && content.status === 'completed';
+    }).length;
+    
+    if (completedSpaces === 0) return '未开始';
+    if (completedSpaces === totalSpaces) return '已完成';
+    return `进行中 (${completedSpaces}/${totalSpaces})`;
+  }
+
+  // 获取流程进度百分比
+  getProcessProgress(processId: string): number {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (!process || process.spaces.length === 0) return 0;
+    
+    const totalSpaces = process.spaces.length;
+    const completedSpaces = process.spaces.filter(space => {
+      const content = process.content[space.id];
+      return content && content.status === 'completed';
+    }).length;
+    
+    return Math.round((completedSpaces / totalSpaces) * 100);
+  }
+
+  // 获取空间内容
+  getSpaceContent(processId: string, spaceId: string): any {
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    return process?.content[spaceId] || null;
+  }
 }