映三色视觉设计表现公司的智能报价系统,支持三级报价体系(一级老客户、二级中端组、三级高端组),实现以空间为单位的独立设计产品报价管理,自动计算内部执行分配。
| 报价等级 | 适用对象 | 价格水平 | 服务定位 |
|---|---|---|---|
| 一级 | 老客户 | 最低 | 标准服务,维护老客户关系 |
| 二级 | 中端客户 | 中等 | 中端市场,平衡性价比 |
| 三级 | 高端客户 | 最高 | 高端市场,优质服务 |
| 风格等级 | 风格类型 |
|---|---|
| 基础风格组 | 现代、北欧、简约、后现代、工业风 |
| 中级风格组 | 轻奢、日式、新中式 |
| 高级风格组 | 简欧、古典中式、欧式、美式、简美、轻法 |
| 顶级风格组 | 古典欧式、古典美式、纯法式 |
| 空间类型 | 基础风格组 | 中级风格组 | 高级风格组 | 顶级风格组 |
|---|---|---|---|---|
| 平层客餐厅 | 300元 | 350元 | 400元 | 500元 |
| 跃层客餐厅 | 400元 | 450元 | 500元 | 600元 |
| 挑空客餐厅 | 600元 | 700元 | 800元 | 900元 |
| 卧室 | 300元 | 300元 | 300元 | 300元 |
功能区加价
造型复杂度加价
设计服务加价
卧室特殊规则
| 空间类型 | 门厅空间 | 封闭空间 |
|---|---|---|
| 办公空间 | 600元 | 400元 |
| 商业空间 | 600元 | 500元 |
| 娱乐空间 | 600元 | 500元 |
| 酒店餐厅 | 600元 | 500元 |
| 公共空间 | 600元 | 600元 |
| 场景类型 | 价格 |
|---|---|
| 门头 | 700元 |
| 小型单体 | 1000元 |
| 大型单体 | 1500元 |
| 鸟瞰 | 2000元 |
设计原则: 每个空间都是一个独立的设计产品服务,有独立的:
家装预设场景:
工装预设场景:
建筑类预设场景:
场景选择逻辑:
function calculateBasePrice(config: {
priceLevel: '一级' | '二级' | '三级';
projectType: '家装' | '工装' | '建筑类';
renderType: '静态单张' | '360全景';
spaceType: string; // 平层/跃层/挑空/卧室 或 门厅空间/封闭空间
styleLevel?: string; // 家装:基础风格组/中级风格组等
businessType?: string; // 工装:办公空间/商业空间等
architectureType?: string; // 建筑类:门头/小型单体等
}): number {
// 根据报价规则表精确查找价格
// 每个空间都是独立的设计产品服务,有独立的基础价格
}
示例1: 家装平层客厅(一级-现代风格-静态单张)
基础价格 = 300元(基础风格组-平层)
示例2: 家装跃层客厅(三级-古典欧式-360全景)
基础价格 = 1700元(顶级风格组-跃层-360全景)
示例3: 工装办公门厅(二级-静态单张)
基础价格 = 700元(二级-办公空间-门厅空间-静态单张)
基于报价总价,自动按固定比例拆分为内部执行三个阶段:
| 分配类型 | 占比 | 说明 | 用途 |
|---|---|---|---|
| 建模阶段 | 10% | 3D模型构建 | 支付给建模设计师的费用 |
| 软装渲染 | 40% | 软装搭配+效果图渲染 | 支付给软装和渲染设计师的费用 |
| 公司分配 | 50% | 公司运营与利润 | 公司管理成本、平台服务费、利润等 |
建模阶段金额 = Math.round(报价总价 × 10%)
软装渲染金额 = Math.round(报价总价 × 40%)
公司分配金额 = Math.round(报价总价 × 50%)
注意: 所有金额使用 Math.round() 四舍五入到整数,确保无小数。
示例1: 厨房空间
报价总价: ¥900
建模阶段: ¥900 × 10% = ¥90
软装渲染: ¥900 × 40% = ¥360
公司分配: ¥900 × 50% = ¥450
示例2: 客厅空间
报价总价: ¥1,500
建模阶段: ¥1,500 × 10% = ¥150
软装渲染: ¥1,500 × 40% = ¥600
公司分配: ¥1,500 × 50% = ¥750
interface Product {
id: string;
project: Pointer<Project>;
productName: string; // 产品名称(空间名称)
productType: string; // 产品类型(living_room, bedroom等)
// 空间信息
space: {
spaceName: string;
area: number;
dimensions: { length: number; width: number; height: number };
spaceType: string; // 平层/跃层/挑空等
styleLevel?: string; // 风格等级(家装)
businessType?: string; // 业态类型(工装)
architectureType?: string; // 建筑类型
complexity: 'low' | 'medium' | 'high';
};
// 报价信息
quotation: {
priceLevel: '一级' | '二级' | '三级';
basePrice: number; // 基础价格(整数)
adjustments: {
extraFunction?: number; // 额外功能区数量
complexity?: number; // 造型复杂度加价
design?: boolean; // 是否需要设计服务
panoramic?: boolean; // 是否全景渲染
};
finalPrice: number; // 最终价格(整数)
// 工序明细
processes: {
modeling: { enabled: boolean; price: number; quantity: number; };
softDecor: { enabled: boolean; price: number; quantity: number; };
rendering: { enabled: boolean; price: number; quantity: number; };
postProcess: { enabled: boolean; price: number; quantity: number; };
};
// 内部分配(整数)
allocation: {
modeling: number;
decoration: number;
company: number;
};
status: 'draft' | 'confirmed' | 'modified';
validUntil: Date;
};
// 需求信息
requirements: {
colorRequirement: any;
materialRequirement: any;
lightingRequirement: any;
specificRequirements: string[];
};
status: 'not_started' | 'in_progress' | 'completed';
profile?: Pointer<Profile>; // 负责的设计师
}
interface ProjectQuotation {
spaces: Array<{
name: string;
productId: string;
processes: ProcessConfig;
subtotal: number; // 该空间小计(整数)
}>;
total: number; // 项目总价(整数)
// 产品占比明细
spaceBreakdown: Array<{
spaceName: string;
spaceId: string;
amount: number; // 整数
percentage: number; // 百分比
}>;
// 内部执行分配(整数)
allocation: {
modeling: {
amount: number;
percentage: 10;
description: '3D模型构建';
};
decoration: {
amount: number;
percentage: 40;
description: '软装搭配+效果图渲染';
};
company: {
amount: number;
percentage: 50;
description: '公司运营与利润';
};
updatedAt: Date;
};
generatedAt: Date;
validUntil: Date;
}
<!-- 添加产品模态框 -->
<div class="add-product-modal">
<div class="modal-header">
<h3>添加设计产品</h3>
<button class="close-btn" (click)="closeModal()">×</button>
</div>
<div class="modal-body">
<!-- 项目类型选择 -->
<div class="form-group">
<label>项目类型</label>
<div class="radio-group">
<label><input type="radio" name="projectType" value="家装"> 家装</label>
<label><input type="radio" name="projectType" value="工装"> 工装</label>
<label><input type="radio" name="projectType" value="建筑类"> 建筑类</label>
</div>
</div>
<!-- 渲染类型选择(家装/工装) -->
@if (projectType === '家装' || projectType === '工装') {
<div class="form-group">
<label>渲染类型</label>
<div class="radio-group">
<label><input type="radio" name="renderType" value="静态单张"> 静态单张</label>
<label><input type="radio" name="renderType" value="360全景"> 360全景</label>
</div>
</div>
}
<!-- 预设场景选择 -->
<div class="form-group">
<label>选择场景</label>
<div class="scene-grid">
@for (scene of getPresetScenes(); track scene) {
<button
class="scene-card"
[class.selected]="selectedScene === scene"
(click)="selectScene(scene)">
<i class="scene-icon icon-{{ scene.icon }}"></i>
<span>{{ scene.name }}</span>
</button>
}
<button
class="scene-card custom"
[class.selected]="selectedScene === 'custom'"
(click)="selectCustomScene()">
<i class="scene-icon icon-add"></i>
<span>自定义</span>
</button>
</div>
</div>
<!-- 风格选择(家装专属) -->
@if (projectType === '家装') {
<div class="form-group">
<label>风格等级</label>
<select [(ngModel)]="styleLevel">
<option value="基础风格组">基础风格组</option>
<option value="中级风格组">中级风格组</option>
<option value="高级风格组">高级风格组</option>
<option value="顶级风格组">顶级风格组</option>
</select>
</div>
<div class="form-group">
<label>空间类型</label>
<select [(ngModel)]="spaceType">
<option value="平层">平层</option>
<option value="跃层">跃层</option>
<option value="挑空">挑空</option>
<option value="卧室">卧室</option>
</select>
</div>
}
<!-- 业态类型(工装专属) -->
@if (projectType === '工装') {
<div class="form-group">
<label>业态类型</label>
<select [(ngModel)]="businessType">
<option value="办公空间">办公空间</option>
<option value="商业空间">商业空间</option>
<option value="娱乐空间">娱乐空间</option>
<option value="酒店餐厅">酒店餐厅</option>
<option value="公共空间">公共空间</option>
</select>
</div>
<div class="form-group">
<label>空间类型</label>
<select [(ngModel)]="spaceType">
<option value="门厅空间">门厅空间</option>
<option value="封闭空间">封闭空间</option>
</select>
</div>
}
<!-- 建筑类型(建筑类专属) -->
@if (projectType === '建筑类') {
<div class="form-group">
<label>建筑类型</label>
<select [(ngModel)]="architectureType">
<option value="门头">门头</option>
<option value="小型单体">小型单体</option>
<option value="大型单体">大型单体</option>
<option value="鸟瞰">鸟瞰</option>
</select>
</div>
}
<!-- 自定义名称 -->
<div class="form-group">
<label>产品名称</label>
<input
type="text"
[(ngModel)]="productName"
placeholder="例如:客厅、主卧、大堂等"
class="form-input">
</div>
<!-- 价格预览 -->
<div class="price-preview">
<span class="label">基础价格:</span>
<span class="price">¥{{ calculatePreviewPrice() }}</span>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" (click)="closeModal()">取消</button>
<button class="btn-primary" (click)="confirmAddProduct()">确认添加</button>
</div>
</div>
<!-- 产品卡片 -->
<div class="product-card">
<!-- 卡片头部 -->
<div class="product-header" (click)="toggleExpand(product)">
<div class="product-info">
<div class="product-icon-box">
<i class="icon-{{ product.productType }}"></i>
</div>
<div class="product-details">
<h4>{{ product.productName }}</h4>
<div class="product-tags">
<span class="tag">{{ product.space.spaceType }}</span>
@if (product.space.styleLevel) {
<span class="tag">{{ product.space.styleLevel }}</span>
}
<span class="tag status-{{ product.status }}">{{ getStatusText(product.status) }}</span>
</div>
</div>
</div>
<div class="product-pricing">
<div class="price-main">¥{{ getProductFinalPrice(product) }}</div>
<div class="price-base">基础 ¥{{ product.quotation.basePrice }}</div>
</div>
<div class="expand-icon">
<i class="icon-chevron-down"></i>
</div>
</div>
<!-- 卡片内容(展开时显示) -->
@if (isExpanded(product)) {
<div class="product-content">
<!-- 空间信息 -->
<div class="info-section">
<h5>空间信息</h5>
<div class="info-grid">
<div class="info-item">
<span class="label">面积</span>
<span class="value">{{ product.space.area || 0 }}㎡</span>
</div>
<div class="info-item">
<span class="label">复杂度</span>
<span class="value">{{ product.space.complexity }}</span>
</div>
</div>
</div>
<!-- 工序明细 -->
<div class="process-section">
<h5>工序明细</h5>
<div class="process-grid">
@for (processType of processTypes; track processType.key) {
<div class="process-card" [class.enabled]="isProcessEnabled(product, processType.key)">
<div class="process-header">
<label class="checkbox">
<input
type="checkbox"
[checked]="isProcessEnabled(product, processType.key)"
(change)="toggleProcess(product, processType.key)">
<span>{{ processType.name }}</span>
</label>
</div>
@if (isProcessEnabled(product, processType.key)) {
<div class="process-inputs">
<div class="input-group">
<label>单价</label>
<input
type="number"
[(ngModel)]="product.quotation.processes[processType.key].price"
(ngModelChange)="onProcessChange(product)"
class="input-field">
</div>
<div class="input-group">
<label>数量</label>
<input
type="number"
[(ngModel)]="product.quotation.processes[processType.key].quantity"
(ngModelChange)="onProcessChange(product)"
class="input-field">
</div>
<div class="process-subtotal">
小计: ¥{{ calculateProcessSubtotal(product, processType.key) }}
</div>
</div>
}
</div>
}
</div>
</div>
<!-- 内部分配明细(该产品) -->
<div class="allocation-mini">
<h5>内部分配</h5>
<div class="allocation-row">
<span class="label">建模阶段 (10%)</span>
<span class="value">¥{{ product.quotation.allocation.modeling }}</span>
</div>
<div class="allocation-row">
<span class="label">软装渲染 (40%)</span>
<span class="value">¥{{ product.quotation.allocation.decoration }}</span>
</div>
<div class="allocation-row">
<span class="label">公司分配 (50%)</span>
<span class="value">¥{{ product.quotation.allocation.company }}</span>
</div>
</div>
</div>
}
</div>
<div class="quotation-summary">
<div class="summary-header">
<h4>报价汇总</h4>
<button class="btn-toggle" (click)="showBreakdown = !showBreakdown">
{{ showBreakdown ? '隐藏' : '显示' }}明细
</button>
</div>
<!-- 产品占比明细 -->
@if (showBreakdown) {
<div class="breakdown-list">
@for (item of quotation.spaceBreakdown; track item.spaceId) {
<div class="breakdown-item">
<span class="name">{{ item.spaceName }}</span>
<span class="amount">¥{{ item.amount }}</span>
<span class="percentage">{{ item.percentage }}%</span>
</div>
}
</div>
}
<!-- 内部执行分配 -->
<div class="allocation-section">
<div class="allocation-header">
<h4>内部执行分配</h4>
<button class="btn-toggle" (click)="showAllocation = !showAllocation">
{{ showAllocation ? '隐藏' : '显示' }}
</button>
</div>
@if (showAllocation && quotation.allocation) {
<div class="allocation-list">
<!-- 建模阶段 -->
<div class="allocation-card modeling">
<div class="allocation-icon">
<svg>...</svg>
</div>
<div class="allocation-info">
<h5>建模阶段</h5>
<p>3D模型构建</p>
</div>
<div class="allocation-values">
<span class="percentage">10%</span>
<span class="amount">¥{{ quotation.allocation.modeling.amount }}</span>
</div>
</div>
<!-- 软装渲染 -->
<div class="allocation-card decoration">
<div class="allocation-icon">
<svg>...</svg>
</div>
<div class="allocation-info">
<h5>软装渲染</h5>
<p>软装搭配+效果图渲染</p>
</div>
<div class="allocation-values">
<span class="percentage">40%</span>
<span class="amount">¥{{ quotation.allocation.decoration.amount }}</span>
</div>
</div>
<!-- 公司分配 -->
<div class="allocation-card company">
<div class="allocation-icon">
<svg>...</svg>
</div>
<div class="allocation-info">
<h5>公司分配</h5>
<p>公司运营与利润</p>
</div>
<div class="allocation-values">
<span class="percentage">50%</span>
<span class="amount">¥{{ quotation.allocation.company.amount }}</span>
</div>
</div>
</div>
<div class="allocation-note">
<i class="icon-info"></i>
<span>内部执行分配为系统自动计算,基于报价总额按固定比例拆分,所有金额均为整数</span>
</div>
}
</div>
<!-- 总价 -->
<div class="total-section">
<div class="total-row">
<span class="label">报价总额</span>
<span class="amount">¥{{ quotation.total }}</span>
</div>
<div class="total-meta">
<span>生成于 {{ quotation.generatedAt | date:'yyyy-MM-dd HH:mm' }}</span>
<span>有效期至 {{ quotation.validUntil | date:'yyyy-MM-dd' }}</span>
</div>
</div>
</div>
@media (max-width: 768px) {
// 产品卡片
.product-card {
margin-bottom: 16px;
.product-header {
flex-direction: column;
align-items: flex-start;
padding: 16px;
.product-pricing {
width: 100%;
margin-top: 12px;
display: flex;
justify-content: space-between;
}
}
}
// 工序网格
.process-grid {
grid-template-columns: 1fr;
}
// 分配卡片
.allocation-card {
flex-direction: column;
align-items: flex-start;
.allocation-values {
width: 100%;
flex-direction: row;
justify-content: space-between;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(0,0,0,0.05);
}
}
// 添加产品模态框
.add-product-modal {
width: 100%;
max-height: 90vh;
border-radius: 16px 16px 0 0;
.scene-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
/**
* 计算空间基础价格
* 严格按照 docs/data/quotation.md 规则
*/
private calculateBasePrice(config: ProductConfig): number {
const {
priceLevel,
projectType,
renderType,
spaceType,
styleLevel,
businessType,
architectureType
} = config;
// 使用quotation-rules.ts中的getBasePrice函数
const basePrice = getBasePrice(
priceLevel,
projectType,
renderType,
spaceType,
styleLevel,
businessType,
architectureType
);
// 确保返回整数
return Math.round(basePrice);
}
/**
* 确保所有价格计算结果都是整数
*/
private ensureIntegerPrices(quotation: any) {
// 工序价格整数化
for (const space of quotation.spaces) {
for (const processKey of Object.keys(space.processes)) {
const process = space.processes[processKey];
process.price = Math.round(process.price);
process.quantity = Math.round(process.quantity);
}
}
// 总价整数化
quotation.total = Math.round(quotation.total);
// 分配金额整数化
if (quotation.allocation) {
quotation.allocation.modeling.amount = Math.round(quotation.allocation.modeling.amount);
quotation.allocation.decoration.amount = Math.round(quotation.allocation.decoration.amount);
quotation.allocation.company.amount = Math.round(quotation.allocation.company.amount);
}
}
test('家装平层基础风格组一级报价应为300元', () => {
const price = calculateBasePrice({
priceLevel: '一级',
projectType: '家装',
renderType: '静态单张',
spaceType: '平层',
styleLevel: '基础风格组'
});
expect(price).toBe(300);
});
test('工装办公门厅三级报价应为900元', () => {
const price = calculateBasePrice({
priceLevel: '三级',
projectType: '工装',
renderType: '静态单张',
spaceType: '门厅空间',
businessType: '办公空间'
});
expect(price).toBe(900);
});
test('900元报价应分配为90-360-450', () => {
const allocation = calculateAllocation(900);
expect(allocation.modeling.amount).toBe(90);
expect(allocation.decoration.amount).toBe(360);
expect(allocation.company.amount).toBe(450);
});
test('分配金额应全部为整数', () => {
const allocation = calculateAllocation(12345);
expect(allocation.modeling.amount % 1).toBe(0);
expect(allocation.decoration.amount % 1).toBe(0);
expect(allocation.company.amount % 1).toBe(0);
});
A: 按照业务规则,每个空间(客厅、卧室、厨房等)都是一个独立的设计图服务,有独立的报价、工序和分配。这样可以精准控制每个空间的成本和利润。
A: 所有涉及价格计算的地方都使用 Math.round() 进行四舍五入,包括基础价格、工序价格、总价、分配金额等。
A: 可以。预设场景列表定义在组件配置中,管理员可以根据实际业务需求调整。如果用户选择"自定义",可以手动输入空间名称。
A: 报价规则定义在 quotation-rules.ts 配置文件和 docs/data/quotation.md 文档中。更新时需同步修改两处,确保代码和文档一致。
文档版本: v2.0 创建日期: 2025-10-25 更新日期: 2025-10-25 作者: Claude Code AI 审核状态: 待审核