quotation-editor.component.html 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. <!-- 基于Product表的报价编辑器组件 -->
  2. <div class="quotation-editor">
  3. <!-- 加载状态 -->
  4. @if (loading) {
  5. <div class="loading-container">
  6. <div class="spinner">
  7. <div class="spinner-circle"></div>
  8. </div>
  9. <p>加载报价数据...</p>
  10. </div>
  11. }
  12. @if (!loading) {
  13. <!-- 产品管理区域 -->
  14. @if (canEdit) {
  15. <div class="product-management">
  16. <div class="product-header">
  17. <h3>设计产品 ({{ products.length }}个空间)</h3>
  18. <div class="product-actions">
  19. <button class="btn-primary" (click)="generateQuotationFromProducts()">
  20. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  21. <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"/>
  22. </svg>
  23. 生成报价
  24. </button>
  25. <button class="btn-secondary" (click)="openAddProductModal()">
  26. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  27. <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"/>
  28. </svg>
  29. 添加产品
  30. </button>
  31. <button class="btn-outline" (click)="cleanupDuplicateProducts()" title="清理重复产品">
  32. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  33. <path fill="currentColor" d="M432 64c26.5 0 48 21.5 48 48v288c0 26.5-21.5 48-48 48H80c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h352zm0-32H80C35.8 32 0 67.8 0 112v288c0 44.2 35.8 80 80 80h352c44.2 0 80-35.8 80-80V112c0-44.2-35.8-80-80-80zM362.7 184l-82.7 82.7L197.3 184c-6.2-6.2-16.4-6.2-22.6 0-6.2 6.2-6.2 16.4 0 22.6l82.7 82.7-82.7 82.7c-6.2 6.2-6.2 16.4 0 22.6 6.2 6.2 16.4 6.2 22.6 0l82.7-82.7 82.7 82.7c6.2 6.2 16.4 6.2 22.6 0 6.2-6.2 6.2-16.4 0-22.6L303.3 289.3l82.7-82.7c6.2-6.2 6.2-16.4 0-22.6-6.2-6.2-16.4-6.2-22.6 0z"/>
  34. </svg>
  35. 清理重复
  36. </button>
  37. <button class="btn-outline" (click)="saveQuotation()">
  38. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  39. <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"/>
  40. </svg>
  41. 保存报价
  42. </button>
  43. </div>
  44. </div>
  45. </div>
  46. }
  47. @if (quotation.spaces.length === 0 && products.length > 0) {
  48. <!-- 空状态 - 有产品但未生成报价 -->
  49. <div class="empty-state">
  50. <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  51. <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"/>
  52. </svg>
  53. <p class="empty-message">尚未生成报价</p>
  54. <p class="empty-hint">已加载 {{ products.length }} 个设计空间,请点击"生成报价"按钮</p>
  55. @if (canEdit) {
  56. <button class="btn-primary" (click)="generateQuotationFromProducts()">立即生成报价</button>
  57. }
  58. </div>
  59. } @else if (quotation.spaces.length === 0) {
  60. <!-- 完全空状态 - 无产品 -->
  61. <div class="empty-state">
  62. <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  63. <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"/>
  64. </svg>
  65. <p class="empty-message">暂无设计产品</p>
  66. <p class="empty-hint">该项目还没有创建任何设计产品</p>
  67. @if (canEdit) {
  68. <button class="btn-primary" (click)="openAddProductModal()">创建第一个产品</button>
  69. }
  70. </div>
  71. } @else {
  72. <!-- 报价工具栏 -->
  73. <div class="quotation-toolbar">
  74. <div class="toolbar-left">
  75. <h4 class="toolbar-title">报价明细 ({{ quotation.spaces.length }}个设计空间)</h4>
  76. <div class="toolbar-meta">
  77. @if (quotation.generatedAt) {
  78. <span class="generate-time">生成于: {{ quotation.generatedAt | date:'MM-dd HH:mm' }}</span>
  79. }
  80. @if (quotation.validUntil) {
  81. <span class="valid-until">有效期至: {{ quotation.validUntil | date:'yyyy-MM-dd' }}</span>
  82. }
  83. </div>
  84. </div>
  85. <div class="toolbar-right">
  86. <button class="btn-icon" (click)="expandAll()" title="展开全部">
  87. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  88. <path fill="currentColor" d="M112 184l144 144 144-144M256 328V88"/>
  89. </svg>
  90. </button>
  91. <button class="btn-icon" (click)="collapseAll()" title="折叠全部">
  92. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  93. <path fill="currentColor" d="M112 328l144-144 144 144M256 184v240"/>
  94. </svg>
  95. </button>
  96. </div>
  97. </div>
  98. <!-- 卡片视图 -->
  99. @if (viewMode === 'card') {
  100. <div class="quotation-products">
  101. @for (space of quotation.spaces; track space.name) {
  102. <div class="product-card" [class.expanded]="isProductExpanded(space.name)">
  103. <!-- 产品头部 -->
  104. <div class="product-header" (click)="toggleProductExpand(space.name)">
  105. <div class="product-info">
  106. <div class="product-title">
  107. <div class="product-icon">
  108. <i class="icon-{{ getProductIconForSpace(space.name) }}"></i>
  109. </div>
  110. <div class="product-details">
  111. <h3 class="product-name">{{ space.name }}</h3>
  112. <div class="product-meta">
  113. <span class="badge" [attr.data-color]="getStatusColorForSpace(space.productId)">
  114. {{ getStatusTextForSpace(space.productId) }}
  115. </span>
  116. <span class="designer-name">{{ getDesignerNameForSpace(space.productId) }}</span>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="product-pricing">
  121. <p class="product-subtotal">{{ formatPrice(calculateSpaceSubtotal(space)) }}</p>
  122. @if (quotation.spaceBreakdown?.length > 1) {
  123. <span class="percentage">{{ formatPercentage(getSpacePercentage(space.productId)) }}</span>
  124. }
  125. </div>
  126. </div>
  127. <div class="product-actions">
  128. @if (canEdit) {
  129. <button class="btn-icon" (click)="openEditProductModal(space.productId); $event.stopPropagation()" title="编辑">
  130. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  131. <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"/>
  132. </svg>
  133. </button>
  134. <button class="btn-icon danger" (click)="deleteProduct(space.productId); $event.stopPropagation()" title="删除">
  135. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  136. <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"/>
  137. </svg>
  138. </button>
  139. }
  140. <div class="product-toggle">
  141. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  142. <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"/>
  143. </svg>
  144. </div>
  145. </div>
  146. </div>
  147. <!-- 产品详情 -->
  148. @if (isProductExpanded(space.name)) {
  149. <div class="product-content">
  150. <!-- 产品信息与价格明细 -->
  151. @if (getProductForSpace(space.productId)) {
  152. <div class="product-details-section">
  153. <h5 class="section-title">
  154. <svg class="title-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  155. <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"/>
  156. </svg>
  157. 空间信息与报价明细
  158. </h5>
  159. <!-- 价格卡片 -->
  160. <div class="price-cards-grid">
  161. <div class="price-card base-price">
  162. <div class="price-card-icon">
  163. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  164. <path fill="currentColor" d="M512 80c0 18-14.3 34.6-38.4 48c-29.1 16.1-72.5 27.5-122.3 30.9c-3.7-1.8-7.4-3.5-11.3-5C300.6 137.4 248.2 128 192 128c-8.3 0-16.4 .2-24.5 .6l-1.1-.6C142.3 114.6 128 98 128 80c0-44.2 86-80 192-80S512 35.8 512 80zM160.7 161.1c10.2-.7 20.7-1.1 31.3-1.1c62.2 0 117.4 12.3 152.5 31.4C369.3 204.9 384 221.7 384 240c0 4-.7 7.9-2.1 11.7c-4.6 13.2-17 25.3-35 35.5c0 0 0 0 0 0c-.1 .1-.3 .1-.4 .2l0 0 0 0c-.3 .2-.6 .3-.9 .5c-35 19.4-90.8 32-153.6 32c-59.6 0-112.9-11.3-148.2-29.1c-1.9-.9-3.7-1.9-5.5-2.9C14.3 274.6 0 258 0 240c0-34.8 53.4-64.5 128-75.4c10.5-1.5 21.4-2.7 32.7-3.5zM416 240c0-21.9-10.6-39.9-24.1-53.4c28.3-4.4 54.2-11.4 76.2-20.5c16.3-6.8 31.5-15.2 43.9-25.5V176c0 19.3-16.5 37.1-43.8 50.9c-14.6 7.4-32.4 13.7-52.4 18.5c.1-1.8 .2-3.5 .2-5.3zm-32 96c0 18-14.3 34.6-38.4 48c-1.8 1-3.6 1.9-5.5 2.9C304.9 404.7 251.6 416 192 416c-62.8 0-118.6-12.6-153.6-32C14.3 370.6 0 354 0 336V300.6c12.5 10.3 27.6 18.7 43.9 25.5C83.4 342.6 135.8 352 192 352s108.6-9.4 148.1-25.9c7.8-3.2 15.3-6.9 22.4-10.9c6.1-3.4 11.8-7.2 17.2-11.2c1.5-1.1 2.9-2.3 4.3-3.4V304v5.7V336zm32 0V304 278.1c19-4.2 36.5-9.5 52.1-16c16.3-6.8 31.5-15.2 43.9-25.5V272c0 10.5-5 21-14.9 30.9c-16.3 16.3-45 29.7-81.3 38.4c.1-1.7 .2-3.5 .2-5.3zM192 448c56.2 0 108.6-9.4 148.1-25.9c16.3-6.8 31.5-15.2 43.9-25.5V432c0 44.2-86 80-192 80S0 476.2 0 432V396.6c12.5 10.3 27.6 18.7 43.9 25.5C83.4 438.6 135.8 448 192 448z"/>
  165. </svg>
  166. </div>
  167. <div class="price-card-content">
  168. <div class="price-card-label">基础报价</div>
  169. <div class="price-card-value">{{ formatPrice(getProductForSpace(space.productId)?.get('quotation')?.basePrice || 0) }}</div>
  170. </div>
  171. </div>
  172. <div class="price-card total-price">
  173. <div class="price-card-icon">
  174. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  175. <path fill="currentColor" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64H80c-8.8 0-16-7.2-16-16s7.2-16 16-16H448c17.7 0 32-14.3 32-32s-14.3-32-32-32H64zM416 272c17.7 0 32 14.3 32 32s-14.3 32-32 32H352c-17.7 0-32-14.3-32-32s14.3-32 32-32h64z"/>
  176. </svg>
  177. </div>
  178. <div class="price-card-content">
  179. <div class="price-card-label">空间总价</div>
  180. <div class="price-card-value highlight">{{ formatPrice(calculateSpaceSubtotal(space)) }}</div>
  181. </div>
  182. </div>
  183. </div>
  184. <!-- 详细信息 -->
  185. <div class="detail-grid">
  186. <div class="detail-item">
  187. <svg class="detail-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  188. <path fill="currentColor" d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96z"/>
  189. </svg>
  190. <div class="detail-content">
  191. <span class="detail-label">产品类型</span>
  192. <span class="detail-value">{{ getProductForSpace(space.productId)?.get('productType') }}</span>
  193. </div>
  194. </div>
  195. <div class="detail-item">
  196. <svg class="detail-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  197. <path fill="currentColor" d="M448 96V224H288V96H448zm0 192V416H288V288H448zM224 224H64V96H224V224zM64 288H224V416H64V288zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/>
  198. </svg>
  199. <div class="detail-content">
  200. <span class="detail-label">空间面积</span>
  201. <span class="detail-value">{{ getProductForSpace(space.productId)?.get('space')?.area || 0 }}㎡</span>
  202. </div>
  203. </div>
  204. <div class="detail-item">
  205. <svg class="detail-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  206. <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
  207. </svg>
  208. <div class="detail-content">
  209. <span class="detail-label">复杂度</span>
  210. <span class="detail-value">{{ getProductForSpace(space.productId)?.get('space')?.complexity || 'medium' }}</span>
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. }
  216. <!-- 协作分工管理 -->
  217. @if (canEdit) {
  218. <div class="collaboration-section">
  219. <h5 class="section-title">
  220. <svg class="title-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  221. <path fill="currentColor" d="M256 0c53 0 96 43 96 96v3.6c0 15.7-12.7 28.4-28.4 28.4H188.4c-15.7 0-28.4-12.7-28.4-28.4V96c0-53 43-96 96-96zM41.4 105.4c12.5-12.5 32.8-12.5 45.3 0l64 64c.7 .7 1.3 1.4 1.9 2.1c14.2-7.3 30.4-11.4 47.5-11.4H312c17.1 0 33.2 4.1 47.5 11.4c.6-.7 1.2-1.4 1.9-2.1l64-64c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-64 64c-.7 .7-1.4 1.3-2.1 1.9c6.2 12 10.1 25.3 11.1 39.5H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H416c0 24.6-5.5 47.8-15.4 68.6c2.2 1.3 4.2 2.9 6 4.8l64 64c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0l-63.1-63.1c-24.5 21.8-55.8 36.2-90.3 39.6V240c0-8.8-7.2-16-16-16s-16 7.2-16 16V479.2c-34.5-3.4-65.8-17.8-90.3-39.6L86.6 502.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l64-64c1.9-1.9 3.9-3.4 6-4.8C101.5 367.8 96 344.6 96 320H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H96.3c1.1-14.1 5-27.5 11.1-39.5c-.7-.6-1.4-1.2-2.1-1.9l-64-64c-12.5-12.5-12.5-32.8 0-45.3z"/>
  222. </svg>
  223. 协作分工管理
  224. <span class="section-subtitle">(少数需协作情况可手动设置)</span>
  225. </h5>
  226. <button class="btn-add-collaboration" (click)="openCollaborationModal(space)">
  227. <svg class="btn-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  228. <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM232 344V280H168c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H280v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/>
  229. </svg>
  230. 添加协作人员
  231. </button>
  232. @if (getSpaceCollaborators(space.productId).length > 0) {
  233. <div class="collaborators-list">
  234. @for (collab of getSpaceCollaborators(space.productId); track collab.id) {
  235. <div class="collaborator-card">
  236. <div class="collaborator-info">
  237. <div class="collaborator-avatar">
  238. @if (collab.profile?.get('avatar')) {
  239. <img [src]="collab.profile.get('avatar')" [alt]="collab.profile.get('realName')">
  240. } @else {
  241. <div class="avatar-placeholder">{{ collab.profile?.get('realName')?.charAt(0) || '?' }}</div>
  242. }
  243. </div>
  244. <div class="collaborator-details">
  245. <div class="collaborator-name">{{ collab.profile?.get('realName') || '未知' }}</div>
  246. <div class="collaborator-role">{{ collab.role }}</div>
  247. </div>
  248. </div>
  249. <div class="collaborator-allocation">
  250. <div class="allocation-input-group">
  251. <label>工作占比</label>
  252. <input
  253. type="number"
  254. [(ngModel)]="collab.workload"
  255. (ngModelChange)="onCollaborationChange(space)"
  256. min="0"
  257. max="100"
  258. class="workload-input">
  259. <span class="unit">%</span>
  260. </div>
  261. <div class="allocation-amount">
  262. <label>分配金额</label>
  263. <div class="amount-display">¥{{ calculateCollaboratorAmount(space, collab.workload) }}</div>
  264. </div>
  265. </div>
  266. @if (canEdit) {
  267. <button class="btn-remove" (click)="removeCollaborator(space, collab.id)" title="移除">
  268. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  269. <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"/>
  270. </svg>
  271. </button>
  272. }
  273. </div>
  274. }
  275. </div>
  276. }
  277. </div>
  278. }
  279. <!-- 内部分配明细(3个分配:建模阶段10%、软装渲染40%、公司分配50%) -->
  280. <div class="allocation-section-detail">
  281. <h5 class="section-title">内部执行分配 <span class="section-subtitle">(基于设计图总价自动分配)</span></h5>
  282. <div class="allocation-grid-detail">
  283. @for (allocationType of allocationTypes; track allocationType.key) {
  284. <div
  285. class="allocation-item-detail"
  286. [class.enabled]="isProcessEnabled(space, allocationType.key)"
  287. [attr.data-type]="allocationType.key">
  288. <div class="allocation-header-detail">
  289. <div class="allocation-left">
  290. <label class="checkbox-wrapper">
  291. <input
  292. type="checkbox"
  293. class="checkbox-input"
  294. [checked]="isProcessEnabled(space, allocationType.key)"
  295. (change)="canEdit && toggleProcess(space, allocationType.key)"
  296. [disabled]="!canEdit" />
  297. <span class="checkbox-custom"></span>
  298. </label>
  299. <div class="allocation-info-detail">
  300. <span class="allocation-name-detail">{{ allocationType.name }}</span>
  301. <span class="allocation-desc-detail">{{ allocationType.description }}</span>
  302. </div>
  303. </div>
  304. <span class="allocation-percentage-badge">{{ allocationType.percentage }}%</span>
  305. </div>
  306. @if (isProcessEnabled(space, allocationType.key)) {
  307. <div class="allocation-input-section">
  308. <label class="input-label-small">分配金额(可编辑)</label>
  309. <div class="input-with-currency">
  310. <span class="currency-symbol">¥</span>
  311. <input
  312. class="input-field amount-input"
  313. type="number"
  314. [ngModel]="getAllocationAmount(space, allocationType.key)"
  315. (ngModelChange)="setAllocationAmount(space, allocationType.key, $event); onProcessChange()"
  316. [disabled]="!canEdit"
  317. placeholder="0" />
  318. </div>
  319. <div class="allocation-hint">
  320. 建议金额: {{ forSpacePrice(space,allocationType) }}
  321. </div>
  322. </div>
  323. }
  324. </div>
  325. }
  326. </div>
  327. </div>
  328. </div>
  329. }
  330. </div>
  331. }
  332. </div>
  333. }
  334. <!-- 报价汇总 -->
  335. <div class="quotation-summary">
  336. <div class="summary-header">
  337. <h4>报价汇总</h4>
  338. @if (quotation.spaceBreakdown?.length > 1) {
  339. <div class="breakdown-toggle">
  340. <button class="btn-text" (click)="showBreakdown = !showBreakdown">
  341. {{ showBreakdown ? '隐藏' : '显示'}}明细
  342. </button>
  343. </div>
  344. }
  345. </div>
  346. @if (quotation.spaceBreakdown?.length > 1 && showBreakdown) {
  347. <div class="breakdown-list">
  348. @for (item of quotation.spaceBreakdown; track item.spaceId) {
  349. <div class="breakdown-item">
  350. <span class="breakdown-name">{{ item.spaceName }}</span>
  351. <span class="breakdown-amount">{{ formatPrice(item.amount) }}</span>
  352. <span class="breakdown-percentage">{{ formatPercentage(item.percentage) }}</span>
  353. </div>
  354. }
  355. </div>
  356. }
  357. <!-- 内部执行分配 -->
  358. @if (quotation.allocation) {
  359. <div class="allocation-section">
  360. <div class="allocation-header">
  361. <h4>内部执行分配</h4>
  362. <button class="btn-text" (click)="toggleAllocation()">
  363. {{ showAllocation ? '隐藏' : '显示' }}
  364. </button>
  365. </div>
  366. @if (showAllocation) {
  367. <div class="allocation-list">
  368. <!-- 建模阶段 -->
  369. <div class="allocation-item modeling">
  370. <div class="allocation-icon">
  371. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  372. <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"/>
  373. </svg>
  374. </div>
  375. <div class="allocation-info">
  376. <span class="allocation-name">{{ allocationRules.modeling.label }}</span>
  377. <span class="allocation-desc">{{ allocationRules.modeling.description }}</span>
  378. </div>
  379. <div class="allocation-values">
  380. <span class="allocation-percentage">{{ quotation.allocation.modeling.percentage }}%</span>
  381. <span class="allocation-amount">{{ formatPrice(quotation.allocation.modeling.amount) }}</span>
  382. </div>
  383. </div>
  384. <!-- 软装渲染 -->
  385. <div class="allocation-item decoration">
  386. <div class="allocation-icon">
  387. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  388. <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"/>
  389. </svg>
  390. </div>
  391. <div class="allocation-info">
  392. <span class="allocation-name">{{ allocationRules.decoration.label }}</span>
  393. <span class="allocation-desc">{{ allocationRules.decoration.description }}</span>
  394. </div>
  395. <div class="allocation-values">
  396. <span class="allocation-percentage">{{ quotation.allocation.decoration.percentage }}%</span>
  397. <span class="allocation-amount">{{ formatPrice(quotation.allocation.decoration.amount) }}</span>
  398. </div>
  399. </div>
  400. <!-- 公司分配 -->
  401. <div class="allocation-item company">
  402. <div class="allocation-icon">
  403. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  404. <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"/>
  405. </svg>
  406. </div>
  407. <div class="allocation-info">
  408. <span class="allocation-name">{{ allocationRules.company.label }}</span>
  409. <span class="allocation-desc">{{ allocationRules.company.description }}</span>
  410. </div>
  411. <div class="allocation-values">
  412. <span class="allocation-percentage">{{ quotation.allocation.company.percentage }}%</span>
  413. <span class="allocation-amount">{{ formatPrice(quotation.allocation.company.amount) }}</span>
  414. </div>
  415. </div>
  416. </div>
  417. <div class="allocation-note">
  418. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  419. <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"/>
  420. </svg>
  421. <span>内部执行分配为系统自动计算,基于报价总额按固定比例拆分,所有金额均为整数</span>
  422. </div>
  423. }
  424. </div>
  425. }
  426. <div class="total-section">
  427. <div class="total-row">
  428. <span class="total-label">报价总额</span>
  429. <span class="total-amount">{{ formatPrice(quotation.total) }}</span>
  430. </div>
  431. @if (quotation.generatedAt) {
  432. <div class="total-meta">
  433. <span class="generate-info">生成于 {{ quotation.generatedAt | date:'yyyy-MM-dd HH:mm' }}</span>
  434. @if (quotation.validUntil) {
  435. <span class="valid-info">有效期至 {{ quotation.validUntil | date:'yyyy-MM-dd' }}</span>
  436. }
  437. </div>
  438. }
  439. </div>
  440. </div>
  441. }
  442. }
  443. </div>
  444. <!-- 协作人员选择模态框 -->
  445. @if (showCollaborationModal) {
  446. <div class="modal-overlay" (click)="closeCollaborationModal()">
  447. <div class="modal-container collaboration-modal" (click)="$event.stopPropagation()">
  448. <div class="modal-header">
  449. <h3>添加协作人员</h3>
  450. <button class="close-btn" (click)="closeCollaborationModal()">
  451. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  452. <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"/>
  453. </svg>
  454. </button>
  455. </div>
  456. <div class="modal-body">
  457. @if (loadingCollaborators) {
  458. <div class="loading-state">
  459. <div class="spinner"></div>
  460. <p>加载团队成员中...</p>
  461. </div>
  462. } @else {
  463. <!-- 搜索框 -->
  464. <div class="search-box">
  465. <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  466. <path fill="currentColor" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z"/>
  467. </svg>
  468. <input
  469. type="text"
  470. [(ngModel)]="collaboratorSearchTerm"
  471. (ngModelChange)="filterCollaborators()"
  472. placeholder="搜索团队成员..."
  473. class="search-input">
  474. </div>
  475. <!-- 团队成员列表 -->
  476. <div class="members-list">
  477. @if (filteredAvailableCollaborators.length === 0) {
  478. <div class="empty-state-small">
  479. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  480. <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"/>
  481. </svg>
  482. <p>没有找到可用的团队成员</p>
  483. </div>
  484. } @else {
  485. @for (member of filteredAvailableCollaborators; track member.id) {
  486. <div
  487. class="member-item"
  488. [class.selected]="isCollaboratorSelected(member.id)"
  489. (click)="toggleCollaboratorSelection(member)">
  490. <div class="member-avatar">
  491. @if (member.get('avatar')) {
  492. <img [src]="member.get('avatar')" [alt]="member.get('realName')">
  493. } @else {
  494. <div class="avatar-placeholder">{{ member.get('realName')?.charAt(0) || '?' }}</div>
  495. }
  496. </div>
  497. <div class="member-info">
  498. <div class="member-name">{{ member.get('realName') || '未知' }}</div>
  499. <div class="member-department">{{ member.get('department')?.get('name') || '未分配部门' }}</div>
  500. </div>
  501. <div class="member-checkbox">
  502. <svg *ngIf="isCollaboratorSelected(member.id)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  503. <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
  504. </svg>
  505. </div>
  506. </div>
  507. }
  508. }
  509. </div>
  510. <!-- 已选择的成员 -->
  511. @if (selectedCollaborators.length > 0) {
  512. <div class="selected-section">
  513. <h4>已选择 ({{ selectedCollaborators.length }}人)</h4>
  514. <div class="selected-list">
  515. @for (collab of selectedCollaborators; track collab.member.id) {
  516. <div class="selected-item">
  517. <div class="selected-info">
  518. <div class="selected-avatar">
  519. @if (collab.member.get('avatar')) {
  520. <img [src]="collab.member.get('avatar')" [alt]="collab.member.get('realName')">
  521. } @else {
  522. <div class="avatar-placeholder">{{ collab.member.get('realName')?.charAt(0) }}</div>
  523. }
  524. </div>
  525. <span>{{ collab.member.get('realName') }}</span>
  526. </div>
  527. <div class="selected-inputs">
  528. <div class="input-group-small">
  529. <label>角色</label>
  530. <select [(ngModel)]="collab.role" class="role-select">
  531. <option value="协作设计师">协作设计师</option>
  532. <option value="建模师">建模师</option>
  533. <option value="渲染师">渲染师</option>
  534. <option value="软装师">软装师</option>
  535. </select>
  536. </div>
  537. <div class="input-group-small">
  538. <label>占比</label>
  539. <input
  540. type="number"
  541. [(ngModel)]="collab.workload"
  542. min="0"
  543. max="100"
  544. class="workload-input-small">
  545. <span class="unit">%</span>
  546. </div>
  547. </div>
  548. <button class="btn-remove-small" (click)="removeSelectedCollaborator(collab.member.id)">
  549. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  550. <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"/>
  551. </svg>
  552. </button>
  553. </div>
  554. }
  555. </div>
  556. </div>
  557. }
  558. }
  559. </div>
  560. <div class="modal-footer">
  561. <button class="btn-secondary" (click)="closeCollaborationModal()">取消</button>
  562. <button
  563. class="btn-primary"
  564. (click)="confirmCollaborators()"
  565. [disabled]="selectedCollaborators.length === 0">
  566. 确认添加 ({{ selectedCollaborators.length }})
  567. </button>
  568. </div>
  569. </div>
  570. </div>
  571. }
  572. <!-- 产品添加/编辑模态框 -->
  573. @if (showAddProductModal) {
  574. <div class="modal-overlay" (click)="closeAddProductModal()">
  575. <div class="modal-container add-product-modal" (click)="$event.stopPropagation()">
  576. <div class="modal-header">
  577. <h3>{{ isEditMode ? '编辑设计产品' : '添加设计产品' }}</h3>
  578. <button class="close-btn" (click)="closeAddProductModal()">
  579. <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  580. <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"/>
  581. </svg>
  582. </button>
  583. </div>
  584. <div class="modal-body">
  585. <!-- 预设场景选择 -->
  586. <div class="form-group">
  587. <label class="form-label">选择空间场景</label>
  588. <div class="scene-grid">
  589. @for (scene of getPresetScenes(); track scene) {
  590. <button
  591. class="scene-card"
  592. [class.selected]="newProduct.sceneName === scene"
  593. (click)="selectScene(scene)">
  594. <span class="scene-name">{{ scene }}</span>
  595. </button>
  596. }
  597. <button
  598. class="scene-card custom"
  599. [class.selected]="newProduct.isCustom"
  600. (click)="selectCustomScene()">
  601. <span class="scene-name">自定义</span>
  602. </button>
  603. </div>
  604. </div>
  605. <!-- 自定义名称 -->
  606. @if (newProduct.isCustom) {
  607. <div class="form-group">
  608. <label class="form-label">产品名称</label>
  609. <input
  610. type="text"
  611. [(ngModel)]="newProduct.productName"
  612. placeholder="例如:客厅、主卧、大堂等"
  613. class="form-input">
  614. </div>
  615. }
  616. <!-- 家装专属配置 -->
  617. @if (projectInfo.projectType === '家装') {
  618. <div class="form-group">
  619. <label class="form-label">空间类型</label>
  620. <div class="radio-group">
  621. <label class="radio-label">
  622. <input type="radio" name="spaceType" value="平层" [(ngModel)]="newProduct.spaceType">
  623. <span>平层</span>
  624. </label>
  625. <label class="radio-label">
  626. <input type="radio" name="spaceType" value="跃层" [(ngModel)]="newProduct.spaceType">
  627. <span>跃层</span>
  628. </label>
  629. <label class="radio-label">
  630. <input type="radio" name="spaceType" value="挑空" [(ngModel)]="newProduct.spaceType">
  631. <span>挑空</span>
  632. </label>
  633. <label class="radio-label">
  634. <input type="radio" name="spaceType" value="卧室" [(ngModel)]="newProduct.spaceType">
  635. <span>卧室</span>
  636. </label>
  637. </div>
  638. </div>
  639. <div class="form-group">
  640. <label class="form-label">风格等级</label>
  641. <select [(ngModel)]="newProduct.styleLevel" class="form-select">
  642. <option value="基础风格组">基础风格组</option>
  643. <option value="中级风格组">中级风格组</option>
  644. <option value="高级风格组">高级风格组</option>
  645. <option value="顶级风格组">顶级风格组</option>
  646. </select>
  647. </div>
  648. }
  649. <!-- 工装专属配置 -->
  650. @if (projectInfo.projectType === '工装') {
  651. <div class="form-group">
  652. <label class="form-label">业态类型</label>
  653. <select [(ngModel)]="newProduct.businessType" class="form-select">
  654. <option value="办公空间">办公空间</option>
  655. <option value="商业空间">商业空间</option>
  656. <option value="娱乐空间">娱乐空间</option>
  657. <option value="酒店餐厅">酒店餐厅</option>
  658. <option value="公共空间">公共空间</option>
  659. </select>
  660. </div>
  661. <div class="form-group">
  662. <label class="form-label">空间类型</label>
  663. <div class="radio-group">
  664. <label class="radio-label">
  665. <input type="radio" name="spaceType" value="门厅空间" [(ngModel)]="newProduct.spaceType">
  666. <span>门厅空间</span>
  667. </label>
  668. <label class="radio-label">
  669. <input type="radio" name="spaceType" value="封闭空间" [(ngModel)]="newProduct.spaceType">
  670. <span>封闭空间</span>
  671. </label>
  672. </div>
  673. </div>
  674. }
  675. <!-- 建筑类专属配置 -->
  676. @if (projectInfo.projectType === '建筑类') {
  677. <div class="form-group">
  678. <label class="form-label">建筑类型</label>
  679. <select [(ngModel)]="newProduct.architectureType" class="form-select">
  680. <option value="门头">门头</option>
  681. <option value="小型单体">小型单体</option>
  682. <option value="大型单体">大型单体</option>
  683. <option value="鸟瞰">鸟瞰</option>
  684. </select>
  685. </div>
  686. }
  687. <!-- 加价规则配置 -->
  688. <div class="pricing-adjustments">
  689. <h5 class="section-title">加价规则配置(可选)</h5>
  690. @if (projectInfo.projectType === '家装' || projectInfo.projectType === '工装') {
  691. <div class="form-group">
  692. <label class="form-label">额外功能区数量</label>
  693. <div class="input-with-hint">
  694. <input
  695. type="number"
  696. [(ngModel)]="newProduct.adjustments.extraFunction"
  697. min="0"
  698. class="form-input"
  699. placeholder="0">
  700. <span class="input-hint">
  701. @if (projectInfo.projectType === '家装') {
  702. 每增加一个功能区 +100元
  703. } @else {
  704. 每增加一个功能区 +400元
  705. }
  706. </span>
  707. </div>
  708. </div>
  709. }
  710. <div class="form-group">
  711. <label class="form-label">造型复杂度加价</label>
  712. <div class="input-with-hint">
  713. <input
  714. type="number"
  715. [(ngModel)]="newProduct.adjustments.complexity"
  716. min="0"
  717. max="200"
  718. class="form-input"
  719. placeholder="0">
  720. <span class="input-hint">立面和吊顶造型复杂: 0-200元</span>
  721. </div>
  722. </div>
  723. <div class="form-group">
  724. <label class="checkbox-label">
  725. <input type="checkbox" [(ngModel)]="newProduct.adjustments.design">
  726. <span>需要我们设计(原价×2)</span>
  727. </label>
  728. </div>
  729. @if (projectInfo.projectType === '工装') {
  730. <div class="form-group">
  731. <label class="checkbox-label">
  732. <input type="checkbox" [(ngModel)]="newProduct.adjustments.panoramic">
  733. <span>需要全景渲染(原价×2)</span>
  734. </label>
  735. </div>
  736. }
  737. </div>
  738. <!-- 价格预览 -->
  739. <div class="price-preview">
  740. <div class="price-preview-row">
  741. <span class="label">基础价格:</span>
  742. <span class="price">{{ formatPrice(calculatePreviewBasePrice()) }}</span>
  743. </div>
  744. @if (calculatePreviewAdjustmentTotal() > 0) {
  745. <div class="price-preview-row adjustment">
  746. <span class="label">加价合计:</span>
  747. <span class="price">+{{ formatPrice(calculatePreviewAdjustmentTotal()) }}</span>
  748. </div>
  749. }
  750. <div class="price-preview-row total">
  751. <span class="label">最终价格:</span>
  752. <span class="price">{{ formatPrice(calculatePreviewFinalPrice()) }}</span>
  753. </div>
  754. </div>
  755. </div>
  756. <div class="modal-footer">
  757. <button class="btn-secondary" (click)="closeAddProductModal()">取消</button>
  758. <button
  759. class="btn-primary"
  760. (click)="confirmAddProduct()"
  761. [disabled]="!isNewProductValid()">
  762. {{ isEditMode ? '确认修改' : '确认添加' }}
  763. </button>
  764. </div>
  765. </div>
  766. </div>
  767. }