Browse Source

feat(project-detail): 添加后期处理阶段并优化样式和交互

- 在项目详情页新增后期处理阶段,包括图片上传和状态管理功能
- 将四大板块按钮组移至标题下方,优化视觉层次
- 调整左右列宽度比例和间距,优化信息展示布局
- 新增后期处理图片上传功能,完善项目流程阶段管理
0235711 8 hours ago
parent
commit
8228d7669a

+ 155 - 22
src/app/pages/designer/project-detail/debug-styles.scss

@@ -56,27 +56,27 @@
 
 /* 强制覆盖左侧列样式 - 使用最高优先级 */
 .progress-tab-content > .main-content-layout > .left-column {
-  width: 33.333% !important;
-  min-width: 33.333% !important;
-  max-width: 33.333% !important;
+  width: 28% !important;
+  min-width: 28% !important;
+  max-width: 28% !important;
   display: flex !important;
   flex-direction: column !important;
-  gap: 20px !important;
+  gap: 16px !important;
   background-color: transparent !important; // 去除左侧粉色底色
-  padding: 10px !important;
+  padding: 8px !important;
   border-radius: 6px !important;
 }
 
 /* 强制覆盖右侧列样式 - 使用最高优先级 */
 .progress-tab-content > .main-content-layout > .right-column {
-  width: 66.667% !important;
-  min-width: 66.667% !important;
-  max-width: 66.667% !important;
+  width: 72% !important;
+  min-width: 72% !important;
+  max-width: 72% !important;
   display: flex !important;
   flex-direction: column !important;
-  gap: 20px !important;
+  gap: 16px !important;
   background-color: transparent !important; // 去除右侧调试底色
-  padding: 10px !important;
+  padding: 8px !important;
   border-radius: 6px !important;
 }
 
@@ -85,11 +85,91 @@
 .right-column .card {
   background-color: white !important;
   border-radius: 8px !important;
-  padding: 20px !important;
+  padding: 16px !important;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
   border: 1px solid #eaeaea !important;
 }
 
+/* 客户信息卡片内部元素优化 */
+.left-column .project-info-card .info-grid {
+  display: grid !important;
+  grid-template-columns: 1fr 1fr !important;
+  gap: 8px !important;
+  margin-bottom: 12px !important;
+}
+
+.left-column .project-info-card .info-item {
+  display: flex !important;
+  flex-direction: column !important;
+  padding: 8px !important;
+  background: #f8f9fa !important;
+  border-radius: 6px !important;
+  border: 1px solid #e9ecef !important;
+  font-size: 12px !important;
+}
+
+.left-column .project-info-card .info-item label {
+  font-weight: 500 !important;
+  color: #6c757d !important;
+  margin-bottom: 4px !important;
+  font-size: 11px !important;
+}
+
+.left-column .project-info-card .info-item span {
+  color: #212529 !important;
+  font-weight: 600 !important;
+  font-size: 12px !important;
+}
+
+.left-column .project-info-card .tags-container {
+  margin-top: 8px !important;
+}
+
+.left-column .project-info-card .tag-section {
+  margin-bottom: 8px !important;
+}
+
+.left-column .project-info-card .tag-section h3 {
+  font-size: 12px !important;
+  font-weight: 600 !important;
+  color: #495057 !important;
+  margin-bottom: 6px !important;
+}
+
+.left-column .project-info-card .tags .tag {
+  font-size: 10px !important;
+  padding: 4px 8px !important;
+  margin: 2px 4px 2px 0 !important;
+  background-color: rgba(24, 144, 255, 0.08) !important;
+  color: #1890ff !important;
+  border-radius: 4px !important;
+}
+
+.left-column .project-info-card .need-list {
+  margin: 0 !important;
+  padding-left: 12px !important;
+}
+
+.left-column .project-info-card .need-list li {
+  font-size: 11px !important;
+  color: #495057 !important;
+  margin-bottom: 3px !important;
+}
+
+.left-column .project-info-card .requirement-sync-info {
+  margin-top: 8px !important;
+  padding: 8px !important;
+  background: #f1f3f4 !important;
+  border-radius: 6px !important;
+}
+
+.left-column .project-info-card .requirement-sync-info h4 {
+  font-size: 11px !important;
+  font-weight: 600 !important;
+  color: #495057 !important;
+  margin-bottom: 6px !important;
+}
+
 /* 确保响应式布局正常工作 */
 @media (max-width: 1024px) {
   .progress-tab-content > .main-content-layout {
@@ -279,20 +359,73 @@
 
 /* 阶段卡片横向排列(按板块的阶段数量自适应列数) */
 .stage-progress-container {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
-  gap: $ios-spacing-lg;
-  align-items: stretch; // 保证同一行的卡片等高
+  display: grid !important;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) !important;
+  gap: 12px !important;
+  align-items: stretch !important; // 保证同一行的卡片等高
 }
 
 .vertical-stage-block {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
+  display: flex !important;
+  flex-direction: column !important;
+  height: 100% !important;
+  padding: 12px !important;
+  background: white !important;
+  border-radius: 8px !important;
+  border: 1px solid #e9ecef !important;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important;
+}
+
+.vertical-stage-header {
+  display: flex !important;
+  align-items: center !important;
+  gap: 8px !important;
+  margin-bottom: 8px !important;
+  padding-bottom: 6px !important;
+  border-bottom: 1px solid #f1f3f4 !important;
+}
+
+.vertical-stage-header h3 {
+  font-size: 13px !important;
+  font-weight: 600 !important;
+  color: #495057 !important;
+  margin: 0 !important;
+}
+
+.vertical-stage-header .dot {
+  width: 8px !important;
+  height: 8px !important;
+  border-radius: 50% !important;
+  background: #dee2e6 !important;
+  flex-shrink: 0 !important;
+}
+
+.vertical-stage-header .dot.completed {
+  background: #28a745 !important;
+}
+
+.vertical-stage-header .dot.active {
+  background: #007bff !important;
 }
+
 .vertical-stage-body {
-  display: flex;
-  flex-direction: column;
-  gap: $ios-spacing-md;
-  flex: 1 1 auto;
+  display: flex !important;
+  flex-direction: column !important;
+  gap: 8px !important;
+  flex: 1 1 auto !important;
+}
+
+/* 活动阶段卡片优化 */
+.vertical-stage-block.active {
+  background: #fff8f0 !important;
+  border-color: #ffc069 !important;
+  box-shadow: 0 2px 8px rgba(255, 192, 105, 0.2) !important;
+}
+
+.vertical-stage-block.active .vertical-stage-header h3 {
+  color: #d46b08 !important;
+}
+
+.vertical-stage-block.active .dot {
+  background: #fa8c16 !important;
 }

+ 84 - 15
src/app/pages/designer/project-detail/project-detail.html

@@ -87,6 +87,19 @@
     </div>
 </div>
 
+<!-- 四大板块按钮组 - 位于项目详情标题区域正下方 -->
+<div class="sections-toolbar-header">
+  @for (sec of sections; track sec.key) {
+    <button class="section-btn"
+            [class.completed]="getSectionStatus(sec.key) === 'completed'"
+            [class.active]="getSectionStatus(sec.key) === 'active'"
+            [class.pending]="getSectionStatus(sec.key) === 'pending'"
+            (click)="toggleSection(sec.key)">
+      <span class="section-label">{{ sec.label }}</span>
+    </button>
+  }
+</div>
+
 <!-- 图片预览模态框 -->
 @if (showImagePreview) {
   <div class="image-preview-modal" (click)="closeImagePreview()">
@@ -238,19 +251,6 @@
             <div class="process-card card">
               <h2>制作流程进度</h2>
 
-              <!-- 新增:四大板块矩形按钮 -->
-              <div class="sections-toolbar">
-                @for (sec of sections; track sec.key) {
-                  <button class="section-btn"
-                          [class.completed]="getSectionStatus(sec.key) === 'completed'"
-                          [class.active]="getSectionStatus(sec.key) === 'active'"
-                          [class.pending]="getSectionStatus(sec.key) === 'pending'"
-                          (click)="toggleSection(sec.key)">
-                    <span class="section-label">{{ sec.label }}</span>
-                  </button>
-                }
-              </div>
-
               <!-- 串式流程:10个阶段横向排列(保持) -->
               <div class="stage-progress-container">
                 @for (stage of getVisibleStages(); track stage) {
@@ -511,9 +511,78 @@
                                        style="display: none;" />
                               </div>
                             }
-                            <div class="upload-actions" style="display:flex;gap:12px;align-items:center;">
+                            <div class="upload-actions">
+                              @if (isDesignerView()) {
+                                <button class="primary-btn" [disabled]="renderLargeImages.length===0" (click)="confirmRenderUpload()">确认上传</button>
+                              }
                               @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('render')">同步图片信息</button> }
-                              @if (renderLargeImages.length>0) { <span class="desc">已上传 {{renderLargeImages.length}} 张</span> }
+                              @if (isCustomerServiceView()) { <span class="desc">只读</span> }
+                            </div>
+                          </div>
+                        </div>
+                      } @else if (stage === '后期') {
+                        <div class="post-process-section">
+                          <div class="upload-section">
+                            <div class="upload-header">
+                              <h4>上传后期处理图片</h4>
+                              <span class="hint">包含调色、修图、特效等后期处理结果</span>
+                            </div>
+                            @if (isDesignerView()) {
+                              <div class="upload-dropzone" 
+                                   (click)="postProcessImages.length === 0 ? triggerFileInput('postProcess') : null"
+                                   (dragover)="postProcessImages.length === 0 ? onDragOver($event) : null"
+                                   (dragleave)="postProcessImages.length === 0 ? onDragLeave($event) : null"
+                                   (drop)="postProcessImages.length === 0 ? onFileDrop($event, 'postProcess') : null"
+                                   [class.drag-over]="isDragOver && postProcessImages.length === 0"
+                                   [class.has-images]="postProcessImages.length > 0">
+                                @if (postProcessImages.length === 0) {
+                                  <div class="upload-icon"></div>
+                                  <div class="upload-text">点击此处或拖拽文件到此处上传</div>
+                                  <div class="upload-hint">支持 JPG、PNG 格式,展示后期处理效果</div>
+                                } @else {
+                                  <div class="uploaded-images-grid">
+                                    @for (img of postProcessImages; 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(); removePostProcessImage(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)="triggerFileInput('postProcess')">
+                                      <div class="add-icon">+</div>
+                                      <div class="add-text">添加更多</div>
+                                    </div>
+                                  </div>
+                                }
+                                <input type="file" 
+                                       id="postProcessFileInput"
+                                       accept="{{allowedImageTypes}}" 
+                                       multiple 
+                                       (change)="onPostProcessPicsSelected($event)" 
+                                       style="display: none;" />
+                              </div>
+                            }
+                            <div class="upload-actions">
+                              @if (isDesignerView()) {
+                                <button class="primary-btn" [disabled]="postProcessImages.length===0" (click)="confirmPostProcessUpload()">确认上传</button>
+                              }
+                              @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('postProcess')">同步图片信息</button> }
+                              @if (isCustomerServiceView()) { <span class="desc">只读</span> }
                             </div>
                           </div>
                         </div>

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

@@ -11,6 +11,65 @@
   overflow-x: auto; // 允许水平滚动以防内容过宽
 }
 
+/* 四大板块按钮组 - 位于项目标题区域正下方 */
+.sections-toolbar-header {
+  display: flex;
+  gap: 12px;
+  width: 100%;
+  margin: 20px 0 24px 0;
+  padding: 0 24px; /* 与项目详情容器保持一致的左右边距 */
+  
+  .section-btn {
+    flex: 1;
+    appearance: none;
+    border: 1px solid $ios-border;
+    background: $ios-background;
+    color: $ios-text-primary;
+    padding: 12px 16px;
+    border-radius: $ios-radius-md;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    text-align: center;
+    min-height: 44px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    
+    .section-label {
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    
+    &:hover {
+      transform: translateY(-1px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    }
+    
+    &.completed {
+      background: linear-gradient(135deg, #e6f7e6 0%, #d4edda 100%);
+      color: $ios-success;
+      border-color: $ios-success;
+      box-shadow: 0 2px 8px rgba(40, 167, 69, 0.15);
+    }
+    
+    &.active {
+      background: linear-gradient(135deg, #e8f0fe 0%, #cce7ff 100%);
+      color: $ios-primary;
+      border-color: $ios-primary;
+      box-shadow: 0 2px 8px rgba(0, 122, 255, 0.2);
+    }
+    
+    &.pending {
+      background: $ios-background;
+      color: $ios-text-secondary;
+      border-color: rgba(0, 0, 0, 0.1);
+    }
+  }
+}
+
 // 图片预览模态框样式
 .image-preview-modal {
   position: fixed;

+ 53 - 15
src/app/pages/designer/project-detail/project-detail.ts

@@ -84,7 +84,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   showDropdown: boolean = false;
   currentStage: string = '';
   // 新增:10阶段顺序(串式流程)
-  stageOrder: ProjectStage[] = ['订单创建', '需求沟通', '方案确认', '建模', '软装', '渲染', '尾款结算', '客户评价', '投诉处理'];
+  stageOrder: ProjectStage[] = ['订单创建', '需求沟通', '方案确认', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价', '投诉处理'];
   // 新增:阶段展开状态(默认全部收起,当前阶段在数据加载后自动展开)
   expandedStages: Partial<Record<ProjectStage, boolean>> = {
     '订单创建': false,
@@ -103,7 +103,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   sections: Array<{ key: SectionKey; label: string; stages: ProjectStage[] }> = [
     { key: 'order', label: '订单创建', stages: ['订单创建'] },
     { key: 'requirements', label: '确认需求', stages: ['需求沟通', '方案确认'] },
-    { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染'] },
+    { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染', '后期'] },
     { key: 'aftercare', label: '售后', stages: ['尾款结算', '客户评价', '投诉处理'] }
   ];
   expandedSection: SectionKey | null = null;
@@ -145,6 +145,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   whiteModelImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   softDecorImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   renderLargeImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
+  postProcessImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   showRenderUploadModal: boolean = false;
   pendingRenderLargeItems: Array<{ id: string; name: string; url: string; file: File }> = [];
 
@@ -342,7 +343,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   }
 
   // ============ 组长:同步上传与审核(新增,模拟实现) ============
-  syncUploadedImages(phase: 'white' | 'soft' | 'render'): void {
+  syncUploadedImages(phase: 'white' | 'soft' | 'render' | 'postProcess'): void {
     if (!this.isTeamLeaderView()) return;
     const markSynced = (arr: Array<{ reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
       arr.forEach(img => {
@@ -353,10 +354,11 @@ export class ProjectDetail implements OnInit, OnDestroy {
     if (phase === 'white') markSynced(this.whiteModelImages);
     if (phase === 'soft') markSynced(this.softDecorImages);
     if (phase === 'render') markSynced(this.renderLargeImages);
+    if (phase === 'postProcess') markSynced(this.postProcessImages);
     alert('已同步该阶段的图片信息(模拟)');
   }
 
-  reviewImage(imageId: string, phase: 'white' | 'soft' | 'render', status: 'approved' | 'rejected'): void {
+  reviewImage(imageId: string, phase: 'white' | 'soft' | 'render' | 'postProcess', status: 'approved' | 'rejected'): void {
     if (!this.isTeamLeaderView()) return;
     const setStatus = (arr: Array<{ id: string; reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
       const target = arr.find(i => i.id === imageId);
@@ -368,6 +370,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
     if (phase === 'white') setStatus(this.whiteModelImages);
     if (phase === 'soft') setStatus(this.softDecorImages);
     if (phase === 'render') setStatus(this.renderLargeImages);
+    if (phase === 'postProcess') setStatus(this.postProcessImages);
   }
 
   getImageReviewStatusText(img: { reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }): string {
@@ -1183,6 +1186,8 @@ export class ProjectDetail implements OnInit, OnDestroy {
         this.removeSoftDecorImage(this.previewImageData.id);
       } else if (this.renderLargeImages.find(i => i.id === this.previewImageData.id)) {
         this.removeRenderLargeImage(this.previewImageData.id);
+      } else if (this.postProcessImages.find(i => i.id === this.previewImageData.id)) {
+        this.removePostProcessImage(this.previewImageData.id);
       }
       this.closeImagePreview();
     }
@@ -1201,7 +1206,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
     this.isDragOver = false;
   }
 
-  onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render'): void {
+  onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
     event.preventDefault();
     event.stopPropagation();
     this.isDragOver = false;
@@ -1227,11 +1232,14 @@ export class ProjectDetail implements OnInit, OnDestroy {
       case 'render':
         this.onRenderLargePicsSelected(mockEvent);
         break;
+      case 'postProcess':
+        this.onPostProcessPicsSelected(mockEvent);
+        break;
     }
   }
 
   // 触发文件输入框
-  triggerFileInput(type: 'whiteModel' | 'softDecor' | 'render'): void {
+  triggerFileInput(type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
     let inputId: string;
     switch (type) {
       case 'whiteModel':
@@ -1243,6 +1251,9 @@ export class ProjectDetail implements OnInit, OnDestroy {
       case 'render':
         inputId = 'renderFileInput';
         break;
+      case 'postProcess':
+        inputId = 'postProcessFileInput';
+        break;
     }
     const input = document.querySelector(`#${inputId}`) as HTMLInputElement;
     if (input) {
@@ -1256,12 +1267,48 @@ export class ProjectDetail implements OnInit, OnDestroy {
     this.softDecorImages = this.softDecorImages.filter(i => i.id !== id);
   }
 
+  // 新增:后期阶段图片上传处理
+  async onPostProcessPicsSelected(event: Event): Promise<void> {
+    const input = event.target as HTMLInputElement;
+    if (!input.files || input.files.length === 0) return;
+    const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
+
+    for (const f of files) {
+      const item = this.makeImageItem(f);
+      this.postProcessImages.unshift({ 
+        id: item.id, 
+        name: item.name, 
+        url: item.url, 
+        size: this.formatFileSize(f.size) 
+      });
+    }
+    input.value = '';
+  }
+
+  removePostProcessImage(id: string): void {
+    const target = this.postProcessImages.find(i => i.id === id);
+    if (target) this.revokeUrl(target.url);
+    this.postProcessImages = this.postProcessImages.filter(i => i.id !== id);
+  }
+
+  // 新增:后期阶段确认上传并自动进入下一阶段(尾款结算)
+  confirmPostProcessUpload(): void {
+    if (this.postProcessImages.length === 0) return;
+    this.advanceToNextStage('后期');
+  }
+
   // 新增:软装阶段 确认上传并自动进入下一阶段(渲染)
   confirmSoftDecorUpload(): void {
     if (this.softDecorImages.length === 0) return;
     this.advanceToNextStage('软装');
   }
 
+  // 新增:渲染阶段 确认上传并自动进入下一阶段(后期)
+  confirmRenderUpload(): void {
+    if (this.renderLargeImages.length === 0) return;
+    this.advanceToNextStage('渲染');
+  }
+
   // =========== 渲染阶段:大图上传(弹窗 + 4K校验) ===========
   openRenderUploadModal(): void {
     this.showRenderUploadModal = true;
@@ -1298,15 +1345,6 @@ export class ProjectDetail implements OnInit, OnDestroy {
     input.value = '';
   }
   
-  confirmRenderUpload(): void {
-    // 将待确认的图片加入正式列表(此处模拟上传成功)
-    const toAdd = this.pendingRenderLargeItems.map(i => ({ id: i.id, name: i.name, url: i.url, size: this.formatFileSize(i.file.size) }));
-    this.renderLargeImages.unshift(...toAdd);
-    this.closeRenderUploadModal();
-    // 新增:渲染阶段确认后,自动进入下一阶段(后期)
-    this.advanceToNextStage('渲染');
-  }
-  
   removeRenderLargeImage(id: string): void {
     const target = this.renderLargeImages.find(i => i.id === id);
     if (target) this.revokeUrl(target.url);