소스 검색

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

徐福静0235668 1 일 전
부모
커밋
ec7ef793bb

+ 38 - 32
CHANGELOG.md

@@ -2,10 +2,17 @@
 ## 第三方接口的代办工作
 - [ ] 渲染监听:渲酷农场渲染进度的数据对接及交付阶段,渲染任务的选择设置
 - [ ] 会话归档:群聊消息监听的权限审核与数据采集分析
+- [ ] 小程序订单:从数据库小程序商城同步订单到客服系统
+    - 订单同步、项目创建、报价分配、拉群成组
 
-## 2025-10-25
+## 2025-10-26
 - [ ] 智能交付:交付页面,拖拽文件,并细化大模型识别文件类型与空间的功能
-- [ ] 报价规则:价格报价器的细节规则优化
+- [ ] 改图工单:改图是独立于初期报价,后期延续的灵活工单,需要独立设计
+- [ ] 员工问卷:专业特长及偏好问卷功能
+- [ ] 方案深化:参考图区分软装、硬装、氛围等用途标签,对应不同分析结果
+
+## 2025-10-25
+- [x] 报价规则:价格报价器的细节规则优化
     - 报价细项的优化与合并
         - 内部执行报价分为:建模阶段、软装渲染、公司分配
             - 建模阶段:占比10%
@@ -13,59 +20,58 @@
             - 公司分配:占比50%
         - 报价自动分配比例是指在原有三级报价总价基础上,自动填写三类分配金额
     - 按默认比例分配 + 人工填写报价
-- [ ] 改图工单:改图是独立于初期报价,后期延续的灵活工单,需要独立设计
-- [ ] 员工问卷:专业特长及偏好问卷功能
-- [ ] 方案深化:参考图区分软装、硬装、氛围等用途标签,对应不同分析结果
 - [x] 组员派单:根据新的空间报价,修改派单逻辑,增加已派单成员移除的功能
+- [x] 项目信息:修复小图日期、交付日期的时间选择组件,小图日期必填
+
 
 # CHANGELOG 更新日志
 
 ## 2025-10-24
 ### 项目启动访谈问卷
-- 新增项目问卷:面向客户的首次需求调研,3-5分钟选择式题目,覆盖基础需求、核心侧重、协作节奏、特殊提醒四大模块。
-- 快速入口:项目详情的客户卡片展示问卷状态;未填写显示“发送问卷”,已填写显示“查看问卷”。
-- 群聊发送:支持在企业微信群聊一键发送问卷链接,客户点击即可填写。
-- 多联系人支持:同一项目的多个联系人可分别填写,答卷独立保存。
-- 结果查看:客服/组员/组长可随时查看已填写的问卷结果。
-- 自动保存与完成标记:答题过程逐题自动保存;提交后标记完成并记录时间。
-- 联系人信息补全:问卷中填写的姓名/手机号可同步至联系人资料;手机号对非本人脱敏显示。
-- 权限与隐私:客户本人与内部成员可查看完整结果,其他外部联系人无权查看。
+- [x] 新增项目问卷:面向客户的首次需求调研,3-5分钟选择式题目,覆盖基础需求、核心侧重、协作节奏、特殊提醒四大模块。
+- [x] 快速入口:项目详情的客户卡片展示问卷状态;未填写显示“发送问卷”,已填写显示“查看问卷”。
+- [x] 群聊发送:支持在企业微信群聊一键发送问卷链接,客户点击即可填写。
+- [x] 多联系人支持:同一项目的多个联系人可分别填写,答卷独立保存。
+- [x] 结果查看:客服/组员/组长可随时查看已填写的问卷结果。
+- [x] 自动保存与完成标记:答题过程逐题自动保存;提交后标记完成并记录时间。
+- [x] 联系人信息补全:问卷中填写的姓名/手机号可同步至联系人资料;手机号对非本人脱敏显示。
+- [x] 权限与隐私:客户本人与内部成员可查看完整结果,其他外部联系人无权查看。
 
 ### 页面手机端适配
-- 项目详情-文件,标题与筛选操作栏,紧凑布局
-- 项目详情-成员,标题与筛选操作栏,紧凑布局
-- 项目详情-问题,标题与筛选操作栏,紧凑布局并全屏弹窗
+- [x] 项目详情-文件,标题与筛选操作栏,紧凑布局
+- [x] 项目详情-成员,标题与筛选操作栏,紧凑布局
+- [x] 项目详情-问题,标题与筛选操作栏,紧凑布局并全屏弹窗
 
 ### 组员分配(项目详情页)
-- 增加删除组员功能,将组员移出项目组
-- 企业微信SDK:添加成员后,自动邀请进群(企业微信PC/手机端)
+- [x] 增加删除组员功能,将组员移出项目组
+- [x] 企业微信SDK:添加成员后,自动邀请进群(企业微信PC/手机端)
 
 ## 2025-10-23
 
 ### 员工管理(后台)
-- 员工列表显示头像与职位,缺失头像自动使用统一占位图,列表更整齐。
-- 员工详情弹窗更丰富:手机号、邮箱、企微ID、身份、部门、入职时间、技能与工作量等一目了然。
-- 弹窗样式与布局优化,对齐与间距更合理,信息更易读。
+- [x] 员工列表显示头像与职位,缺失头像自动使用统一占位图,列表更整齐。
+- [x] 员工详情弹窗更丰富:手机号、邮箱、企微ID、身份、部门、入职时间、技能与工作量等一目了然。
+- [x] 弹窗样式与布局优化,对齐与间距更合理,信息更易读。
 
 ### 表格与界面一致性
-- 多页面的列宽与对齐优化,整体信息密度与可读性提升,浏览体验更统一。
+- [x] 多页面的列宽与对齐优化,整体信息密度与可读性提升,浏览体验更统一。
 
 ### 稳定性与兼容性
-- 刷新后列表自动同步最新数据,减少信息不一致的情况。
-- 无跟进记录时自动显示历史跟进信息,保证页面内容完整性。
+- [x] 刷新后列表自动同步最新数据,减少信息不一致的情况。
+- [x] 无跟进记录时自动显示历史跟进信息,保证页面内容完整性。
 ## 2025-10-22
 
 ### 客户选择与详情体验
-- 项目页支持便捷选择或创建客户,已建档/未建档清晰分区,搜索更高效。
-- 新增“一键刷新客户信息”,可同步企业微信的最新资料,名单与详情保持一致。
-- 客户详情以侧栏弹窗方式展示,点击返回或遮罩即可关闭,不会跳转到错误页面。
-- 跟进记录默认显示当前项目的记录,支持切换查看该客户的全部跟进历史。
+- [x] 项目页支持便捷选择或创建客户,已建档/未建档清晰分区,搜索更高效。
+- [x] 新增“一键刷新客户信息”,可同步企业微信的最新资料,名单与详情保持一致。
+- [x] 客户详情以侧栏弹窗方式展示,点击返回或遮罩即可关闭,不会跳转到错误页面。
+- [x] 跟进记录默认显示当前项目的记录,支持切换查看该客户的全部跟进历史。
 
 ### 客户信息显示优化
-- 统一头像占位图为 `/assets/images/default-avatar.svg`,列表与详情一致,缺失头像时显示更友好。
-- “所在群聊”改为纵向列表,信息展示更完整,阅读更舒适。
+- [x] 统一头像占位图为 `/assets/images/default-avatar.svg`,列表与详情一致,缺失头像时显示更友好。
+- [x] “所在群聊”改为纵向列表,信息展示更完整,阅读更舒适。
 
 ### 客户管理(后台)
-- 客户列表增加头像、类型、名称等信息显示,点击即可查看详情。
-- 在详情中可直接刷新客户数据,确保资料实时准确。
+- [x] 客户列表增加头像、类型、名称等信息显示,点击即可查看详情。
+- [x] 在详情中可直接刷新客户数据,确保资料实时准确。
 

+ 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 重要说明
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 683 - 337
docs/prd/功能-报价自动分配.md


+ 1 - 0
rules/schemas.md

@@ -385,6 +385,7 @@ GroupChat "n" --> "1" Project : 关联项目(可选)
 | assignee | Pointer | 否 | 负责设计师 | → Profile |
 | status | String | 是 | 项目状态 | "进行中" |
 | currentStage | String | 是 | 当前阶段 | "建模" |
+| demoday | Date | 否 | 小图时间 | 2024-12-31T00:00:00.000Z |
 | deadline | Date | 否 | 截止时间 | 2024-12-31T00:00:00.000Z |
 | data | Object | 否 | 扩展数据 | { requirements, stageHistory, ... } |
 | isDeleted | Boolean | 否 | 软删除标记 | false |

+ 25 - 2
src/modules/project/components/contact-selector/contact-selector.component.scss

@@ -26,5 +26,28 @@
 .sub { font-size:12px; color:#777; }
 .ops { display:flex; align-items:center; }
 .overlay { position:fixed; inset:0; background:rgba(0,0,0,0.2); }
-.customer-panel { z-index:100;position:fixed; right:20px; top:60px; width:480px; height:80vh; background:#fff; border:1px solid #ddd; border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,0.1); overflow:auto; padding:8px; }
-.customer-panel .close { position:absolute; right:12px; top:10px; padding:6px 10px; }
+@media (max-width: 768px) {
+    .customer-panel { 
+        width:100vw!important;
+        height:100vh!important;
+        top:0px!important;
+        left:0px;
+        position: absolute;
+    }
+}
+
+.customer-panel { 
+    z-index:100;position:fixed;
+     right:20px;
+     top:60px;
+     width:480px;
+     height:90vh;
+     background:#fff;
+     border:1px solid #ddd;
+     border-radius:8px;
+     box-shadow:0 8px 24px rgba(0,0,0,0.1);
+     overflow:auto;
+     padding:8px;
+ }
+.customer-panel .close { 
+    position:absolute; right:12px; top:10px; padding:6px 10px; }

+ 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)="openEditProductModal(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>{{ isEditMode ? '编辑设计产品' : '添加设计产品' }}</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()">
+          {{ isEditMode ? '确认修改' : '确认添加' }}
+        </button>
       </div>
     </div>
-  }
+  </div>
 }

+ 720 - 3
src/modules/project/components/quotation-editor.component.scss

@@ -1001,7 +1001,7 @@
         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 0;
+        margin: 0 -20px 0px;
         padding: 16px 20px;
 
         .total-label {
@@ -1060,7 +1060,7 @@
     .allocation-section {
       .allocation-list {
         .allocation-item {
-          flex-wrap: wrap;
+          flex-wrap: nowrap;
           padding: 14px;
 
           .allocation-icon {
@@ -1075,7 +1075,7 @@
 
           .allocation-info {
             flex: 1;
-            min-width: 0;
+            min-width: 120px;
 
             .allocation-name {
               font-size: 14px;
@@ -1125,3 +1125,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;
+    }
+  }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 367 - 299
src/modules/project/components/quotation-editor.component.ts


+ 1 - 2
src/modules/project/pages/project-detail/stages/stage-aftercare.component.ts

@@ -5,7 +5,7 @@ import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
 import { ProjectFileService } from '../../../services/project-file.service';
 import { ProductSpaceService, Project } from '../../../services/product-space.service';
-import { WxworkAuth } from 'fmode-ng/social';
+import { WxworkAuth } from 'fmode-ng/core';
 
 const Parse = FmodeParse.with('nova');
 
@@ -343,7 +343,6 @@ export class StageAftercareComponent implements OnInit {
       this.cid = params['cid'] || localStorage.getItem("company") || '';
       this.projectId = params['projectId'] || this.project?.id || '';
       
-      // @ts-ignore - WxworkAuth type issue with fmode-ng
       let wwauth = new WxworkAuth({cid:this.cid})
       this.currentUser = await wwauth.currentProfile();
       await this.loadData();

+ 26 - 6
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -78,12 +78,32 @@
 
           <!-- 交付期限 -->
           <div class="form-group">
-            <label class="form-label">交付期限 <span class="required">*</span></label>
-            <input
-              class="form-input"
-              type="date"
-              [(ngModel)]="projectInfo.deadline"
-              [disabled]="!canEdit" />
+            <label class="form-label">小图日期 <span class="required">*</span></label>
+            <mat-form-field appearance="outline" class="date-picker-field">
+              <input
+                matInput
+                [matDatepicker]="demodayPicker"
+                [(ngModel)]="projectInfo.demoday"
+                [disabled]="!canEdit"
+                placeholder="选择小图日期">
+              <mat-datepicker-toggle matIconSuffix [for]="demodayPicker"></mat-datepicker-toggle>
+              <mat-datepicker #demodayPicker></mat-datepicker>
+            </mat-form-field>
+          </div>
+
+          <!-- 交付期限 -->
+          <div class="form-group">
+            <label class="form-label">交付日期 </label>
+            <mat-form-field appearance="outline" class="date-picker-field">
+              <input
+                matInput
+                [matDatepicker]="deadlinePicker"
+                [(ngModel)]="projectInfo.deadline"
+                [disabled]="!canEdit"
+                placeholder="选择交付日期">
+              <mat-datepicker-toggle matIconSuffix [for]="deadlinePicker"></mat-datepicker-toggle>
+              <mat-datepicker #deadlinePicker></mat-datepicker>
+            </mat-form-field>
           </div>
 
           <!-- 项目描述 -->

+ 76 - 0
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -1,5 +1,81 @@
 // 订单分配阶段样式 - 纯 div+scss 实现
 
+// ============ Material DatePicker 样式 ============
+.date-picker-field {
+  width: 100%;
+
+  ::ng-deep {
+    .mat-mdc-form-field-infix {
+      padding-top: 8px;
+      padding-bottom: 8px;
+    }
+
+    .mat-mdc-text-field-wrapper {
+      padding-left: 0;
+      padding-right: 0;
+    }
+
+    .mat-mdc-form-field-subscript-wrapper {
+      display: none; // 隐藏底部的提示文本区域
+    }
+
+    .mat-mdc-form-field-focus-overlay {
+      background-color: transparent;
+    }
+
+    .mat-datepicker-toggle {
+      color: var(--ion-color-primary, #3880ff);
+    }
+
+    input.mat-mdc-input-element {
+      padding: 10px 14px;
+      font-size: 14px;
+      color: #111827;
+
+      &::placeholder {
+        color: #9ca3af;
+      }
+
+      &:disabled {
+        color: #6b7280;
+        cursor: not-allowed;
+      }
+    }
+
+    .mat-mdc-form-field-outline {
+      border-radius: 8px;
+    }
+
+    .mdc-notched-outline__leading,
+    .mdc-notched-outline__notch,
+    .mdc-notched-outline__trailing {
+      border-color: #e5e7eb;
+      border-width: 1.5px;
+    }
+
+    .mdc-notched-outline:hover .mdc-notched-outline__leading,
+    .mdc-notched-outline:hover .mdc-notched-outline__notch,
+    .mdc-notched-outline:hover .mdc-notched-outline__trailing {
+      border-color: var(--ion-color-primary, #3880ff);
+    }
+
+    .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
+    .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
+    .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
+      border-color: var(--ion-color-primary, #3880ff);
+    }
+
+    .mat-mdc-form-field.mat-form-field-disabled {
+      .mdc-notched-outline__leading,
+      .mdc-notched-outline__notch,
+      .mdc-notched-outline__trailing {
+        border-color: #d1d5db;
+        background-color: #f3f4f6;
+      }
+    }
+  }
+}
+
 // 文件上传相关样式
 .files-card {
   .wx-support-indicator {

+ 32 - 11
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -2,7 +2,11 @@ import { Component, OnInit, Input, ViewChild, ElementRef, ChangeDetectionStrateg
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { FmodeObject, FmodeParse, WxworkSDK } from 'fmode-ng/core';
+import { FmodeObject, FmodeParse, WxworkAuth, WxworkSDK } from 'fmode-ng/core';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatInputModule } from '@angular/material/input';
+import { MatNativeDateModule } from '@angular/material/core';
+import { MatFormFieldModule } from '@angular/material/form-field';
 
 import { ProjectFileService } from '../../../services/project-file.service';
 import { ProductSpaceService, Project } from '../../../services/product-space.service';
@@ -35,7 +39,16 @@ const Parse = FmodeParse.with('nova');
 @Component({
   selector: 'app-stage-order',
   standalone: true,
-  imports: [CommonModule, FormsModule, QuotationEditorComponent, TeamAssignComponent],
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatDatepickerModule,
+    MatInputModule,
+    MatNativeDateModule,
+    MatFormFieldModule,
+    QuotationEditorComponent,
+    TeamAssignComponent
+  ],
   templateUrl: './stage-order.component.html',
   styleUrls: ['./stage-order.component.scss'],
   changeDetection: ChangeDetectionStrategy.OnPush
@@ -54,12 +67,14 @@ export class StageOrderComponent implements OnInit {
     title: '',
     projectType: '', // 家装 | 工装
     renderType: '', // 静态单张 | 360全景
-    deadline: '',
+    demoday: new Date() as Date | null,
+    deadline: new Date() as Date | null,
     description: '',
     priceLevel: '一级', // 一级(老客户) | 二级(中端组) | 三级(高端组)
     spaceType: 'single' // single | multi
   };
 
+  
   // 空间管理
   projectSpaces: Project[] = [];
   isMultiSpaceProject: boolean = false;
@@ -285,8 +300,8 @@ export class StageOrderComponent implements OnInit {
       }
 
       if (!this.currentUser && this.cid) {
-        const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
-        this.currentUser = await wxwork.getCurrentUser();
+        const wxwork = new WxworkAuth({ cid: this.cid, appId: 'crm' });
+        this.currentUser = await wxwork.currentProfile();
 
         const role = this.currentUser?.get('roleName') || '';
         this.canEdit = ['客服', '组员', '组长', '管理员'].includes(role);
@@ -297,7 +312,8 @@ export class StageOrderComponent implements OnInit {
         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.deadline = this.project.get('deadline') || new Date();
+        this.projectInfo.demoday = this.project.get('demoday') || new Date();
         this.projectInfo.description = this.project.get('description') || '';
 
         const data = this.project.get('data') || {};
@@ -739,6 +755,7 @@ export class StageOrderComponent implements OnInit {
       this.project.set('projectType', this.projectInfo.projectType);
       this.project.set('renderType', this.projectInfo.renderType);
       this.project.set('deadline', this.projectInfo.deadline);
+      this.project.set('demoday', this.projectInfo.demoday);
       this.project.set('description', this.projectInfo.description);
       this.project.set('spaceType', this.projectInfo.spaceType);
 
@@ -777,10 +794,14 @@ export class StageOrderComponent implements OnInit {
       alert('请选择项目类型');
       return;
     }
-    if (!this.projectInfo.deadline) {
-      alert('请选择交付期限');
+    if (!this.projectInfo.demoday) {
+      alert('请选择小图日期');
       return;
     }
+    // if (!this.projectInfo.deadline) {
+    //   alert('请选择交付期限');
+    //   return;
+    // }
     if (this.quotation.total === 0) {
       alert('请配置报价明细');
       return;
@@ -823,9 +844,9 @@ export class StageOrderComponent implements OnInit {
       approvalHistory.push({
         stage: '订单分配',
         submitter: {
-          id: this.currentUser!.id,
-          name: this.currentUser!.get('name'),
-          role: this.currentUser!.get('roleName')
+          id: this.currentUser?.id,
+          name: this.currentUser?.get('name'),
+          role: this.currentUser?.get('roleName')
         },
         submitTime: new Date(),
         status: 'confirm',

+ 4 - 6
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -60,6 +60,10 @@
             </h3>
             <p class="card-subtitle">上传风格、空间或材质参考图,点击图片可查看色彩分析</p>
             <div class="ai-analysis-actions">
+              <span class="badge badge-success">
+                    <ion-icon name="checkmark-circle"></ion-icon>
+                    AI分析完成
+                </span>
               @if (referenceImages.length > 0 && canEdit) {
                 <button
                   class="btn btn-sm btn-primary"
@@ -82,12 +86,6 @@
             <!-- AI图片分析结果 -->
             @if (aiAnalysisResults.imageAnalysis && aiAnalysisResults.imageAnalysis.length > 0) {
               <div class="ai-analysis-results">
-                <div class="analysis-header">
-                  <span class="badge badge-success">
-                    <ion-icon name="checkmark-circle"></ion-icon>
-                    AI分析完成
-                  </span>
-                </div>
                 <div class="analysis-grid">
                   @for (analysis of aiAnalysisResults.imageAnalysis; track analysis.imageId) {
                     

+ 23 - 0
src/modules/project/pages/project-detail/stages/stage-requirements.component.scss

@@ -1629,8 +1629,30 @@
   }
 }
 
+
+@media (max-width: 768px) {
+    .card-header{
+      display: flex!important;
+      flex-wrap: wrap!important;
+      .card-subtitle{
+        max-width: 150px;
+      }
+    }
+    .ai-analysis-actions{
+      width:100%;
+      justify-content: between;
+    }
+      .analysis-item{
+        display: flex!important;
+        flex-direction: column!important;
+      }
+
+}
+
 // 响应式适配
 @media (min-width: 768px) {
+
+
   .stage-requirements-container {
     max-width: 800px;
     margin: 0 auto;
@@ -1648,6 +1670,7 @@
       }
     }
 
+
     .ai-solution-card {
       .ai-solution-content {
         .summary {

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.