123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- <!-- 基于Product表的报价编辑器组件 -->
- <div class="quotation-editor">
- <!-- 加载状态 -->
- @if (loading) {
- <div class="loading-container">
- <div class="spinner">
- <div class="spinner-circle"></div>
- </div>
- <p>加载报价数据...</p>
- </div>
- }
- @if (!loading) {
- <!-- 产品管理区域 -->
- @if (canEdit) {
- <div class="product-management">
- <div class="product-header">
- <h3>产品设计产品 ({{ products.length }}个)</h3>
- <div class="product-actions">
- <button class="btn-primary" (click)="generateQuotationFromProducts()">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M447.1 96h-288C131.3 96 112 115.3 112 140.4v231.2C112 396.7 131.3 416 159.1 416h288c27.6 0 48-19.3 48-44.4V140.4C495.1 115.3 475.6 96 447.1 96zM447.1 144v192h-288V144H447.1zM336 320c17.7 0 32-14.3 32-32s-14.3-32-32-32c-11.4 0-21.4 5.9-27.1 14.9c-7.2-2.4-14.9-3.7-22.9-3.7-30.9 0-56 25.1-56 56s25.1 56 56 56c8 0 15.7-1.3 22.9-3.7C314.6 314.1 324.6 320 336 320z"/>
- <path fill="currentColor" d="M176 80C176 71.16 167.8 64 160 64H80C71.16 64 64 71.16 64 80s7.163 16 16 16h32L64 192C64 209.7 81.75 224 96 224s32-14.3 32-32L112 96h48C167.8 96 176 88.84 176 80z"/>
- </svg>
- 生成报价
- </button>
- <button class="btn-secondary" (click)="addProduct()">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM256 48c114.7 0 208 93.31 208 208s-93.31 208-208 208S48 370.7 48 256S141.3 48 256 48zM256 336c13.25 0 24-10.75 24-24V280h32c13.25 0 24-10.75 24-24s-10.75-24-24-24h-32V176c0-13.25-10.75-24-24-24s-24 10.75-24 24v32H200c-13.25 0-24 10.75-24 24s10.75 24 24 24h32v32C232 325.3 242.8 336 256 336z"/>
- </svg>
- 添加产品
- </button>
- <button class="btn-outline" (click)="saveQuotation()">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M504.1 141C490.6 121.4 471.1 107.5 447.8 96C424.6 84.51 400.8 80 376.1 80H136c-24.74 0-48.48 4.511-71.79 16.01C40.88 107.5 21.36 121.4 7.85 141C-5.654 160.6-1.466 180.2 11.66 195.7L144.1 353c11.14 13.4 27.62 21 44.8 21h124.3c17.18 0 33.66-7.6 44.8-21l133.3-157.4C504.5 180.2 508.6 160.6 504.1 141zM434.1 165.6L300.7 322.1c-3.734 4.498-9.291 7.059-15.16 7.059H226.5c-5.871 0-11.43-2.561-15.16-7.059L77.86 165.6C72.16 158.7 70.54 149.7 73.65 141.3C76.77 132.9 83.98 126.5 92.95 123.4C107.3 118.7 122.4 116 137.7 116h236.5c15.28 0 30.43 2.687 44.77 7.393c8.972 3.104 16.18 9.516 19.3 17.94C441.5 149.7 439.8 158.7 434.1 165.6z"/>
- </svg>
- 保存报价
- </button>
- </div>
- </div>
- </div>
- }
- @if (quotation.spaces.length === 0 && products.length > 0) {
- <!-- 空状态 - 有产品但未生成报价 -->
- <div class="empty-state">
- <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M447.1 96h-288C131.3 96 112 115.3 112 140.4v231.2C112 396.7 131.3 416 159.1 416h288c27.6 0 48-19.3 48-44.4V140.4C495.1 115.3 475.6 96 447.1 96zM447.1 144v192h-288V144H447.1zM336 320c17.7 0 32-14.3 32-32s-14.3-32-32-32c-11.4 0-21.4 5.9-27.1 14.9c-7.2-2.4-14.9-3.7-22.9-3.7-30.9 0-56 25.1-56 56s25.1 56 56 56c8 0 15.7-1.3 22.9-3.7C314.6 314.1 324.6 320 336 320z"/>
- </svg>
- <p class="empty-message">尚未生成报价</p>
- <p class="empty-hint">已加载 {{ products.length }} 个产品设计产品,请点击"生成报价"按钮</p>
- @if (canEdit) {
- <button class="btn-primary" (click)="generateQuotationFromProducts()">立即生成报价</button>
- }
- </div>
- } @else if (quotation.spaces.length === 0) {
- <!-- 完全空状态 - 无产品 -->
- <div class="empty-state">
- <svg class="icon empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 464c-114.7 0-208-93.31-208-208S141.3 48 256 48s208 93.31 208 208S370.7 464 256 464zM256 224c-17.67 0-32 14.33-32 32c0 17.67 14.33 32 32 32s32-14.33 32-32C288 238.3 273.7 224 256 224zM320 128H192c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32v-32C352 142.3 337.7 128 320 128z"/>
- </svg>
- <p class="empty-message">暂无产品设计产品</p>
- <p class="empty-hint">该项目还没有创建任何产品设计产品</p>
- @if (canEdit) {
- <button class="btn-primary" (click)="addProduct()">创建第一个产品</button>
- }
- </div>
- } @else {
- <!-- 报价工具栏 -->
- <div class="quotation-toolbar">
- <div class="toolbar-left">
- <h4 class="toolbar-title">报价明细 ({{ quotation.spaces.length }}个产品设计产品)</h4>
- <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>
- <!-- 卡片视图 -->
- @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-actions">
- @if (canEdit) {
- <button class="btn-icon" (click)="editProduct(space.productId); $event.stopPropagation()" title="编辑">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32l39.38 39.38c25 25 25 65.62 0 90.62l-109.5 109.5c-25 25-65.62 25-90.62 0l-109.5-109.5c-25-25-25-65.62 0-90.62L272.1 19.32zM336.5 256.1L227.1 365.5c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l109.5-109.5c12.5-12.5 32.75-12.5 45.25 0S349 243.6 336.5 256.1zM192 416h64v64h-64V416z"/>
- </svg>
- </button>
- <button class="btn-icon danger" (click)="deleteProduct(space.productId); $event.stopPropagation()" title="删除">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M96 480c0 17.67 14.33 32 32 32h256c17.67 0 32-14.33 32-32V128H96V480zM312 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24c-13.25 0-24-10.75-24-24V216C288 202.8 298.8 192 312 192zM200 192c13.25 0 24 10.75 24 24v208c0 13.25-10.75 24-24 24s-24-10.75-24-24V216C176 202.8 186.8 192 200 192zM472 64h-80V48c0-26.51-21.49-48-48-48h-176C141.5 0 120 21.49 120 48v64H48c-17.67 0-32 14.33-32 32s14.33 32 32 32h32v352c0 26.51 21.49 48 48 48h256c26.51 0 48-21.49 48-48V128h32c17.67 0 32-14.33 32-32S489.7 64 472 64zM168 48h176v16H168V48zm232 400H112V128h288V448z"/>
- </svg>
- </button>
- }
- <div class="product-toggle">
- <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M256 294.1L383 167c9.4-9.4 24.6-9.4 33.9 0s9.3 24.6 0 34L273 345c-9.1 9.1-23.7 9.3-33.1.7L95 201.1c-4.7-4.7-7-10.9-7-17s2.3-12.3 7-17c9.4-9.4 24.6-9.4 33.9 0l127.1 127z"/>
- </svg>
- </div>
- </div>
- </div>
- <!-- 产品详情 -->
- @if (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>
- </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>
- </div>
- </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>
- </div>
- </div>
- <div class="process-subtotal">
- 小计: ¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}
- </div>
- </div>
- }
- </div>
- }
- </div>
- </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>
- </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>
- </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 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>
- }
- }
|