瀏覽代碼

fix: quotition-editor

ryanemax 1 天之前
父節點
當前提交
e84a56febc

+ 185 - 32
src/modules/project/components/quotation-editor.component.html

@@ -1,20 +1,83 @@
-<!-- 报价编辑器组件 -->
+<!-- 基于Product表的报价编辑器组件 -->
 <div class="quotation-editor">
-  @if (quotation.spaces.length === 0) {
-    <!-- 空状态 -->
-    <div class="empty-state">
-      <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-        <rect x="112" y="128" width="288" height="320" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
-        <path fill="currentColor" d="M320 304h-32v-24a8 8 0 00-8-8h-48a8 8 0 00-8 8v24h-32a8 8 0 00-8 8v16a8 8 0 008 8h32v24a8 8 0 008 8h48a8 8 0 008-8v-24h32a8 8 0 008-8v-16a8 8 0 00-8-8z"/>
-      </svg>
-      <p class="empty-message">尚未生成报价</p>
-      <p class="empty-hint">请先选择场景并生成报价表</p>
+  <!-- 加载状态 -->
+  @if (loading) {
+    <div class="loading-container">
+      <div class="spinner">
+        <div class="spinner-circle"></div>
+      </div>
+      <p>加载报价数据...</p>
     </div>
-  } @else {
-    <!-- 报价工具栏 -->
+  }
+
+  @if (!loading) {
+    <!-- 产品管理区域 -->
+    @if (canEdit) {
+      <div class="product-management">
+        <div class="product-header">
+          <h3>产品设计产品 ({{ products.length }}个)</h3>
+          <div class="product-actions">
+            <button class="btn-primary" (click)="generateQuotationFromProducts()">
+              <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                <path fill="currentColor" d="M447.1 96h-288C131.3 96 112 115.3 112 140.4v231.2C112 396.7 131.3 416 159.1 416h288c27.6 0 48-19.3 48-44.4V140.4C495.1 115.3 475.6 96 447.1 96zM447.1 144v192h-288V144H447.1zM336 320c17.7 0 32-14.3 32-32s-14.3-32-32-32c-11.4 0-21.4 5.9-27.1 14.9c-7.2-2.4-14.9-3.7-22.9-3.7-30.9 0-56 25.1-56 56s25.1 56 56 56c8 0 15.7-1.3 22.9-3.7C314.6 314.1 324.6 320 336 320z"/>
+                <path fill="currentColor" d="M176 80C176 71.16 167.8 64 160 64H80C71.16 64 64 71.16 64 80s7.163 16 16 16h32L64 192C64 209.7 81.75 224 96 224s32-14.3 32-32L112 96h48C167.8 96 176 88.84 176 80z"/>
+              </svg>
+              生成报价
+            </button>
+            <button class="btn-secondary" (click)="addProduct()">
+              <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM256 48c114.7 0 208 93.31 208 208s-93.31 208-208 208S48 370.7 48 256S141.3 48 256 48zM256 336c13.25 0 24-10.75 24-24V280h32c13.25 0 24-10.75 24-24s-10.75-24-24-24h-32V176c0-13.25-10.75-24-24-24s-24 10.75-24 24v32H200c-13.25 0-24 10.75-24 24s10.75 24 24 24h32v32C232 325.3 242.8 336 256 336z"/>
+              </svg>
+              添加产品
+            </button>
+            <button class="btn-outline" (click)="saveQuotation()">
+              <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                <path fill="currentColor" d="M504.1 141C490.6 121.4 471.1 107.5 447.8 96C424.6 84.51 400.8 80 376.1 80H136c-24.74 0-48.48 4.511-71.79 16.01C40.88 107.5 21.36 121.4 7.85 141C-5.654 160.6-1.466 180.2 11.66 195.7L144.1 353c11.14 13.4 27.62 21 44.8 21h124.3c17.18 0 33.66-7.6 44.8-21l133.3-157.4C504.5 180.2 508.6 160.6 504.1 141zM434.1 165.6L300.7 322.1c-3.734 4.498-9.291 7.059-15.16 7.059H226.5c-5.871 0-11.43-2.561-15.16-7.059L77.86 165.6C72.16 158.7 70.54 149.7 73.65 141.3C76.77 132.9 83.98 126.5 92.95 123.4C107.3 118.7 122.4 116 137.7 116h236.5c15.28 0 30.43 2.687 44.77 7.393c8.972 3.104 16.18 9.516 19.3 17.94C441.5 149.7 439.8 158.7 434.1 165.6z"/>
+              </svg>
+              保存报价
+            </button>
+          </div>
+        </div>
+      </div>
+    }
+
+    @if (quotation.spaces.length === 0 && products.length > 0) {
+      <!-- 空状态 - 有产品但未生成报价 -->
+      <div class="empty-state">
+        <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+          <path fill="currentColor" d="M447.1 96h-288C131.3 96 112 115.3 112 140.4v231.2C112 396.7 131.3 416 159.1 416h288c27.6 0 48-19.3 48-44.4V140.4C495.1 115.3 475.6 96 447.1 96zM447.1 144v192h-288V144H447.1zM336 320c17.7 0 32-14.3 32-32s-14.3-32-32-32c-11.4 0-21.4 5.9-27.1 14.9c-7.2-2.4-14.9-3.7-22.9-3.7-30.9 0-56 25.1-56 56s25.1 56 56 56c8 0 15.7-1.3 22.9-3.7C314.6 314.1 324.6 320 336 320z"/>
+        </svg>
+        <p class="empty-message">尚未生成报价</p>
+        <p class="empty-hint">已加载 {{ products.length }} 个产品设计产品,请点击"生成报价"按钮</p>
+        @if (canEdit) {
+          <button class="btn-primary" (click)="generateQuotationFromProducts()">立即生成报价</button>
+        }
+      </div>
+    } @else if (quotation.spaces.length === 0) {
+      <!-- 完全空状态 - 无产品 -->
+      <div class="empty-state">
+        <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+          <path fill="currentColor" d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 464c-114.7 0-208-93.31-208-208S141.3 48 256 48s208 93.31 208 208S370.7 464 256 464zM256 224c-17.67 0-32 14.33-32 32c0 17.67 14.33 32 32 32s32-14.33 32-32C288 238.3 273.7 224 256 224zM320 128H192c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32v-32C352 142.3 337.7 128 320 128z"/>
+        </svg>
+        <p class="empty-message">暂无产品设计产品</p>
+        <p class="empty-hint">该项目还没有创建任何产品设计产品</p>
+        @if (canEdit) {
+          <button class="btn-primary" (click)="addProduct()">创建第一个产品</button>
+        }
+      </div>
+    } @else {
+      <!-- 报价工具栏 -->
     <div class="quotation-toolbar">
       <div class="toolbar-left">
-        <h4 class="toolbar-title">报价明细 ({{ quotation.spaces.length }}个空间)</h4>
+        <h4 class="toolbar-title">报价明细 ({{ quotation.spaces.length }}个产品设计产品)</h4>
+        <div class="toolbar-meta">
+          @if (quotation.generatedAt) {
+            <span class="generate-time">生成于: {{ quotation.generatedAt | date:'MM-dd HH:mm' }}</span>
+          }
+          @if (quotation.validUntil) {
+            <span class="valid-until">有效期至: {{ quotation.validUntil | date:'yyyy-MM-dd' }}</span>
+          }
+        </div>
       </div>
       <div class="toolbar-right">
         <button class="btn-icon" (click)="expandAll()" title="展开全部">
@@ -32,25 +95,79 @@
 
     <!-- 卡片视图 -->
     @if (viewMode === 'card') {
-      <div class="quotation-spaces">
+      <div class="quotation-products">
         @for (space of quotation.spaces; track space.name) {
-          <div class="space-card" [class.expanded]="isSpaceExpanded(space.name)">
-            <!-- 空间头部 -->
-            <div class="space-header" (click)="toggleSpaceExpand(space.name)">
-              <div class="space-info">
-                <h3 class="space-name">{{ space.name }}</h3>
-                <p class="space-subtotal">小计: ¥{{ calculateSpaceSubtotal(space).toFixed(2) }}</p>
+          <div class="product-card" [class.expanded]="isProductExpanded(space.name)">
+            <!-- 产品头部 -->
+            <div class="product-header" (click)="toggleProductExpand(space.name)">
+              <div class="product-info">
+                <div class="product-title">
+                  <div class="product-icon">
+                    <i class="icon-{{ getProductIconForSpace(space.name) }}"></i>
+                  </div>
+                  <div class="product-details">
+                    <h3 class="product-name">{{ space.name }}</h3>
+                    <div class="product-meta">
+                      <span class="badge" [attr.data-color]="getStatusColorForSpace(space.productId)">
+                        {{ getStatusTextForSpace(space.productId) }}
+                      </span>
+                      <span class="designer-name">{{ getDesignerNameForSpace(space.productId) }}</span>
+                    </div>
+                  </div>
+                </div>
+                <div class="product-pricing">
+                  <p class="product-subtotal">{{ formatPrice(calculateSpaceSubtotal(space)) }}</p>
+                  @if (quotation.spaceBreakdown?.length > 1) {
+                    <span class="percentage">{{ formatPercentage(getSpacePercentage(space.productId)) }}</span>
+                  }
+                </div>
               </div>
-              <div class="space-toggle">
-                <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                  <path fill="currentColor" d="M256 294.1L383 167c9.4-9.4 24.6-9.4 33.9 0s9.3 24.6 0 34L273 345c-9.1 9.1-23.7 9.3-33.1.7L95 201.1c-4.7-4.7-7-10.9-7-17s2.3-12.3 7-17c9.4-9.4 24.6-9.4 33.9 0l127.1 127z"/>
-                </svg>
+              <div class="product-actions">
+                @if (canEdit) {
+                  <button class="btn-icon" (click)="editProduct(space.productId); $event.stopPropagation()" title="编辑">
+                    <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32l39.38 39.38c25 25 25 65.62 0 90.62l-109.5 109.5c-25 25-65.62 25-90.62 0l-109.5-109.5c-25-25-25-65.62 0-90.62L272.1 19.32zM336.5 256.1L227.1 365.5c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l109.5-109.5c12.5-12.5 32.75-12.5 45.25 0S349 243.6 336.5 256.1zM192 416h64v64h-64V416z"/>
+                    </svg>
+                  </button>
+                  <button class="btn-icon danger" (click)="deleteProduct(space.productId); $event.stopPropagation()" title="删除">
+                    <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M96 480c0 17.67 14.33 32 32 32h256c17.67 0 32-14.33 32-32V128H96V480zM312 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24c-13.25 0-24-10.75-24-24V216C288 202.8 298.8 192 312 192zM200 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24s-24-10.75-24-24V216C176 202.8 186.8 192 200 192zM472 64h-80V48c0-26.51-21.49-48-48-48h-176C141.5 0 120 21.49 120 48v64H48c-17.67 0-32 14.33-32 32s14.33 32 32 32h32v352c0 26.51 21.49 48 48 48h256c26.51 0 48-21.49 48-48V128h32c17.67 0 32-14.33 32-32S489.7 64 472 64zM168 48h176v16H168V48zm232 400H112V128h288V448z"/>
+                    </svg>
+                  </button>
+                }
+                <div class="product-toggle">
+                  <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                    <path fill="currentColor" d="M256 294.1L383 167c9.4-9.4 24.6-9.4 33.9 0s9.3 24.6 0 34L273 345c-9.1 9.1-23.7 9.3-33.1.7L95 201.1c-4.7-4.7-7-10.9-7-17s2.3-12.3 7-17c9.4-9.4 24.6-9.4 33.9 0l127.1 127z"/>
+                  </svg>
+                </div>
               </div>
             </div>
 
-            <!-- 空间详情 -->
-            @if (isSpaceExpanded(space.name)) {
-              <div class="space-content">
+                      <!-- 产品详情 -->
+            @if (isProductExpanded(space.name)) {
+              <div class="product-content">
+                <!-- 产品信息 -->
+                @if (getProductForSpace(space.productId)) {
+                  <div class="product-details-section">
+                    <div class="detail-item">
+                      <span class="detail-label">产品类型:</span>
+                      <span class="detail-value">{{ getProductForSpace(space.productId)?.get('productType') }}</span>
+                    </div>
+                    <div class="detail-item">
+                      <span class="detail-label">空间面积:</span>
+                      <span class="detail-value">{{ getProductForSpace(space.productId)?.get('space')?.area || 0 }}㎡</span>
+                    </div>
+                    <div class="detail-item">
+                      <span class="detail-label">复杂度:</span>
+                      <span class="detail-value">{{ getProductForSpace(space.productId)?.get('space')?.complexity || 'medium' }}</span>
+                    </div>
+                    <div class="detail-item">
+                      <span class="detail-label">基础报价:</span>
+                      <span class="detail-value">{{ formatPrice(getProductForSpace(space.productId)?.get('quotation')?.price || 0) }}</span>
+                    </div>
+                  </div>
+                }
+
                 <!-- 工序网格 -->
                 <div class="process-grid">
                   @for (processType of processTypes; track processType.key) {
@@ -199,10 +316,46 @@
       </div>
     }
 
-    <!-- 总价 -->
-    <div class="quotation-total">
-      <div class="total-label">报价总额</div>
-      <div class="total-amount">¥{{ quotation.total.toFixed(2) }}</div>
+      <!-- 报价汇总 -->
+    <div class="quotation-summary">
+      <div class="summary-header">
+        <h4>报价汇总</h4>
+        @if (quotation.spaceBreakdown?.length > 1) {
+          <div class="breakdown-toggle">
+            <button class="btn-text" (click)="showBreakdown = !showBreakdown">
+              {{ showBreakdown ? '隐藏' : '显示'}}明细
+            </button>
+          </div>
+        }
+      </div>
+
+      @if (quotation.spaceBreakdown?.length > 1 && showBreakdown) {
+        <div class="breakdown-list">
+          @for (item of quotation.spaceBreakdown; track item.spaceId) {
+            <div class="breakdown-item">
+              <span class="breakdown-name">{{ item.spaceName }}</span>
+              <span class="breakdown-amount">{{ formatPrice(item.amount) }}</span>
+              <span class="breakdown-percentage">{{ formatPercentage(item.percentage) }}</span>
+            </div>
+          }
+        </div>
+      }
+
+      <div class="total-section">
+        <div class="total-row">
+          <span class="total-label">报价总额</span>
+          <span class="total-amount">{{ formatPrice(quotation.total) }}</span>
+        </div>
+
+        @if (quotation.generatedAt) {
+          <div class="total-meta">
+            <span class="generate-info">生成于 {{ quotation.generatedAt | date:'yyyy-MM-dd HH:mm' }}</span>
+            @if (quotation.validUntil) {
+              <span class="valid-info">有效期至 {{ quotation.validUntil | date:'yyyy-MM-dd' }}</span>
+            }
+          </div>
+        }
+      </div>
     </div>
   }
-</div>
+}

+ 376 - 0
src/modules/project/components/quotation-editor.component.scss

@@ -1,6 +1,68 @@
 .quotation-editor {
   width: 100%;
 
+  // 加载状态
+  .loading-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 24px;
+    text-align: center;
+
+    .spinner {
+      margin-bottom: 16px;
+    }
+
+    p {
+      color: var(--ion-color-medium);
+      margin: 0;
+    }
+  }
+
+  // 产品管理区域
+  .product-management {
+    margin-bottom: 20px;
+
+    .product-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 16px 20px;
+      background: var(--ion-color-light);
+      border-radius: 12px;
+      border: 1px solid var(--ion-color-light-shade);
+
+      h3 {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--ion-color-dark);
+      }
+
+      .product-actions {
+        display: flex;
+        gap: 12px;
+
+        button {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          padding: 8px 16px;
+          border-radius: 8px;
+          font-size: 14px;
+          font-weight: 500;
+          transition: all 0.2s ease;
+
+          .icon {
+            width: 16px;
+            height: 16px;
+          }
+        }
+      }
+    }
+  }
+
   // 空状态
   .empty-state {
     text-align: center;
@@ -501,4 +563,318 @@
       color: white;
     }
   }
+
+  // ============ 产品相关样式 ============
+
+  // 产品卡片
+  .quotation-products {
+    .product-card {
+      border: 1px solid var(--ion-color-light-shade);
+      border-radius: 12px;
+      margin-bottom: 16px;
+      background: white;
+      overflow: hidden;
+      transition: all 0.3s ease;
+
+      &.expanded {
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+      }
+
+      .product-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 16px 20px;
+        cursor: pointer;
+        user-select: none;
+        transition: background-color 0.2s;
+
+        &:hover {
+          background-color: var(--ion-color-light-tint);
+        }
+
+        .product-info {
+          display: flex;
+          align-items: center;
+          gap: 16px;
+          flex: 1;
+
+          .product-title {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+
+            .product-icon {
+              width: 40px;
+              height: 40px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              background: var(--ion-color-primary-tint);
+              border-radius: 10px;
+              color: var(--ion-color-primary);
+              font-size: 20px;
+            }
+
+            .product-details {
+              .product-name {
+                font-size: 18px;
+                font-weight: 600;
+                color: var(--ion-color-dark);
+                margin: 0 0 4px;
+              }
+
+              .product-meta {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+
+                .badge {
+                  font-size: 12px;
+                  padding: 2px 8px;
+                  border-radius: 12px;
+                  font-weight: 500;
+                }
+
+                .designer-name {
+                  font-size: 13px;
+                  color: var(--ion-color-medium);
+                }
+              }
+            }
+          }
+
+          .product-pricing {
+            text-align: right;
+
+            .product-subtotal {
+              font-size: 20px;
+              font-weight: 700;
+              color: var(--ion-color-primary);
+              margin: 0 0 2px;
+            }
+
+            .percentage {
+              font-size: 13px;
+              color: var(--ion-color-medium);
+            }
+          }
+        }
+
+        .product-actions {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+
+          .btn-icon {
+            width: 32px;
+            height: 32px;
+            border-radius: 8px;
+            border: none;
+            background: var(--ion-color-light-tint);
+            color: var(--ion-color-medium);
+            cursor: pointer;
+            transition: all 0.2s;
+
+            &:hover {
+              background: var(--ion-color-light-shade);
+              color: var(--ion-color-dark);
+            }
+
+            &.danger:hover {
+              background: var(--ion-color-danger-tint);
+              color: var(--ion-color-danger);
+            }
+
+            .icon {
+              width: 16px;
+              height: 16px;
+            }
+          }
+
+          .product-toggle {
+            margin-left: 8px;
+            color: var(--ion-color-medium);
+            transition: transform 0.3s ease;
+
+            .icon {
+              width: 20px;
+              height: 20px;
+            }
+          }
+        }
+      }
+
+      .product-card.expanded .product-toggle {
+        transform: rotate(180deg);
+      }
+
+      .product-content {
+        border-top: 1px solid var(--ion-color-light-shade);
+        animation: slideDown 0.3s ease-out;
+      }
+
+      .product-details-section {
+        padding: 16px 20px;
+        background: var(--ion-color-light-tint);
+        border-bottom: 1px solid var(--ion-color-light-shade);
+
+        .detail-item {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: 8px;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+
+          .detail-label {
+            font-size: 14px;
+            color: var(--ion-color-medium);
+          }
+
+          .detail-value {
+            font-size: 14px;
+            font-weight: 500;
+            color: var(--ion-color-dark);
+          }
+        }
+      }
+    }
+  }
+
+  // 报价汇总
+  .quotation-summary {
+    background: white;
+    border: 1px solid var(--ion-color-light-shade);
+    border-radius: 12px;
+    padding: 20px;
+    margin-top: 20px;
+
+    .summary-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 16px;
+
+      h4 {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--ion-color-dark);
+      }
+
+      .breakdown-toggle {
+        .btn-text {
+          background: none;
+          border: none;
+          color: var(--ion-color-primary);
+          font-size: 14px;
+          cursor: pointer;
+          padding: 4px 8px;
+          border-radius: 4px;
+          transition: background-color 0.2s;
+
+          &:hover {
+            background-color: var(--ion-color-primary-tint);
+          }
+        }
+      }
+    }
+
+    .breakdown-list {
+      margin-bottom: 16px;
+
+      .breakdown-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px 0;
+        border-bottom: 1px solid var(--ion-color-light-tint);
+
+        &:last-child {
+          border-bottom: none;
+        }
+
+        .breakdown-name {
+          flex: 1;
+          font-size: 14px;
+          color: var(--ion-color-dark);
+        }
+
+        .breakdown-amount {
+          font-size: 15px;
+          font-weight: 600;
+          color: var(--ion-color-dark);
+          margin: 0 16px;
+        }
+
+        .breakdown-percentage {
+          font-size: 13px;
+          color: var(--ion-color-medium);
+          min-width: 50px;
+          text-align: right;
+        }
+      }
+    }
+
+    .total-section {
+      .total-row {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 16px 0;
+        border-top: 2px solid var(--ion-color-primary);
+        background: linear-gradient(135deg, var(--ion-color-primary) 0%, var(--ion-color-primary-shade) 100%);
+        margin: 0 -20px -20px;
+        padding: 16px 20px;
+
+        .total-label {
+          font-size: 16px;
+          font-weight: 500;
+          color: white;
+        }
+
+        .total-amount {
+          font-size: 24px;
+          font-weight: 700;
+          color: white;
+        }
+      }
+
+      .total-meta {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-top: 8px;
+        font-size: 12px;
+        color: var(--ion-color-medium);
+
+        .generate-info,
+        .valid-info {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+        }
+      }
+    }
+  }
+
+  // 兼容性 - 保持原有space类名
+  .quotation-spaces {
+    @extend .quotation-products;
+  }
+
+  .space-card {
+    @extend .product-card;
+  }
+
+  .space-header {
+    @extend .product-header;
+  }
+
+  .space-content {
+    @extend .product-content;
+  }
 }

+ 724 - 42
src/modules/project/components/quotation-editor.component.ts

@@ -1,15 +1,21 @@
-import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { FmodeParse } from 'fmode-ng/parse';
+import { Subscription } from 'rxjs';
+
+const Parse = FmodeParse.with('nova');
 
 /**
- * 报价编辑器组件
+ * 基于Product表的报价编辑器组件
  *
  * 功能:
- * 1. 展示报价明细,支持折叠/展开
- * 2. 编辑工序价格和数量
- * 3. 自动计算小计和总价
- * 4. 支持表格和卡片两种展示模式
+ * 1. 通过project.id自动加载和管理所有Product的报价
+ * 2. 支持多产品设计产品的报价管理
+ * 3. 智能报价生成和编辑
+ * 4. 支持家装/工装项目类型
+ * 5. 自动计算小计和总价
+ * 6. 支持表格和卡片两种展示模式
  */
 @Component({
   selector: 'app-quotation-editor',
@@ -18,13 +24,40 @@ import { FormsModule } from '@angular/forms';
   templateUrl: './quotation-editor.component.html',
   styleUrls: ['./quotation-editor.component.scss']
 })
-export class QuotationEditorComponent implements OnChanges {
-  @Input() quotation: any = { spaces: [], total: 0 };
+export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
+  // 输入属性
+  @Input() projectId: string = '';
   @Input() canEdit: boolean = false;
-  @Input() viewMode: 'table' | 'card' = 'card'; // 展示模式
+  @Input() viewMode: 'table' | 'card' = 'card';
+  @Input() currentUser: any = null;
 
+  // 输出事件
   @Output() quotationChange = new EventEmitter<any>();
   @Output() totalChange = new EventEmitter<number>();
+  @Output() loadingChange = new EventEmitter<boolean>();
+  @Output() productsChange = new EventEmitter<any[]>();
+
+  // 数据状态
+  loading: boolean = false;
+  project: any = null;
+  products: any[] = [];
+  projectInfo: any = {
+    title: '',
+    projectType: '', // 家装 | 工装
+    renderType: '', // 静态单张 | 360全景
+    deadline: '',
+    description: '',
+    priceLevel: '一级', // 一级(老客户) | 二级(中端组) | 三级(高端组)
+  };
+
+  // 报价数据结构
+  quotation: any = {
+    spaces: [], // 兼容旧格式,现在基于products
+    total: 0,
+    spaceBreakdown: [], // 产品占比明细
+    generatedAt: null,
+    validUntil: null
+  };
 
   // 工序类型定义
   processTypes = [
@@ -35,49 +68,400 @@ export class QuotationEditorComponent implements OnChanges {
   ];
 
   // 折叠状态
-  expandedSpaces: Set<string> = new Set();
+  expandedProducts: Set<string> = new Set();
+
+  // UI状态
+  showBreakdown: boolean = false;
+
+  // 报价配置
+  priceTable: any = {};
+  styleLevels: any = {};
+  spaceTypes: any = {};
+  businessTypes: any = {};
+  homeDefaultRooms: string[] = [];
+
+  // 订阅管理
+  private subscriptions: Subscription[] = [];
+
+  ngOnInit() {
+    this.loadQuotationConfig();
+    if (this.projectId) {
+      this.loadProjectData();
+    }
+  }
 
   ngOnChanges(changes: SimpleChanges) {
+    if (changes['projectId'] && changes['projectId'].currentValue) {
+      this.loadProjectData();
+    }
+
     if (changes['quotation'] && this.quotation?.spaces?.length > 0) {
-      // 默认展开第一个空间
-      if (this.expandedSpaces.size === 0) {
-        this.expandedSpaces.add(this.quotation.spaces[0].name);
+      // 默认展开第一个产品
+      if (this.expandedProducts.size === 0) {
+        this.expandedProducts.add(this.quotation.spaces[0].name);
       }
     }
   }
 
+  ngOnDestroy() {
+    this.subscriptions.forEach(sub => sub.unsubscribe());
+  }
+
   /**
-   * 切换空间展开/折叠状态
+   * 加载报价配置
    */
-  toggleSpaceExpand(spaceName: string) {
-    if (this.expandedSpaces.has(spaceName)) {
-      this.expandedSpaces.delete(spaceName);
-    } else {
-      this.expandedSpaces.add(spaceName);
+  private loadQuotationConfig(): void {
+    // 设置默认值 - 暂时硬编码,后续可改为动态导入
+    this.homeDefaultRooms = ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'];
+    this.priceTable = {};
+    this.styleLevels = {};
+    this.spaceTypes = {};
+    this.businessTypes = {};
+  }
+
+  /**
+   * 加载项目数据
+   */
+  private async loadProjectData(): Promise<void> {
+    if (!this.projectId) return;
+
+    try {
+      this.loading = true;
+      this.loadingChange.emit(true);
+
+      // 加载项目信息
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.include('customer', 'assignee', 'department');
+      this.project = await projectQuery.get(this.projectId);
+
+      if (this.project) {
+        // 加载项目信息
+        this.projectInfo.title = this.project.get('title') || '';
+        this.projectInfo.projectType = this.project.get('projectType') || '';
+        this.projectInfo.renderType = this.project.get('renderType') || '';
+        this.projectInfo.deadline = this.project.get('deadline') || '';
+        this.projectInfo.description = this.project.get('description') || '';
+
+        const data = this.project.get('data') || {};
+        if (data.priceLevel) {
+          this.projectInfo.priceLevel = data.priceLevel;
+        }
+
+        // 加载项目的产品列表
+        await this.loadProjectProducts();
+
+        // 加载现有报价数据
+        if (data.quotation) {
+          this.quotation = data.quotation;
+          this.updateProductsFromQuotation();
+        }
+      }
+
+    } catch (error) {
+      console.error('加载项目数据失败:', error);
+    } finally {
+      this.loading = false;
+      this.loadingChange.emit(false);
+    }
+  }
+
+  /**
+   * 加载项目产品列表
+   */
+  private async loadProjectProducts(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      const productQuery = new Parse.Query('Product');
+      productQuery.equalTo('project', this.project.toPointer());
+      productQuery.include('profile');
+      productQuery.ascending('productName');
+
+      this.products = await productQuery.find();
+      this.productsChange.emit(this.products);
+
+      // 如果没有产品但有项目数据,创建默认产品
+      if (this.products.length === 0) {
+        await this.createDefaultProducts();
+      }
+
+    } catch (error) {
+      console.error('加载产品列表失败:', error);
+    }
+  }
+
+  /**
+   * 创建默认产品
+   */
+  private async createDefaultProducts(): Promise<void> {
+    if (!this.project || !this.projectInfo.projectType) return;
+
+    try {
+      const defaultRooms = this.getDefaultRoomsForProjectType();
+
+      for (const roomName of defaultRooms) {
+        const product = new Parse.Object('Product');
+        product.set('project', this.project.toPointer());
+        product.set('productName', roomName);
+        product.set('productType', this.inferProductType(roomName));
+
+        // 设置空间信息
+        product.set('space', {
+          spaceName: roomName,
+          area: 0,
+          dimensions: { length: 0, width: 0, height: 0 },
+          features: [],
+          constraints: [],
+          priority: 5,
+          complexity: 'medium'
+        });
+
+        // 设置报价信息
+        const basePrice = this.calculateBasePrice(roomName);
+        product.set('quotation', {
+          price: basePrice,
+          currency: 'CNY',
+          breakdown: this.calculatePriceBreakdown(basePrice),
+          status: 'draft',
+          validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30天后过期
+        });
+
+        // 设置需求信息
+        product.set('requirements', {
+          colorRequirement: {},
+          materialRequirement: {},
+          lightingRequirement: {},
+          specificRequirements: [],
+          constraints: {}
+        });
+
+        product.set('status', 'not_started');
+        await product.save();
+      }
+
+      // 重新加载产品列表
+      await this.loadProjectProducts();
+
+    } catch (error) {
+      console.error('创建默认产品失败:', error);
     }
   }
 
   /**
-   * 检查空间是否展开
+   * 根据项目类型获取默认房间
    */
-  isSpaceExpanded(spaceName: string): boolean {
-    return this.expandedSpaces.has(spaceName);
+  private getDefaultRoomsForProjectType(): string[] {
+    if (this.projectInfo.projectType === '家装') {
+      return this.homeDefaultRooms.length > 0 ? this.homeDefaultRooms : ['客厅', '主卧', '次卧', '厨房', '卫生间'];
+    } else if (this.projectInfo.projectType === '工装') {
+      return ['门厅', '主要空间', '辅助空间'];
+    }
+    return ['客厅'];
   }
 
   /**
-   * 展开所有空间
+   * 推断产品类型
+   */
+  private inferProductType(roomName: string): string {
+    const name = roomName.toLowerCase();
+    if (name.includes('客厅') || name.includes('起居')) return 'living_room';
+    if (name.includes('卧室') || name.includes('主卧') || name.includes('次卧')) return 'bedroom';
+    if (name.includes('厨房')) return 'kitchen';
+    if (name.includes('卫生间') || name.includes('浴室')) return 'bathroom';
+    if (name.includes('餐厅')) return 'dining_room';
+    if (name.includes('书房') || name.includes('工作室')) return 'study';
+    if (name.includes('阳台')) return 'balcony';
+    if (name.includes('玄关') || name.includes('走廊')) return 'corridor';
+    return 'other';
+  }
+
+  /**
+   * 计算基础价格
+   */
+  private calculateBasePrice(roomName: string): number {
+    // 这里应该根据报价规则计算,暂时返回默认值
+    const basePrices: Record<string, number> = {
+      '客厅': 35000,
+      '主卧': 28000,
+      '次卧': 22000,
+      '厨房': 25000,
+      '卫生间': 15000,
+      '餐厅': 20000,
+      '书房': 18000,
+      '阳台': 8000
+    };
+
+    for (const [key, price] of Object.entries(basePrices)) {
+      if (roomName.includes(key)) return price;
+    }
+
+    return 20000; // 默认价格
+  }
+
+  /**
+   * 计算价格明细
+   */
+  private calculatePriceBreakdown(basePrice: number): any {
+    return {
+      design: basePrice * 0.3,
+      modeling: basePrice * 0.25,
+      rendering: basePrice * 0.25,
+      softDecor: basePrice * 0.15,
+      postProcess: basePrice * 0.05
+    };
+  }
+
+  /**
+   * 从报价数据更新产品
+   */
+  private updateProductsFromQuotation(): void {
+    if (!this.quotation.spaces || !this.products.length) return;
+
+    // 将报价数据映射到产品
+    this.quotation.spaces.forEach((space: any) => {
+      const product = this.products.find(p =>
+        p.get('productName') === space.name ||
+        p.get('productName').includes(space.name)
+      );
+
+      if (product) {
+        // 更新产品的报价信息
+        const quotation = product.get('quotation') || {};
+        quotation.price = space.subtotal || 0;
+        quotation.processes = space.processes;
+        product.set('quotation', quotation);
+      }
+    });
+  }
+
+  // ============ 报价管理核心方法 ============
+
+  /**
+   * 生成基于产品的报价
+   */
+  async generateQuotationFromProducts(): Promise<void> {
+    if (!this.products.length) return;
+
+    this.quotation.spaces = [];
+    this.quotation.generatedAt = new Date();
+    this.quotation.validUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
+
+    for (const product of this.products) {
+      const productName = product.get('productName');
+      const quotation = product.get('quotation') || {};
+      const basePrice = quotation.price || this.calculateBasePrice(productName);
+
+      // 生成工序明细
+      const processes = this.generateDefaultProcesses(basePrice);
+
+      const spaceData = {
+        name: productName,
+        productId: product.id,
+        processes: processes,
+        subtotal: this.calculateProductSubtotal(processes)
+      };
+
+      this.quotation.spaces.push(spaceData);
+    }
+
+    this.calculateTotal();
+    this.updateProductBreakdown();
+    await this.saveQuotationToProject();
+  }
+
+  /**
+   * 生成默认工序
+   */
+  private generateDefaultProcesses(basePrice: number): any {
+    return {
+      modeling: {
+        enabled: true,
+        price: basePrice * 0.25,
+        unit: '项',
+        quantity: 1
+      },
+      softDecor: {
+        enabled: true,
+        price: basePrice * 0.15,
+        unit: '项',
+        quantity: 1
+      },
+      rendering: {
+        enabled: true,
+        price: basePrice * 0.25,
+        unit: '张',
+        quantity: 1
+      },
+      postProcess: {
+        enabled: true,
+        price: basePrice * 0.05,
+        unit: '项',
+        quantity: 1
+      }
+    };
+  }
+
+  /**
+   * 计算产品小计
+   */
+  private calculateProductSubtotal(processes: any): number {
+    let subtotal = 0;
+    for (const process of Object.values(processes)) {
+      const proc = process as any;
+      if (proc.enabled) {
+        subtotal += proc.price * proc.quantity;
+      }
+    }
+    return subtotal;
+  }
+
+  /**
+   * 更新产品占比明细
+   */
+  private updateProductBreakdown(): void {
+    this.quotation.spaceBreakdown = this.quotation.spaces.map((space: any) => ({
+      spaceName: space.name,
+      spaceId: space.productId || '',
+      amount: space.subtotal,
+      percentage: this.quotation.total > 0 ? Math.round((space.subtotal / this.quotation.total) * 100) : 0
+    }));
+  }
+
+  /**
+   * 保存报价到项目
+   */
+  private async saveQuotationToProject(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      const data = this.project.get('data') || {};
+      data.quotation = this.quotation;
+      this.project.set('data', data);
+      await this.project.save();
+
+      this.quotationChange.emit(this.quotation);
+    } catch (error) {
+      console.error('保存报价失败:', error);
+    }
+  }
+
+  // ============ UI交互方法 ============
+
+  
+  /**
+   * 展开所有产品
    */
   expandAll() {
     this.quotation.spaces.forEach((space: any) => {
-      this.expandedSpaces.add(space.name);
+      this.expandedProducts.add(space.name);
     });
   }
 
   /**
-   * 折叠所有空间
+   * 折叠所有产品
    */
   collapseAll() {
-    this.expandedSpaces.clear();
+    this.expandedProducts.clear();
   }
 
   /**
@@ -119,19 +503,14 @@ export class QuotationEditorComponent implements OnChanges {
   }
 
   /**
-   * 计算空间小计
+   * 计算空间小计(保持兼容性)
    */
   calculateSpaceSubtotal(space: any): number {
-    let subtotal = 0;
-    for (const processKey of Object.keys(space.processes)) {
-      const process = space.processes[processKey];
-      if (process.enabled) {
-        subtotal += process.price * process.quantity;
-      }
-    }
-    return subtotal;
+    return this.calculateProductSubtotal(space.processes);
   }
 
+  // ============ 辅助方法 ============
+
   /**
    * 辅助方法:检查工序是否启用
    */
@@ -176,19 +555,322 @@ export class QuotationEditorComponent implements OnChanges {
     return process?.quantity || 0;
   }
 
+  
   /**
-   * 辅助方法:获取工序单位
+   * 辅助方法:计算工序小计
    */
-  getProcessUnit(space: any, processKey: string): string {
+  calculateProcessSubtotal(space: any, processKey: string): number {
     const process = space.processes?.[processKey];
-    return process?.unit || '';
+    return (process?.price || 0) * (process?.quantity || 0);
   }
 
+  // ============ 产品管理方法 ============
+
   /**
-   * 辅助方法:计算工序小计
+   * 添加新产品
    */
-  calculateProcessSubtotal(space: any, processKey: string): number {
-    const process = space.processes?.[processKey];
-    return (process?.price || 0) * (process?.quantity || 0);
+  async addProduct(productName?: string): Promise<void> {
+    if (!this.project) return;
+
+    const name = productName || prompt('请输入产品名称:');
+    if (!name) return;
+
+    try {
+      const product = new Parse.Object('Product');
+      product.set('project', this.project.toPointer());
+      product.set('productName', name);
+      product.set('productType', this.inferProductType(name));
+
+      // 设置空间信息
+      product.set('space', {
+        spaceName: name,
+        area: 0,
+        dimensions: { length: 0, width: 0, height: 0 },
+        features: [],
+        constraints: [],
+        priority: 5,
+        complexity: 'medium'
+      });
+
+      // 设置报价信息
+      const basePrice = this.calculateBasePrice(name);
+      product.set('quotation', {
+        price: basePrice,
+        currency: 'CNY',
+        breakdown: this.calculatePriceBreakdown(basePrice),
+        status: 'draft',
+        validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+      });
+
+      product.set('status', 'not_started');
+      await product.save();
+
+      // 重新加载产品列表
+      await this.loadProjectProducts();
+      await this.generateQuotationFromProducts();
+
+    } catch (error) {
+      console.error('添加产品失败:', error);
+      alert('添加失败,请重试');
+    }
+  }
+
+  /**
+   * 编辑产品名称
+   */
+  async editProduct(productId: string): Promise<void> {
+    const product = this.products.find(p => p.id === productId);
+    if (!product) return;
+
+    const newName = prompt('修改产品名称:', product.get('productName'));
+    if (!newName || newName === product.get('productName')) return;
+
+    try {
+      product.set('productName', newName);
+      product.set('productType', this.inferProductType(newName));
+      await product.save();
+
+      // 更新报价中的名称
+      const spaceData = this.quotation.spaces.find((s: any) => s.productId === productId);
+      if (spaceData) {
+        spaceData.name = newName;
+      }
+
+      await this.saveQuotationToProject();
+      await this.loadProjectProducts();
+
+    } catch (error) {
+      console.error('更新产品失败:', error);
+      alert('更新失败,请重试');
+    }
+  }
+
+  /**
+   * 删除产品
+   */
+  async deleteProduct(productId: string): Promise<void> {
+    if (!confirm('确定要删除这个产品吗?相关数据将被清除。')) return;
+
+    try {
+      const product = this.products.find(p => p.id === productId);
+      if (product) {
+        await product.destroy();
+      }
+
+      // 从本地列表中移除
+      this.products = this.products.filter(p => p.id !== productId);
+
+      // 从报价中移除
+      this.quotation.spaces = this.quotation.spaces.filter((s: any) => s.productId !== productId);
+
+      // 重新计算
+      this.calculateTotal();
+      this.updateProductBreakdown();
+      await this.saveQuotationToProject();
+
+    } catch (error) {
+      console.error('删除产品失败:', error);
+      alert('删除失败,请重试');
+    }
+  }
+
+  /**
+   * 获取产品设计师
+   */
+  getProductDesigner(product: any): string {
+    const profile = product.get('profile');
+    return profile ? profile.get('name') : '未分配';
+  }
+
+  /**
+   * 获取产品状态
+   */
+  getProductStatus(product: any): string {
+    return product.get('status') || 'not_started';
+  }
+
+  /**
+   * 获取产品状态颜色
+   */
+  getProductStatusColor(status: string): string {
+    const colorMap: Record<string, string> = {
+      'not_started': 'medium',
+      'in_progress': 'warning',
+      'awaiting_review': 'info',
+      'completed': 'success',
+      'blocked': 'danger',
+      'delayed': 'danger'
+    };
+    return colorMap[status] || 'medium';
+  }
+
+  /**
+   * 获取产品状态文本
+   */
+  getProductStatusText(status: string): string {
+    const textMap: Record<string, string> = {
+      'not_started': '未开始',
+      'in_progress': '进行中',
+      'awaiting_review': '待审核',
+      'completed': '已完成',
+      'blocked': '已阻塞',
+      'delayed': '已延期'
+    };
+    return textMap[status] || status;
+  }
+
+  /**
+   * 保存报价
+   */
+  async saveQuotation(): Promise<void> {
+    if (!this.canEdit) return;
+
+    try {
+      await this.saveQuotationToProject();
+      alert('保存成功');
+    } catch (error) {
+      console.error('保存失败:', error);
+      alert('保存失败');
+    }
+  }
+
+  /**
+   * 获取产品图标
+   */
+  getProductIcon(productType: string): string {
+    const iconMap: Record<string, string> = {
+      'living_room': 'living-room',
+      'bedroom': 'bedroom',
+      'kitchen': 'kitchen',
+      'bathroom': 'bathroom',
+      'dining_room': 'dining-room',
+      'study': 'study',
+      'balcony': 'balcony',
+      'corridor': 'corridor',
+      'storage': 'storage',
+      'entrance': 'entrance',
+      'other': 'room'
+    };
+    return iconMap[productType] || 'room';
+  }
+
+  /**
+   * 格式化价格显示
+   */
+  formatPrice(price: number): string {
+    return `¥${price.toFixed(2)}`;
+  }
+
+  /**
+   * 格式化百分比
+   */
+  formatPercentage(value: number): string {
+    return `${value}%`;
+  }
+
+  // ============ 辅助方法用于简化模板 ============
+
+  /**
+   * 根据空间名称获取产品类型
+   */
+  getProductTypeForSpace(spaceName: string): string {
+    const name = spaceName.toLowerCase();
+    if (name.includes('客厅') || name.includes('起居')) return 'living_room';
+    if (name.includes('卧室') || name.includes('主卧') || name.includes('次卧')) return 'bedroom';
+    if (name.includes('厨房')) return 'kitchen';
+    if (name.includes('卫生间') || name.includes('浴室')) return 'bathroom';
+    if (name.includes('餐厅')) return 'dining_room';
+    if (name.includes('书房') || name.includes('工作室')) return 'study';
+    if (name.includes('阳台')) return 'balcony';
+    if (name.includes('玄关') || name.includes('走廊')) return 'corridor';
+    return 'other';
+  }
+
+  /**
+   * 根据空间名称获取产品图标
+   */
+  getProductIconForSpace(spaceName: string): string {
+    const productType = this.getProductTypeForSpace(spaceName);
+    return this.getProductIcon(productType);
+  }
+
+  /**
+   * 根据空间ID获取状态颜色
+   */
+  getStatusColorForSpace(spaceId: string): string {
+    const product = this.products.find(p => p.id === spaceId);
+    if (product) {
+      return this.getProductStatusColor(product.get('status'));
+    }
+    return 'medium';
+  }
+
+  /**
+   * 根据空间ID获取状态文本
+   */
+  getStatusTextForSpace(spaceId: string): string {
+    const product = this.products.find(p => p.id === spaceId);
+    if (product) {
+      return this.getProductStatusText(product.get('status'));
+    }
+    return '未开始';
+  }
+
+  /**
+   * 根据空间ID获取设计师名称
+   */
+  getDesignerNameForSpace(spaceId: string): string {
+    const product = this.products.find(p => p.id === spaceId);
+    if (product) {
+      return this.getProductDesigner(product);
+    }
+    return '未分配';
+  }
+
+  /**
+   * 根据空间ID获取产品对象
+   */
+  getProductForSpace(productId: string): any {
+    return this.products.find(p => p.id === productId) || null;
+  }
+
+  /**
+   * 获取工序单位
+   */
+  getProcessUnit(_space: any, processKey: string): string {
+    const units: { [key: string]: string } = {
+      'modeling': '项',
+      'softDecor': '项',
+      'rendering': '张',
+      'postProcess': '项'
+    };
+    return units[processKey] || '项';
+  }
+
+  /**
+   * 切换产品展开状态(重载以保持兼容性)
+   */
+  toggleProductExpand(spaceName: string): void {
+    if (this.expandedProducts.has(spaceName)) {
+      this.expandedProducts.delete(spaceName);
+    } else {
+      this.expandedProducts.add(spaceName);
+    }
+  }
+
+  /**
+   * 检查产品是否展开(重载以保持兼容性)
+   */
+  isProductExpanded(spaceName: string): boolean {
+    return this.expandedProducts.has(spaceName);
+  }
+
+  /**
+   * 获取空间占比
+   */
+  getSpacePercentage(spaceId: string): number {
+    if (!this.quotation.spaceBreakdown) return 0;
+    const breakdown = this.quotation.spaceBreakdown.find((b: any) => b.spaceId === spaceId);
+    return breakdown?.percentage || 0;
   }
 }

+ 24 - 23
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -381,30 +381,31 @@
       </div>
     }
 
-    <!-- 4. 报价明细(使用独立组件) -->
-    @if (quotation.spaces.length > 0) {
-      <div class="card quotation-card">
-        <div class="card-header">
-          <h3 class="card-title">
-            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-              <rect x="112" y="128" width="288" height="320" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
-              <path fill="currentColor" d="M320 304h-32v-24a8 8 0 00-8-8h-48a8 8 0 00-8 8v24h-32a8 8 0 00-8 8v16a8 8 0 008 8h32v24a8 8 0 008 8h48a8 8 0 008-8v-24h32a8 8 0 008-8v-16a8 8 0 00-8-8z"/>
-            </svg>
-            报价明细
-          </h3>
-          <p class="card-subtitle">支持折叠展开,可手动调整工序和价格</p>
-        </div>
-        <div class="card-content">
-          <app-quotation-editor
-            [quotation]="quotation"
-            [canEdit]="canEdit"
-            [viewMode]="'card'"
-            (quotationChange)="onQuotationChange($event)"
-            (totalChange)="onTotalChange($event)">
-          </app-quotation-editor>
-        </div>
+    <!-- 4. 基于Product表的报价管理 -->
+    <div class="card quotation-card">
+      <div class="card-header">
+        <h3 class="card-title">
+          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+            <rect x="112" y="128" width="288" height="320" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
+            <path fill="currentColor" d="M320 304h-32v-24a8 8 0 00-8-8h-48a8 8 0 00-8 8v24h-32a8 8 0 00-8 8v16a8 8 0 008 8h32v24a8 8 0 008 8h48a8 8 0 008-8v-24h32a8 8 0 008-8v-16a8 8 0 00-8-8z"/>
+          </svg>
+          产品报价管理
+        </h3>
+        <p class="card-subtitle">基于Product表的智能报价生成和管理系统</p>
       </div>
-    }
+      <div class="card-content">
+        <app-quotation-editor
+          [projectId]="projectId"
+          [canEdit]="canEdit"
+          [viewMode]="'card'"
+          [currentUser]="currentUser"
+          (quotationChange)="onQuotationChange($event)"
+          (totalChange)="onTotalChange($event)"
+          (loadingChange)="onQuotationLoadingChange($event)"
+          (productsChange)="onProductsChange($event)">
+        </app-quotation-editor>
+      </div>
+    </div>
 
     <!-- 5. 设计师分配 -->
     <div class="card designer-card">

+ 33 - 139
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -663,147 +663,11 @@ export class StageOrderComponent implements OnInit {
     }));
   }
 
-  /**
-   * 家装场景:空间类型和风格等级改变,生成预设房间列表
-   */
-  onHomeSceneChange() {
-    if (!this.homeScenes.spaceType || !this.homeScenes.styleLevel) {
-      this.homeScenes.rooms = [];
-      return;
-    }
-
-    const renderType = this.projectInfo.renderType || '静态单张';
-
-    // 使用配置文件中的工具函数获取价格
-    this.homeScenes.rooms = this.homeDefaultRooms.map(name => {
-      const spaceType = name.includes('卧') ? '卧室' : this.homeScenes.spaceType;
-      const basePrice = getBasePrice(
-        this.projectInfo.priceLevel,
-        '家装',
-        renderType,
-        spaceType,
-        this.homeScenes.styleLevel
-      );
-
-      return {
-        name,
-        spaceType,
-        selected: false,
-        basePrice,
-        adjustments: {
-          extraFunction: 0,
-          complexity: 0,
-          design: false
-        }
-      };
-    });
-  }
+  // 注意:报价生成和管理功能已移至 quotation-editor 组件
+// 以下方法保留用于兼容性和数据处理
 
   /**
-   * 工装场景:业态类型改变,生成预设空间列表
-   */
-  onCommercialTypeChange() {
-    if (!this.commercialScenes.businessType) {
-      this.commercialScenes.spaces = [];
-      return;
-    }
-
-    const renderType = '静态单张';
-
-    this.commercialScenes.spaces = [
-      {
-        name: '门厅',
-        type: '门厅空间',
-        selected: false,
-        basePrice: getBasePrice(
-          this.projectInfo.priceLevel,
-          '工装',
-          renderType,
-          '门厅空间',
-          undefined,
-          this.commercialScenes.businessType
-        ),
-        adjustments: {
-          extraFunction: 0,
-          complexity: 0,
-          design: false,
-          panoramic: false
-        }
-      },
-      {
-        name: '封闭空间1',
-        type: '封闭空间',
-        selected: false,
-        basePrice: getBasePrice(
-          this.projectInfo.priceLevel,
-          '工装',
-          renderType,
-          '封闭空间',
-          undefined,
-          this.commercialScenes.businessType
-        ),
-        adjustments: {
-          extraFunction: 0,
-          complexity: 0,
-          design: false,
-          panoramic: false
-        }
-      }
-    ];
-  }
-
-  /**
-   * 生成最终报价明细
-   */
-  generateQuotation() {
-    this.quotation.spaces = [];
-
-    if (this.projectInfo.projectType === '家装') {
-      // 遍历选中的房间
-      for (const room of this.homeScenes.rooms.filter(r => r.selected)) {
-        // 使用配置文件中的价格计算函数
-        const finalPrice = calculateFinalPrice(
-          room.basePrice,
-          '家装',
-          room.adjustments
-        );
-
-        // 使用配置文件生成默认工序
-        const processes = getDefaultProcesses('家装', finalPrice);
-
-        this.quotation.spaces.push({
-          name: room.name,
-          spaceId: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
-          processes: processes as any,
-          subtotal: finalPrice + processes.postProcess.price
-        });
-      }
-    } else if (this.projectInfo.projectType === '工装') {
-      // 遍历选中的空间
-      for (const space of this.commercialScenes.spaces.filter(s => s.selected)) {
-        const finalPrice = calculateFinalPrice(
-          space.basePrice,
-          '工装',
-          space.adjustments
-        );
-
-        const processes = getDefaultProcesses('工装', finalPrice);
-
-        this.quotation.spaces.push({
-          name: space.name,
-          spaceId: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
-          processes: processes as any,
-          subtotal: finalPrice + processes.postProcess.price
-        });
-      }
-    }
-
-    this.calculateTotal();
-    this.updateSpaceBreakdown();
-  }
-
-  /**
-   * 计算报价总额
+   * 计算报价总额(兼容性方法)
    */
   calculateTotal() {
     let total = 0;
@@ -818,6 +682,21 @@ export class StageOrderComponent implements OnInit {
     this.quotation.total = total;
   }
 
+  /**
+   * 空方法保持兼容性
+   */
+  onHomeSceneChange() {
+    // 空实现,功能已移至quotation-editor组件
+  }
+
+  onCommercialTypeChange() {
+    // 空实现,功能已移至quotation-editor组件
+  }
+
+  generateQuotation() {
+    // 空实现,功能已移至quotation-editor组件
+  }
+
   /**
    * 获取空间进度
    */
@@ -1112,6 +991,21 @@ export class StageOrderComponent implements OnInit {
     this.updateSpaceBreakdown();
   }
 
+  /**
+   * 报价加载状态变化回调
+   */
+  onQuotationLoadingChange(loading: boolean) {
+    // 可以在这里处理加载状态,比如显示loading indicator
+  }
+
+  /**
+   * 产品列表变化回调
+   */
+  onProductsChange(products: any[]) {
+    // 可以在这里处理产品列表变化
+    console.log('产品列表已更新:', products.length, '个产品');
+  }
+
   /**
    * 保存草稿
    */