Browse Source

feat: quotation with new part

ryanemax 11 hours ago
parent
commit
a1e4dbadf0

+ 11 - 11
docs/data/quotation.md

@@ -460,17 +460,17 @@
 
 ### 7.3 分配示例
 
-#### 示例1: 家装项目
-- **报价总价**: ¥60,000
-- **建模阶段**: ¥60,000 × 10% = ¥6,000
-- **软装渲染**: ¥60,000 × 40% = ¥24,000
-- **公司分配**: ¥60,000 × 50% = ¥30,000
-
-#### 示例2: 工装项目
-- **报价总价**: ¥85,000
-- **建模阶段**: ¥85,000 × 10% = ¥8,500
-- **软装渲染**: ¥85,000 × 40% = ¥34,000
-- **公司分配**: ¥85,000 × 50% = ¥42,500
+#### 示例1: 厨房空间
+- **报价总价**: ¥900
+- **建模阶段**: ¥900 × 10% = ¥90
+- **软装渲染**: ¥900 × 40% = ¥360
+- **公司分配**: ¥900 × 50% = ¥450
+
+#### 示例2: 客厅空间
+- **报价总价**: ¥900
+- **建模阶段**: ¥900 × 10% = ¥90
+- **软装渲染**: ¥900 × 40% = ¥360
+- **公司分配**: ¥900 × 50% = ¥450
 
 ### 7.4 重要说明
 

File diff suppressed because it is too large
+ 683 - 337
docs/prd/功能-报价自动分配.md


+ 447 - 323
src/modules/project/components/quotation-editor.component.html

@@ -15,24 +15,23 @@
     @if (canEdit) {
       <div class="product-management">
         <div class="product-header">
-          <h3>产品设计产品 ({{ products.length }}个)</h3>
+          <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"/>
+                <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.1z"/>
               </svg>
               生成报价
             </button>
-            <button class="btn-secondary" (click)="addProduct()">
+            <button class="btn-secondary" (click)="openAddProductModal()">
               <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"/>
+                <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 24v56H200c-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"/>
+                <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 141z"/>
               </svg>
               保存报价
             </button>
@@ -45,10 +44,10 @@
       <!-- 空状态 - 有产品但未生成报价 -->
       <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"/>
+          <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.1z"/>
         </svg>
         <p class="empty-message">尚未生成报价</p>
-        <p class="empty-hint">已加载 {{ products.length }} 个产品设计产品,请点击"生成报价"按钮</p>
+        <p class="empty-hint">已加载 {{ products.length }} 个设计空间,请点击"生成报价"按钮</p>
         @if (canEdit) {
           <button class="btn-primary" (click)="generateQuotationFromProducts()">立即生成报价</button>
         }
@@ -57,379 +56,504 @@
       <!-- 完全空状态 - 无产品 -->
       <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"/>
+          <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 464z"/>
         </svg>
-        <p class="empty-message">暂无产品设计产品</p>
-        <p class="empty-hint">该项目还没有创建任何产品设计产品</p>
+        <p class="empty-message">暂无设计产品</p>
+        <p class="empty-hint">该项目还没有创建任何设计产品</p>
         @if (canEdit) {
-          <button class="btn-primary" (click)="addProduct()">创建第一个产品</button>
+          <button class="btn-primary" (click)="openAddProductModal()">创建第一个产品</button>
         }
       </div>
     } @else {
       <!-- 报价工具栏 -->
-    <div class="quotation-toolbar">
-      <div class="toolbar-left">
-        <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 class="quotation-toolbar">
+        <div class="toolbar-left">
+          <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="展开全部">
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M112 184l144 144 144-144M256 328V88"/>
+            </svg>
+          </button>
+          <button class="btn-icon" (click)="collapseAll()" title="折叠全部">
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M112 328l144-144 144 144M256 184v240"/>
+            </svg>
+          </button>
         </div>
       </div>
-      <div class="toolbar-right">
-        <button class="btn-icon" (click)="expandAll()" title="展开全部">
-          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M112 184l144 144 144-144M256 328V88"/>
-          </svg>
-        </button>
-        <button class="btn-icon" (click)="collapseAll()" title="折叠全部">
-          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M112 328l144-144 144 144M256 184v240"/>
-          </svg>
-        </button>
-      </div>
-    </div>
 
-    <!-- 卡片视图 -->
-    @if (viewMode === 'card') {
-      <div class="quotation-products">
-        @for (space of quotation.spaces; track space.name) {
-          <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>
+      <!-- 卡片视图 -->
+      @if (viewMode === 'card') {
+        <div class="quotation-products">
+          @for (space of quotation.spaces; track space.name) {
+            <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="product-pricing">
-                  <p class="product-subtotal">{{ formatPrice(calculateSpaceSubtotal(space)) }}</p>
-                  @if (quotation.spaceBreakdown?.length > 1) {
-                    <span class="percentage">{{ formatPercentage(getSpacePercentage(space.productId)) }}</span>
+                <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.32z"/>
+                      </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 192z"/>
+                      </svg>
+                    </button>
                   }
-                </div>
-              </div>
-              <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="删除">
+                  <div class="product-toggle">
                     <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"/>
+                      <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>
-                  </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>
-            </div>
 
-                      <!-- 产品详情 -->
-            @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) {
-                    <div
-                      class="process-item"
-                      [class.enabled]="isProcessEnabled(space, processType.key)">
-                      <div class="process-header" (click)="canEdit && toggleProcess(space, processType.key)">
-                        <label class="checkbox-wrapper">
-                          <input
-                            type="checkbox"
-                            class="checkbox-input"
-                            [checked]="isProcessEnabled(space, processType.key)"
-                            [disabled]="!canEdit" />
-                          <span class="checkbox-custom"></span>
-                        </label>
-                        <span class="badge" [attr.data-color]="processType.color">
-                          {{ processType.name }}
-                        </span>
+              <!-- 产品详情 -->
+              @if (isProductExpanded(space.name)) {
+                <div class="product-content">
+                  <!-- 产品信息 -->
+                  @if (getProductForSpace(space.productId)) {
+                    <div class="product-details-section">
+                      <h5 class="section-title">产品信息</h5>
+                      <div class="detail-grid">
+                        <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 price">{{ formatPrice(getProductForSpace(space.productId)?.get('quotation')?.basePrice || 0) }}</span>
+                        </div>
                       </div>
+                    </div>
+                  }
 
-                      @if (isProcessEnabled(space, processType.key)) {
-                        <div class="process-inputs">
-                          <div class="input-group">
-                            <label class="input-label">单价</label>
-                            <div class="input-with-note">
-                              <input
-                                class="input-field"
-                                type="number"
-                                [ngModel]="getProcessPrice(space, processType.key)"
-                                (ngModelChange)="setProcessPrice(space, processType.key, $event); onProcessChange()"
-                                [disabled]="!canEdit"
-                                placeholder="0" />
-                              <span class="input-note">元/{{ getProcessUnit(space, processType.key) }}</span>
+                  <!-- 内部分配明细(3个分配:建模阶段10%、软装渲染40%、公司分配50%) -->
+                  <div class="allocation-section-detail">
+                    <h5 class="section-title">内部执行分配 <span class="section-subtitle">(基于设计图总价自动分配)</span></h5>
+                    <div class="allocation-grid-detail">
+                      @for (allocationType of allocationTypes; track allocationType.key) {
+                        <div
+                          class="allocation-item-detail"
+                          [class.enabled]="isProcessEnabled(space, allocationType.key)"
+                          [attr.data-type]="allocationType.key">
+                          <div class="allocation-header-detail">
+                            <div class="allocation-left">
+                              <label class="checkbox-wrapper">
+                                <input
+                                  type="checkbox"
+                                  class="checkbox-input"
+                                  [checked]="isProcessEnabled(space, allocationType.key)"
+                                  (change)="canEdit && toggleProcess(space, allocationType.key)"
+                                  [disabled]="!canEdit" />
+                                <span class="checkbox-custom"></span>
+                              </label>
+                              <div class="allocation-info-detail">
+                                <span class="allocation-name-detail">{{ allocationType.name }}</span>
+                                <span class="allocation-desc-detail">{{ allocationType.description }}</span>
+                              </div>
                             </div>
+                            <span class="allocation-percentage-badge">{{ allocationType.percentage }}%</span>
                           </div>
 
-                          <div class="input-group">
-                            <label class="input-label">数量</label>
-                            <div class="input-with-note">
-                              <input
-                                class="input-field"
-                                type="number"
-                                [ngModel]="getProcessQuantity(space, processType.key)"
-                                (ngModelChange)="setProcessQuantity(space, processType.key, $event); onProcessChange()"
-                                [disabled]="!canEdit"
-                                placeholder="0" />
-                              <span class="input-note">{{ getProcessUnit(space, processType.key) }}</span>
+                          @if (isProcessEnabled(space, allocationType.key)) {
+                            <div class="allocation-input-section">
+                              <label class="input-label-small">分配金额(可编辑)</label>
+                              <div class="input-with-currency">
+                                <span class="currency-symbol">¥</span>
+                                <input
+                                  class="input-field amount-input"
+                                  type="number"
+                                  [ngModel]="getAllocationAmount(space, allocationType.key)"
+                                  (ngModelChange)="setAllocationAmount(space, allocationType.key, $event); onProcessChange()"
+                                  [disabled]="!canEdit"
+                                  placeholder="0" />
+                              </div>
+                              <div class="allocation-hint">
+                                建议金额: {{ forSpacePrice(space,allocationType) }}
+                              </div>
                             </div>
-                          </div>
-
-                          <div class="process-subtotal">
-                            小计: ¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}
-                          </div>
+                          }
                         </div>
                       }
                     </div>
-                  }
+                  </div>
+
                 </div>
+              }
+            </div>
+          }
+        </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>
-    }
 
-    <!-- 表格视图 -->
-    @if (viewMode === 'table') {
-      <div class="quotation-table">
-        @for (space of quotation.spaces; track space.name) {
-          <div class="table-section">
-            <div class="table-header">
-              <h3 class="table-space-name">{{ space.name }}</h3>
-              <span class="table-space-subtotal">小计: ¥{{ calculateSpaceSubtotal(space).toFixed(2) }}</span>
+        <!-- 内部执行分配 -->
+        @if (quotation.allocation) {
+          <div class="allocation-section">
+            <div class="allocation-header">
+              <h4>内部执行分配</h4>
+              <button class="btn-text" (click)="toggleAllocation()">
+                {{ showAllocation ? '隐藏' : '显示' }}
+              </button>
             </div>
-            <table class="process-table">
-              <thead>
-                <tr>
-                  <th class="col-checkbox"></th>
-                  <th class="col-process">工序</th>
-                  <th class="col-price">单价(元)</th>
-                  <th class="col-quantity">数量</th>
-                  <th class="col-unit">单位</th>
-                  <th class="col-subtotal">小计(元)</th>
-                </tr>
-              </thead>
-              <tbody>
-                @for (processType of processTypes; track processType.key) {
-                  <tr [class.enabled]="isProcessEnabled(space, processType.key)">
-                    <td class="col-checkbox">
-                      <label class="checkbox-wrapper">
-                        <input
-                          type="checkbox"
-                          [checked]="isProcessEnabled(space, processType.key)"
-                          (change)="canEdit && toggleProcess(space, processType.key)"
-                          [disabled]="!canEdit" />
-                        <span class="checkbox-custom"></span>
-                      </label>
-                    </td>
-                    <td class="col-process">
-                      <span class="badge" [attr.data-color]="processType.color">
-                        {{ processType.name }}
-                      </span>
-                    </td>
-                    <td class="col-price">
-                      @if (isProcessEnabled(space, processType.key)) {
-                        <input
-                          class="table-input"
-                          type="number"
-                          [ngModel]="getProcessPrice(space, processType.key)"
-                          (ngModelChange)="setProcessPrice(space, processType.key, $event); onProcessChange()"
-                          [disabled]="!canEdit"
-                          placeholder="0" />
-                      } @else {
-                        <span class="disabled-value">-</span>
-                      }
-                    </td>
-                    <td class="col-quantity">
-                      @if (isProcessEnabled(space, processType.key)) {
-                        <input
-                          class="table-input"
-                          type="number"
-                          [ngModel]="getProcessQuantity(space, processType.key)"
-                          (ngModelChange)="setProcessQuantity(space, processType.key, $event); onProcessChange()"
-                          [disabled]="!canEdit"
-                          placeholder="0" />
-                      } @else {
-                        <span class="disabled-value">-</span>
-                      }
-                    </td>
-                    <td class="col-unit">
-                      {{ getProcessUnit(space, processType.key) || '-' }}
-                    </td>
-                    <td class="col-subtotal">
-                      @if (isProcessEnabled(space, processType.key)) {
-                        <strong>¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}</strong>
-                      } @else {
-                        <span class="disabled-value">-</span>
-                      }
-                    </td>
-                  </tr>
-                }
-              </tbody>
-            </table>
+
+            @if (showAllocation) {
+              <div class="allocation-list">
+                <!-- 建模阶段 -->
+                <div class="allocation-item modeling">
+                  <div class="allocation-icon">
+                    <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M234.5 5.709C248.4 .7377 263.6 .7377 277.5 5.709L469.5 74.28C494.1 83.38 512 107.5 512 134.6C512 161.7 494.1 185.8 469.5 194.9L277.5 263.5C263.6 268.5 248.4 268.5 234.5 263.5L42.47 194.9C17.05 185.8 0 161.7 0 134.6C0 107.5 17.05 83.38 42.47 74.28L234.5 5.709z"/>
+                    </svg>
+                  </div>
+                  <div class="allocation-info">
+                    <span class="allocation-name">{{ allocationRules.modeling.label }}</span>
+                    <span class="allocation-desc">{{ allocationRules.modeling.description }}</span>
+                  </div>
+                  <div class="allocation-values">
+                    <span class="allocation-percentage">{{ quotation.allocation.modeling.percentage }}%</span>
+                    <span class="allocation-amount">{{ formatPrice(quotation.allocation.modeling.amount) }}</span>
+                  </div>
+                </div>
+
+                <!-- 软装渲染 -->
+                <div class="allocation-item decoration">
+                  <div class="allocation-icon">
+                    <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M384 160C366.3 160 352 145.7 352 128C352 110.3 366.3 96 384 96C401.7 96 416 110.3 416 128C416 145.7 401.7 160 384 160z"/>
+                    </svg>
+                  </div>
+                  <div class="allocation-info">
+                    <span class="allocation-name">{{ allocationRules.decoration.label }}</span>
+                    <span class="allocation-desc">{{ allocationRules.decoration.description }}</span>
+                  </div>
+                  <div class="allocation-values">
+                    <span class="allocation-percentage">{{ quotation.allocation.decoration.percentage }}%</span>
+                    <span class="allocation-amount">{{ formatPrice(quotation.allocation.decoration.amount) }}</span>
+                  </div>
+                </div>
+
+                <!-- 公司分配 -->
+                <div class="allocation-item company">
+                  <div class="allocation-icon">
+                    <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M320 32c-8.1 0-16.1 1.4-23.7 4.1L15.8 137.4C6.3 140.9 0 149.9 0 160s6.3 19.1 15.8 22.6l57.9 20.9C57.3 229.3 48 259.8 48 291.9v28.1c0 28.4-10.8 57.7-22.3 80.8c-6.5 13-13.9 25.8-22.5 37.6C0 442.7-.9 448.3 .9 453.4s6 8.9 11.2 10.2l64 16c4.2 1.1 8.7 .3 12.4-2s6.3-6.1 7.1-10.4c8.6-42.8 4.3-81.2-2.1-108.7C90.3 344.3 86 329.8 80 316.5V295.6c0-4.2 .3-8.4 1-12.4L288.1 406.6c8.4 3.2 17.4 4.8 26.6 4.8H448c17.7 0 32-14.3 32-32V256c0-17.7-14.3-32-32-32H448c-17.7 0-32-14.3-32-32s14.3-32 32-32h32c9.7 0 18.5-5.8 22.2-14.8s1.7-19.3-5.2-26.2l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8h32c17.7 0 32 14.3 32 32s-14.3 32-32 32H320z"/>
+                    </svg>
+                  </div>
+                  <div class="allocation-info">
+                    <span class="allocation-name">{{ allocationRules.company.label }}</span>
+                    <span class="allocation-desc">{{ allocationRules.company.description }}</span>
+                  </div>
+                  <div class="allocation-values">
+                    <span class="allocation-percentage">{{ quotation.allocation.company.percentage }}%</span>
+                    <span class="allocation-amount">{{ formatPrice(quotation.allocation.company.amount) }}</span>
+                  </div>
+                </div>
+              </div>
+
+              <div class="allocation-note">
+                <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 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/>
+                </svg>
+                <span>内部执行分配为系统自动计算,基于报价总额按固定比例拆分,所有金额均为整数</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>
 
-      <!-- 报价汇总 -->
-    <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 ? '隐藏' : '显示'}}明细
+<!-- 产品添加模态框 -->
+@if (showAddProductModal) {
+  <div class="modal-overlay" (click)="closeAddProductModal()">
+    <div class="modal-container add-product-modal" (click)="$event.stopPropagation()">
+      <div class="modal-header">
+        <h3>添加设计产品</h3>
+        <button class="close-btn" (click)="closeAddProductModal()">
+          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+            <path fill="currentColor" d="M256 48C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48zm52.7 283.3L256 278.6l-52.7 52.7c-6.2 6.2-16.4 6.2-22.6 0-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3l52.7-52.7-52.7-52.7c-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3 6.2-6.2 16.4-6.2 22.6 0l52.7 52.7 52.7-52.7c6.2-6.2 16.4-6.2 22.6 0 6.2 6.2 6.2 16.4 0 22.6L278.6 256l52.7 52.7c6.2 6.2 6.2 16.4 0 22.6-6.2 6.3-16.4 6.3-22.6 0z"/>
+          </svg>
+        </button>
+      </div>
+
+      <div class="modal-body">
+        <!-- 预设场景选择 -->
+        <div class="form-group">
+          <label class="form-label">选择空间场景</label>
+          <div class="scene-grid">
+            @for (scene of getPresetScenes(); track scene) {
+              <button
+                class="scene-card"
+                [class.selected]="newProduct.sceneName === scene"
+                (click)="selectScene(scene)">
+                <span class="scene-name">{{ scene }}</span>
+              </button>
+            }
+            <button
+              class="scene-card custom"
+              [class.selected]="newProduct.isCustom"
+              (click)="selectCustomScene()">
+              <span class="scene-name">自定义</span>
             </button>
           </div>
+        </div>
+
+        <!-- 自定义名称 -->
+        @if (newProduct.isCustom) {
+          <div class="form-group">
+            <label class="form-label">产品名称</label>
+            <input
+              type="text"
+              [(ngModel)]="newProduct.productName"
+              placeholder="例如:客厅、主卧、大堂等"
+              class="form-input">
+          </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>
+        <!-- 家装专属配置 -->
+        @if (projectInfo.projectType === '家装') {
+          <div class="form-group">
+            <label class="form-label">空间类型</label>
+            <div class="radio-group">
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="平层" [(ngModel)]="newProduct.spaceType">
+                <span>平层</span>
+              </label>
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="跃层" [(ngModel)]="newProduct.spaceType">
+                <span>跃层</span>
+              </label>
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="挑空" [(ngModel)]="newProduct.spaceType">
+                <span>挑空</span>
+              </label>
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="卧室" [(ngModel)]="newProduct.spaceType">
+                <span>卧室</span>
+              </label>
             </div>
-          }
-        </div>
-      }
+          </div>
 
-      <!-- 内部执行分配 -->
-      @if (quotation.allocation) {
-        <div class="allocation-section">
-          <div class="allocation-header">
-            <h4>内部执行分配</h4>
-            <button class="btn-text" (click)="toggleAllocation()">
-              {{ showAllocation ? '隐藏' : '显示' }}
-            </button>
+          <div class="form-group">
+            <label class="form-label">风格等级</label>
+            <select [(ngModel)]="newProduct.styleLevel" class="form-select">
+              <option value="基础风格组">基础风格组</option>
+              <option value="中级风格组">中级风格组</option>
+              <option value="高级风格组">高级风格组</option>
+              <option value="顶级风格组">顶级风格组</option>
+            </select>
           </div>
+        }
 
-          @if (showAllocation) {
-            <div class="allocation-list">
-              <!-- 建模阶段 -->
-              <div class="allocation-item modeling">
-                <div class="allocation-icon">
-                  <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                    <path fill="currentColor" d="M234.5 5.709C248.4 .7377 263.6 .7377 277.5 5.709L469.5 74.28C494.1 83.38 512 107.5 512 134.6C512 161.7 494.1 185.8 469.5 194.9L277.5 263.5C263.6 268.5 248.4 268.5 234.5 263.5L42.47 194.9C17.05 185.8 0 161.7 0 134.6C0 107.5 17.05 83.38 42.47 74.28L234.5 5.709zM256 65.98L82.34 128L256 190L429.7 128L256 65.98zM288 434.6L469.5 346.9C494.1 337.8 512 313.7 512 286.6V214.3C512 194.1 492.9 180.1 473.6 185.8C462.4 189.2 454.8 200.3 454.8 212.8L454.8 286.6C454.8 295.9 449.1 304.3 440.8 308.3L288 375.4V434.6zM170.4 308.3C162.1 304.3 156.3 295.9 156.3 286.6V212.8C156.3 200.3 148.6 189.2 137.4 185.8C118.1 180.1 99.13 194.1 99.13 214.3V286.6C99.13 313.7 116.2 337.8 141.6 346.9L223.9 375.4V434.6C223.9 454.9 242.1 469.8 261.4 464.1C272.6 460.7 280.2 449.6 280.2 437.1V375.4L170.4 308.3z"/>
-                  </svg>
-                </div>
-                <div class="allocation-info">
-                  <span class="allocation-name">{{ allocationRules.modeling.label }}</span>
-                  <span class="allocation-desc">{{ allocationRules.modeling.description }}</span>
-                </div>
-                <div class="allocation-values">
-                  <span class="allocation-percentage">{{ quotation.allocation.modeling.percentage }}%</span>
-                  <span class="allocation-amount">{{ formatPrice(quotation.allocation.modeling.amount) }}</span>
-                </div>
-              </div>
+        <!-- 工装专属配置 -->
+        @if (projectInfo.projectType === '工装') {
+          <div class="form-group">
+            <label class="form-label">业态类型</label>
+            <select [(ngModel)]="newProduct.businessType" class="form-select">
+              <option value="办公空间">办公空间</option>
+              <option value="商业空间">商业空间</option>
+              <option value="娱乐空间">娱乐空间</option>
+              <option value="酒店餐厅">酒店餐厅</option>
+              <option value="公共空间">公共空间</option>
+            </select>
+          </div>
 
-              <!-- 软装渲染 -->
-              <div class="allocation-item decoration">
-                <div class="allocation-icon">
-                  <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                    <path fill="currentColor" d="M384 160C366.3 160 352 145.7 352 128C352 110.3 366.3 96 384 96C401.7 96 416 110.3 416 128C416 145.7 401.7 160 384 160zM384 64C348.7 64 320 92.65 320 128C320 163.3 348.7 192 384 192C419.3 192 448 163.3 448 128C448 92.65 419.3 64 384 64zM456 288H384V216C384 207.2 376.8 200 368 200H144C135.2 200 128 207.2 128 216V288H56C42.75 288 32 298.8 32 312V472C32 485.3 42.75 496 56 496H144H368H456C469.3 496 480 485.3 480 472V312C480 298.8 469.3 288 456 288zM144 464H64V320H144V464zM336 464H176V232H336V464zM448 464H368V320H448V464z"/>
-                  </svg>
-                </div>
-                <div class="allocation-info">
-                  <span class="allocation-name">{{ allocationRules.decoration.label }}</span>
-                  <span class="allocation-desc">{{ allocationRules.decoration.description }}</span>
-                </div>
-                <div class="allocation-values">
-                  <span class="allocation-percentage">{{ quotation.allocation.decoration.percentage }}%</span>
-                  <span class="allocation-amount">{{ formatPrice(quotation.allocation.decoration.amount) }}</span>
-                </div>
-              </div>
+          <div class="form-group">
+            <label class="form-label">空间类型</label>
+            <div class="radio-group">
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="门厅空间" [(ngModel)]="newProduct.spaceType">
+                <span>门厅空间</span>
+              </label>
+              <label class="radio-label">
+                <input type="radio" name="spaceType" value="封闭空间" [(ngModel)]="newProduct.spaceType">
+                <span>封闭空间</span>
+              </label>
+            </div>
+          </div>
+        }
 
-              <!-- 公司分配 -->
-              <div class="allocation-item company">
-                <div class="allocation-icon">
-                  <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                    <path fill="currentColor" d="M320 32c-8.1 0-16.1 1.4-23.7 4.1L15.8 137.4C6.3 140.9 0 149.9 0 160s6.3 19.1 15.8 22.6l57.9 20.9C57.3 229.3 48 259.8 48 291.9v28.1c0 28.4-10.8 57.7-22.3 80.8c-6.5 13-13.9 25.8-22.5 37.6C0 442.7-.9 448.3 .9 453.4s6 8.9 11.2 10.2l64 16c4.2 1.1 8.7 .3 12.4-2s6.3-6.1 7.1-10.4c8.6-42.8 4.3-81.2-2.1-108.7C90.3 344.3 86 329.8 80 316.5V295.6c0-4.2 .3-8.4 1-12.4L288.1 406.6c8.4 3.2 17.4 4.8 26.6 4.8H448c17.7 0 32-14.3 32-32V256c0-17.7-14.3-32-32-32H448c-17.7 0-32-14.3-32-32s14.3-32 32-32h32c9.7 0 18.5-5.8 22.2-14.8s1.7-19.3-5.2-26.2l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8h32c17.7 0 32 14.3 32 32s-14.3 32-32 32H320c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h128v64H320z"/>
-                  </svg>
-                </div>
-                <div class="allocation-info">
-                  <span class="allocation-name">{{ allocationRules.company.label }}</span>
-                  <span class="allocation-desc">{{ allocationRules.company.description }}</span>
-                </div>
-                <div class="allocation-values">
-                  <span class="allocation-percentage">{{ quotation.allocation.company.percentage }}%</span>
-                  <span class="allocation-amount">{{ formatPrice(quotation.allocation.company.amount) }}</span>
-                </div>
+        <!-- 建筑类专属配置 -->
+        @if (projectInfo.projectType === '建筑类') {
+          <div class="form-group">
+            <label class="form-label">建筑类型</label>
+            <select [(ngModel)]="newProduct.architectureType" class="form-select">
+              <option value="门头">门头</option>
+              <option value="小型单体">小型单体</option>
+              <option value="大型单体">大型单体</option>
+              <option value="鸟瞰">鸟瞰</option>
+            </select>
+          </div>
+        }
+
+        <!-- 加价规则配置 -->
+        <div class="pricing-adjustments">
+          <h5 class="section-title">加价规则配置(可选)</h5>
+
+          @if (projectInfo.projectType === '家装' || projectInfo.projectType === '工装') {
+            <div class="form-group">
+              <label class="form-label">额外功能区数量</label>
+              <div class="input-with-hint">
+                <input
+                  type="number"
+                  [(ngModel)]="newProduct.adjustments.extraFunction"
+                  min="0"
+                  class="form-input"
+                  placeholder="0">
+                <span class="input-hint">
+                  @if (projectInfo.projectType === '家装') {
+                    每增加一个功能区 +100元
+                  } @else {
+                    每增加一个功能区 +400元
+                  }
+                </span>
               </div>
             </div>
+          }
 
-            <div class="allocation-note">
-              <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 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208c-17.7 0-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32s-14.3-32-32-32z"/>
-              </svg>
-              <span>内部执行分配为系统自动计算,基于报价总额按固定比例拆分</span>
+          <div class="form-group">
+            <label class="form-label">造型复杂度加价</label>
+            <div class="input-with-hint">
+              <input
+                type="number"
+                [(ngModel)]="newProduct.adjustments.complexity"
+                min="0"
+                max="200"
+                class="form-input"
+                placeholder="0">
+              <span class="input-hint">立面和吊顶造型复杂: 0-200元</span>
+            </div>
+          </div>
+
+          <div class="form-group">
+            <label class="checkbox-label">
+              <input type="checkbox" [(ngModel)]="newProduct.adjustments.design">
+              <span>需要我们设计(原价×2)</span>
+            </label>
+          </div>
+
+          @if (projectInfo.projectType === '工装') {
+            <div class="form-group">
+              <label class="checkbox-label">
+                <input type="checkbox" [(ngModel)]="newProduct.adjustments.panoramic">
+                <span>需要全景渲染(原价×2)</span>
+              </label>
             </div>
           }
         </div>
-      }
 
-      <div class="total-section">
-        <div class="total-row">
-          <span class="total-label">报价总额</span>
-          <span class="total-amount">{{ formatPrice(quotation.total) }}</span>
+        <!-- 价格预览 -->
+        <div class="price-preview">
+          <div class="price-preview-row">
+            <span class="label">基础价格:</span>
+            <span class="price">{{ formatPrice(calculatePreviewBasePrice()) }}</span>
+          </div>
+          @if (calculatePreviewAdjustmentTotal() > 0) {
+            <div class="price-preview-row adjustment">
+              <span class="label">加价合计:</span>
+              <span class="price">+{{ formatPrice(calculatePreviewAdjustmentTotal()) }}</span>
+            </div>
+          }
+          <div class="price-preview-row total">
+            <span class="label">最终价格:</span>
+            <span class="price">{{ formatPrice(calculatePreviewFinalPrice()) }}</span>
+          </div>
         </div>
+      </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 class="modal-footer">
+        <button class="btn-secondary" (click)="closeAddProductModal()">取消</button>
+        <button
+          class="btn-primary"
+          (click)="confirmAddProduct()"
+          [disabled]="!isNewProductValid()">
+          确认添加
+        </button>
       </div>
     </div>
-  }
+  </div>
 }

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

@@ -1122,3 +1122,720 @@
     }
   }
 }
+
+// ============ 模态框样式 ============
+
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(4px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  padding: 20px;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.modal-container {
+  background: white;
+  border-radius: 16px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+  max-width: 600px;
+  width: 100%;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e5e7eb;
+
+  h3 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .close-btn {
+    width: 32px;
+    height: 32px;
+    border-radius: 8px;
+    border: none;
+    background: transparent;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s ease;
+    color: #6b7280;
+
+    &:hover {
+      background: #f3f4f6;
+      color: #111827;
+    }
+
+    &:active {
+      transform: scale(0.95);
+    }
+
+    .icon {
+      width: 20px;
+      height: 20px;
+    }
+  }
+}
+
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: #f3f4f6;
+    border-radius: 4px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #d1d5db;
+    border-radius: 4px;
+
+    &:hover {
+      background: #9ca3af;
+    }
+  }
+}
+
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 24px;
+  border-top: 1px solid #e5e7eb;
+
+  button {
+    padding: 10px 20px;
+    border-radius: 8px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    border: none;
+
+    &.btn-secondary {
+      background: #f3f4f6;
+      color: #374151;
+
+      &:hover {
+        background: #e5e7eb;
+      }
+    }
+
+    &.btn-primary {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      color: white;
+
+      &:hover:not(:disabled) {
+        transform: translateY(-1px);
+        box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+      }
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+    }
+
+    &:active:not(:disabled) {
+      transform: scale(0.98);
+    }
+  }
+}
+
+// ============ 表单样式 ============
+
+.form-group {
+  margin-bottom: 20px;
+
+  .form-label {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 14px;
+    font-weight: 500;
+    color: #374151;
+  }
+
+  .form-input,
+  .form-select {
+    width: 100%;
+    padding: 10px 14px;
+    border: 1.5px solid #e5e7eb;
+    border-radius: 8px;
+    font-size: 14px;
+    color: #111827;
+    transition: all 0.2s ease;
+    outline: none;
+    background: white;
+
+    &:focus {
+      border-color: #667eea;
+      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+    }
+
+    &::placeholder {
+      color: #9ca3af;
+    }
+  }
+
+  .form-select {
+    cursor: pointer;
+  }
+
+  .input-with-hint {
+    .input-hint {
+      display: block;
+      margin-top: 6px;
+      font-size: 12px;
+      color: #6b7280;
+    }
+  }
+
+  .checkbox-label {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    cursor: pointer;
+    padding: 12px;
+    border-radius: 8px;
+    transition: background 0.2s ease;
+
+    &:hover {
+      background: #f9fafb;
+    }
+
+    input[type="checkbox"] {
+      width: 18px;
+      height: 18px;
+      cursor: pointer;
+      accent-color: #667eea;
+    }
+
+    span {
+      font-size: 14px;
+      color: #374151;
+      user-select: none;
+    }
+  }
+
+  .radio-group {
+    display: flex;
+    gap: 12px;
+    flex-wrap: wrap;
+
+    .radio-label {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 10px 16px;
+      border: 1.5px solid #e5e7eb;
+      border-radius: 8px;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      background: white;
+
+      &:hover {
+        border-color: #667eea;
+        background: #f5f7ff;
+      }
+
+      input[type="radio"] {
+        cursor: pointer;
+        accent-color: #667eea;
+      }
+
+      span {
+        font-size: 14px;
+        color: #374151;
+        user-select: none;
+      }
+
+      &:has(input:checked) {
+        border-color: #667eea;
+        background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+        font-weight: 500;
+      }
+    }
+  }
+}
+
+// ============ 场景选择网格 ============
+
+.scene-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  gap: 12px;
+
+  .scene-card {
+    padding: 16px 12px;
+    border: 1.5px solid #e5e7eb;
+    border-radius: 10px;
+    background: white;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+
+    &:hover {
+      border-color: #667eea;
+      background: #f5f7ff;
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
+    }
+
+    &:active {
+      transform: translateY(0);
+    }
+
+    &.selected {
+      border-color: #667eea;
+      background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
+      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+
+      .scene-name {
+        color: #667eea;
+        font-weight: 600;
+      }
+    }
+
+    &.custom {
+      border-style: dashed;
+      border-color: #9ca3af;
+      color: #6b7280;
+
+      &:hover {
+        border-color: #667eea;
+        border-style: solid;
+      }
+
+      &.selected {
+        border-style: solid;
+        border-color: #667eea;
+        color: #667eea;
+      }
+    }
+
+    .scene-name {
+      font-size: 13px;
+      font-weight: 500;
+      color: #374151;
+      transition: all 0.2s ease;
+    }
+  }
+}
+
+// ============ 加价规则配置 ============
+
+.pricing-adjustments {
+  margin-top: 24px;
+  padding: 20px;
+  background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
+  border-radius: 12px;
+  border: 1px solid #e5e7eb;
+
+  .section-title {
+    margin: 0 0 16px 0;
+    font-size: 15px;
+    font-weight: 600;
+    color: #374151;
+  }
+
+  .form-group {
+    margin-bottom: 16px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+// ============ 价格预览 ============
+
+.price-preview {
+  margin-top: 24px;
+  padding: 20px;
+  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+  border-radius: 12px;
+  border: 2px solid #fbbf24;
+
+  .price-preview-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 8px 0;
+    font-size: 14px;
+
+    &.adjustment {
+      color: #f59e0b;
+      font-weight: 500;
+
+      .price {
+        color: #f59e0b;
+      }
+    }
+
+    &.total {
+      margin-top: 8px;
+      padding-top: 12px;
+      border-top: 2px solid rgba(251, 191, 36, 0.3);
+      font-size: 16px;
+      font-weight: 600;
+
+      .price {
+        font-size: 24px;
+        color: #d97706;
+      }
+    }
+
+    .label {
+      color: #92400e;
+      font-weight: 500;
+    }
+
+    .price {
+      font-weight: 600;
+      color: #92400e;
+    }
+  }
+}
+
+// ============ 内部分配小卡片 ============
+
+.allocation-mini {
+  margin-top: 20px;
+  padding: 16px;
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  border-radius: 10px;
+  border: 1px solid #86efac;
+
+  .section-title {
+    margin: 0 0 12px 0;
+    font-size: 14px;
+    font-weight: 600;
+    color: #166534;
+  }
+
+  .allocation-mini-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+    gap: 10px;
+
+    .allocation-mini-item {
+      padding: 10px 12px;
+      background: white;
+      border-radius: 8px;
+      border-left: 3px solid;
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+
+      &.modeling {
+        border-left-color: #8b5cf6;
+      }
+
+      &.decoration {
+        border-left-color: #f59e0b;
+      }
+
+      &.company {
+        border-left-color: #10b981;
+      }
+
+      .label {
+        font-size: 11px;
+        color: #6b7280;
+        font-weight: 500;
+      }
+
+      .value {
+        font-size: 14px;
+        font-weight: 600;
+        color: #111827;
+      }
+    }
+  }
+}
+
+// ============ 分配明细网格样式 ============
+
+.allocation-section-detail {
+  margin-top: 20px;
+
+  .section-title {
+    margin: 0 0 16px 0;
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+
+    .section-subtitle {
+      font-size: 13px;
+      font-weight: 400;
+      color: #6b7280;
+      margin-left: 8px;
+    }
+  }
+
+  .allocation-grid-detail {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    .allocation-item-detail {
+      border: 1.5px solid #e5e7eb;
+      border-radius: 10px;
+      padding: 14px 16px;
+      background: white;
+      transition: all 0.2s ease;
+
+      &:not(.enabled) {
+        opacity: 0.5;
+        background: #f9fafb;
+      }
+
+      &.enabled {
+        border-left-width: 4px;
+      }
+
+      &[data-type="modeling"].enabled {
+        border-left-color: #8b5cf6;
+        background: linear-gradient(90deg, rgba(139, 92, 246, 0.03) 0%, white 100%);
+      }
+
+      &[data-type="decoration"].enabled {
+        border-left-color: #f59e0b;
+        background: linear-gradient(90deg, rgba(245, 158, 11, 0.03) 0%, white 100%);
+      }
+
+      &[data-type="company"].enabled {
+        border-left-color: #10b981;
+        background: linear-gradient(90deg, rgba(16, 185, 129, 0.03) 0%, white 100%);
+      }
+
+      .allocation-header-detail {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 12px;
+
+        .allocation-left {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          flex: 1;
+
+          .checkbox-wrapper {
+            display: flex;
+            align-items: center;
+
+            .checkbox-input {
+              width: 18px;
+              height: 18px;
+              cursor: pointer;
+              accent-color: #667eea;
+            }
+
+            .checkbox-custom {
+              display: none;
+            }
+          }
+
+          .allocation-info-detail {
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+
+            .allocation-name-detail {
+              font-size: 15px;
+              font-weight: 600;
+              color: #111827;
+            }
+
+            .allocation-desc-detail {
+              font-size: 12px;
+              color: #6b7280;
+            }
+          }
+        }
+
+        .allocation-percentage-badge {
+          padding: 4px 12px;
+          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+          color: white;
+          border-radius: 16px;
+          font-size: 13px;
+          font-weight: 600;
+        }
+      }
+
+      .allocation-input-section {
+        padding-left: 30px;
+
+        .input-label-small {
+          display: block;
+          margin-bottom: 8px;
+          font-size: 13px;
+          font-weight: 500;
+          color: #374151;
+        }
+
+        .input-with-currency {
+          position: relative;
+          display: flex;
+          align-items: center;
+
+          .currency-symbol {
+            position: absolute;
+            left: 14px;
+            font-size: 14px;
+            font-weight: 600;
+            color: #6b7280;
+            pointer-events: none;
+          }
+
+          .amount-input {
+            width: 100%;
+            padding: 10px 14px 10px 32px;
+            border: 1.5px solid #e5e7eb;
+            border-radius: 8px;
+            font-size: 16px;
+            font-weight: 600;
+            color: #111827;
+            transition: all 0.2s ease;
+            outline: none;
+
+            &:focus {
+              border-color: #667eea;
+              box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+            }
+
+            &:disabled {
+              background: #f3f4f6;
+              cursor: not-allowed;
+            }
+          }
+        }
+
+        .allocation-hint {
+          margin-top: 6px;
+          font-size: 12px;
+          color: #6b7280;
+          padding-left: 2px;
+        }
+      }
+    }
+  }
+}
+
+// ============ 移动端适配(模态框) ============
+
+@media (max-width: 768px) {
+  .modal-container {
+    max-width: none;
+    width: 100%;
+    max-height: 95vh;
+    margin: 0;
+    border-radius: 16px 16px 0 0;
+  }
+
+  .modal-header {
+    padding: 16px 20px;
+
+    h3 {
+      font-size: 18px;
+    }
+  }
+
+  .modal-body {
+    padding: 20px;
+  }
+
+  .modal-footer {
+    padding: 12px 20px;
+
+    button {
+      flex: 1;
+      padding: 12px;
+    }
+  }
+
+  .scene-grid {
+    grid-template-columns: repeat(3, 1fr);
+    gap: 10px;
+
+    .scene-card {
+      padding: 12px 8px;
+
+      .scene-name {
+        font-size: 12px;
+      }
+    }
+  }
+
+  .form-group .radio-group {
+    flex-direction: column;
+
+    .radio-label {
+      width: 100%;
+    }
+  }
+
+  .allocation-mini .allocation-mini-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .price-preview {
+    padding: 16px;
+
+    .price-preview-row.total .price {
+      font-size: 20px;
+    }
+  }
+}

File diff suppressed because it is too large
+ 365 - 299
src/modules/project/components/quotation-editor.component.ts


Some files were not shown because too many files changed in this diff