| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428 |
- import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
- import { FmodeParse } from 'fmode-ng/parse';
- import { Subscription } from 'rxjs';
- import {
- calculateAllocation,
- calculateProductAllocation,
- ALLOCATION_RULES,
- getBasePrice,
- STYLE_LEVELS,
- SPACE_TYPES,
- BUSINESS_TYPES,
- HOME_DEFAULT_ROOMS,
- ARCHITECTURE_TYPES,
- type Allocation
- } from '../config/quotation-rules';
- const Parse = FmodeParse.with('nova');
- /**
- * 基于Product表的报价编辑器组件(重构版)
- *
- * 核心改进:
- * 1. 严格按照docs/data/quotation.md规则计算基础价格
- * 2. 每个空间都是独立的设计产品服务
- * 3. 添加产品支持预设场景选择
- * 4. 所有价格均为整数(无小数)
- * 5. 自动计算内部执行分配(10%-40%-50%)
- */
- @Component({
- selector: 'app-quotation-editor',
- standalone: true,
- imports: [CommonModule, FormsModule],
- templateUrl: './quotation-editor.component.html',
- styleUrls: ['./quotation-editor.component.scss']
- })
- export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
- // 输入属性
- @Input() projectId: string = '';
- @Input() project: any = null;
- @Input() canEdit: boolean = false;
- @Input() viewMode: 'table' | 'card' = 'card';
- @Input() currentUser: any = null;
- // 输出事件
- @Output() quotationChange = new EventEmitter<any>();
- @Output() totalChange = new EventEmitter<number>();
- @Output() loadingChange = new EventEmitter<boolean>();
- @Output() productsChange = new EventEmitter<any[]>();
- // 数据状态
- loading: boolean = false;
- products: any[] = [];
- projectInfo: any = {
- title: '',
- projectType: '家装', // 家装 | 工装 | 建筑类
- renderType: '静态单张', // 静态单张 | 360全景
- deadline: '',
- description: '',
- priceLevel: '一级', // 一级(老客户) | 二级(中端组) | 三级(高端组)
- };
- // 报价数据结构
- quotation: any = {
- spaces: [],
- total: 0,
- spaceBreakdown: [],
- allocation: null as Allocation | null,
- generatedAt: null,
- validUntil: null
- };
- // 分配类型定义(3个分配:建模阶段10%、软装渲染40%、公司分配50%)
- allocationTypes = [
- { key: 'modeling', name: '建模阶段', percentage: 10, color: 'primary', icon: 'cube-outline', description: '3D模型构建' },
- { key: 'decoration', name: '软装渲染', percentage: 40, color: 'warning', icon: 'color-palette-outline', description: '软装搭配+效果图渲染' },
- { key: 'company', name: '公司分配', percentage: 50, color: 'success', icon: 'business-outline', description: '公司运营与利润' }
- ];
- // 折叠状态
- expandedProducts: Set<string> = new Set();
- // UI状态
- showBreakdown: boolean = false;
- showAllocation: boolean = false;
- // 分配规则配置
- allocationRules = ALLOCATION_RULES;
- // 报价配置(从quotation-rules导入)
- styleLevels = STYLE_LEVELS;
- spaceTypes = SPACE_TYPES;
- businessTypes = BUSINESS_TYPES;
- architectureTypes = ARCHITECTURE_TYPES;
- homeDefaultRooms = HOME_DEFAULT_ROOMS;
- // 订阅管理
- private subscriptions: Subscription[] = [];
- // 预设场景列表
- presetScenes: { [key: string]: string[] } = {
- '家装': ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'],
- '工装': ['大堂', '接待区', '会议室', '办公区', '休息区', '展示区', '洽谈区'],
- '建筑类': ['门头', '小型单体', '大型单体', '鸟瞰']
- };
- // 产品添加/编辑模态框状态
- showAddProductModal: boolean = false;
- isEditMode: boolean = false; // 是否为编辑模式
- editingProductId: string = ''; // 正在编辑的产品ID
- newProduct: any = {
- isCustom: false,
- sceneName: '',
- productName: '',
- spaceType: '平层',
- styleLevel: '基础风格组',
- businessType: '办公空间',
- architectureType: '门头',
- adjustments: {
- extraFunction: 0,
- complexity: 0,
- design: false,
- panoramic: false
- }
- };
- ngOnInit() {
- if (this.project) {
- this.loadProjectDataFromProject();
- } else if (this.projectId) {
- this.loadProjectData();
- }
- }
- ngOnChanges(changes: SimpleChanges) {
- if (changes['project'] && changes['project'].currentValue) {
- this.loadProjectDataFromProject();
- } else if (changes['projectId'] && changes['projectId'].currentValue) {
- this.loadProjectData();
- }
- if (changes['quotation'] && this.quotation?.spaces?.length > 0) {
- if (this.expandedProducts.size === 0) {
- this.expandedProducts.add(this.quotation.spaces[0].name);
- }
- }
- }
- ngOnDestroy() {
- this.subscriptions.forEach(sub => sub.unsubscribe());
- }
- /**
- * 加载项目数据
- */
- private async loadProjectData(): Promise<void> {
- if (!this.projectId) return;
- try {
- this.loading = true;
- this.loadingChange.emit(true);
- const projectQuery = new Parse.Query('Project');
- projectQuery.include('customer', 'assignee', 'department');
- this.project = await projectQuery.get(this.projectId);
- if (this.project) {
- 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.description = this.project.get('description') || '';
- const data = this.project.get('data') || {};
- if (data.priceLevel) {
- this.projectInfo.priceLevel = data.priceLevel;
- }
- await this.loadProjectProducts();
- if (data.quotation) {
- this.quotation = data.quotation;
- this.updateProductsFromQuotation();
- }
- }
- } catch (error) {
- console.error('加载项目数据失败:', error);
- } finally {
- this.loading = false;
- this.loadingChange.emit(false);
- }
- }
- /**
- * 从传入的项目对象初始化数据
- */
- private async loadProjectDataFromProject(): Promise<void> {
- if (!this.project) return;
- try {
- this.loading = true;
- this.loadingChange.emit(true);
- 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.description = this.project.get('description') || '';
- const data = this.project.get('data') || {};
- if (data.priceLevel) {
- this.projectInfo.priceLevel = data.priceLevel;
- }
- await this.loadProjectProducts();
- if (data.quotation) {
- this.quotation = data.quotation;
- this.updateProductsFromQuotation();
- }
- } catch (error) {
- console.error('从项目对象加载数据失败:', error);
- } finally {
- this.loading = false;
- this.loadingChange.emit(false);
- }
- }
- /**
- * 加载项目产品列表
- */
- private async loadProjectProducts(): Promise<void> {
- if (!this.project) return;
- try {
- const productQuery = new Parse.Query('Product');
- productQuery.equalTo('project', this.project.toPointer());
- productQuery.include('profile');
- productQuery.ascending('productName');
- this.products = await productQuery.find();
- this.productsChange.emit(this.products);
- if (this.products.length === 0) {
- await this.createDefaultProducts();
- }
- } catch (error) {
- console.error('加载产品列表失败:', error);
- }
- }
- /**
- * 创建默认产品
- */
- private async createDefaultProducts(): Promise<void> {
- if (!this.project || !this.projectInfo.projectType) return;
- try {
- const defaultRooms = this.getDefaultRoomsForProjectType();
- for (const roomName of defaultRooms) {
- await this.createProduct(roomName);
- }
- await this.loadProjectProducts();
- } catch (error) {
- console.error('创建默认产品失败:', error);
- }
- }
- /**
- * 根据项目类型获取默认房间
- */
- private getDefaultRoomsForProjectType(): string[] {
- return this.presetScenes[this.projectInfo.projectType] || ['空间1'];
- }
- /**
- * 创建产品(重构版)
- * 支持传入完整配置,根据报价规则自动计算基础价格
- */
- private async createProduct(
- productName: string,
- config?: {
- spaceType?: string;
- styleLevel?: string;
- businessType?: string;
- architectureType?: string;
- }
- ): Promise<any> {
- if (!this.project) return null;
- try {
- const product = new Parse.Object('Product');
- product.set('project', this.project.toPointer());
- product.set('productName', productName);
- product.set('productType', this.inferProductType(productName));
- // 确定配置
- const spaceType = config?.spaceType || this.getDefaultSpaceType();
- const styleLevel = config?.styleLevel || '基础风格组';
- const businessType = config?.businessType || '办公空间';
- const architectureType = config?.architectureType;
- // 设置空间信息
- product.set('space', {
- spaceName: productName,
- area: 0,
- dimensions: { length: 0, width: 0, height: 0 },
- spaceType: spaceType,
- styleLevel: styleLevel,
- businessType: businessType,
- architectureType: architectureType,
- features: [],
- constraints: [],
- priority: 5,
- complexity: 'medium'
- });
- // 计算基础价格(使用重构后的方法)
- const basePrice = this.calculateBasePrice({
- priceLevel: this.projectInfo.priceLevel,
- projectType: this.projectInfo.projectType,
- renderType: this.projectInfo.renderType,
- spaceType: spaceType,
- styleLevel: styleLevel,
- businessType: businessType,
- architectureType: architectureType
- });
- // 设置报价信息
- product.set('quotation', {
- priceLevel: this.projectInfo.priceLevel,
- basePrice: Math.round(basePrice),
- price: Math.round(basePrice),
- currency: 'CNY',
- breakdown: this.calculatePriceBreakdown(basePrice),
- status: 'draft',
- validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
- });
- // 设置需求信息
- product.set('requirements', {
- colorRequirement: {},
- materialRequirement: {},
- lightingRequirement: {},
- specificRequirements: [],
- constraints: {}
- });
- product.set('status', 'not_started');
- await product.save();
- return product;
- } catch (error) {
- console.error('创建产品失败:', error);
- return null;
- }
- }
- /**
- * 计算基础价格(重构版)
- * 严格按照 docs/data/quotation.md 规则
- */
- private calculateBasePrice(config: {
- priceLevel: string;
- projectType: string;
- renderType: string;
- spaceType: string;
- styleLevel?: string;
- businessType?: string;
- architectureType?: string;
- }): number {
- const {
- priceLevel,
- projectType,
- renderType,
- spaceType,
- styleLevel,
- businessType,
- architectureType
- } = config;
- // 使用quotation-rules.ts中的getBasePrice函数
- const basePrice = getBasePrice(
- priceLevel,
- projectType as '家装' | '工装' | '建筑类',
- renderType,
- spaceType,
- styleLevel,
- businessType,
- architectureType
- );
- // 确保返回整数
- return Math.round(basePrice);
- }
- /**
- * 获取默认空间类型
- */
- private getDefaultSpaceType(): string {
- if (this.projectInfo.projectType === '家装') {
- return '平层';
- } else if (this.projectInfo.projectType === '工装') {
- return '门厅空间';
- } else if (this.projectInfo.projectType === '建筑类') {
- return '门头';
- }
- return '平层';
- }
- /**
- * 推断产品类型
- */
- private inferProductType(roomName: string): string {
- const name = roomName.toLowerCase();
- if (name.includes('客厅') || name.includes('起居')) return 'living_room';
- if (name.includes('卧室') || name.includes('主卧') || name.includes('次卧')) return 'bedroom';
- if (name.includes('厨房')) return 'kitchen';
- if (name.includes('卫生间') || name.includes('浴室')) return 'bathroom';
- if (name.includes('餐厅')) return 'dining_room';
- if (name.includes('书房') || name.includes('工作室')) return 'study';
- if (name.includes('阳台')) return 'balcony';
- if (name.includes('玄关') || name.includes('走廊')) return 'corridor';
- return 'other';
- }
- /**
- * 计算价格明细
- */
- private calculatePriceBreakdown(basePrice: number): any {
- // 确保所有价格都是整数
- return {
- design: Math.round(basePrice * 0.3),
- modeling: Math.round(basePrice * 0.25),
- rendering: Math.round(basePrice * 0.25),
- softDecor: Math.round(basePrice * 0.15),
- postProcess: Math.round(basePrice * 0.05)
- };
- }
- /**
- * 从报价数据更新产品
- */
- private updateProductsFromQuotation(): void {
- if (!this.quotation.spaces || !this.products.length) return;
- this.quotation.spaces.forEach((space: any) => {
- const product = this.products.find(p =>
- p.get('productName') === space.name ||
- p.get('productName').includes(space.name)
- );
- if (product) {
- const quotation = product.get('quotation') || {};
- quotation.price = Math.round(space.subtotal || 0);
- quotation.processes = space.processes;
- product.set('quotation', quotation);
- }
- });
- }
- // ============ 报价管理核心方法 ============
- /**
- * 生成基于产品的报价
- */
- async generateQuotationFromProducts(): Promise<void> {
- if (!this.products.length) return;
- this.quotation.spaces = [];
- this.quotation.generatedAt = new Date();
- this.quotation.validUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
- for (const product of this.products) {
- const productName = product.get('productName');
- const quotation = product.get('quotation') || {};
- const basePrice = quotation.price || this.calculateBasePrice({
- priceLevel: this.projectInfo.priceLevel,
- projectType: this.projectInfo.projectType,
- renderType: this.projectInfo.renderType,
- spaceType: product.get('space')?.spaceType || this.getDefaultSpaceType(),
- styleLevel: product.get('space')?.styleLevel,
- businessType: product.get('space')?.businessType,
- architectureType: product.get('space')?.architectureType
- });
- // 生成工序明细(确保价格为整数)
- const processes = this.generateDefaultProcesses(Math.round(basePrice));
- const spaceData = {
- name: productName,
- productId: product.id,
- processes: processes,
- subtotal: this.calculateProductSubtotal(processes)
- };
- this.quotation.spaces.push(spaceData);
- }
- this.calculateTotal();
- this.updateProductBreakdown();
- await this.saveQuotationToProject();
- }
- /**
- * 生成默认分配(所有价格为整数)
- * 基于设计图总价按比例分配:建模阶段10%、软装渲染40%、公司分配50%
- */
- private generateDefaultProcesses(basePrice: number): any {
- return {
- modeling: {
- enabled: true,
- amount: Math.round(basePrice * 0.1),
- percentage: 10,
- description: '3D模型构建'
- },
- decoration: {
- enabled: true,
- amount: Math.round(basePrice * 0.4),
- percentage: 40,
- description: '软装搭配+效果图渲染'
- },
- company: {
- enabled: true,
- amount: Math.round(basePrice * 0.5),
- percentage: 50,
- description: '公司运营与利润'
- }
- };
- }
- /**
- * 计算产品小计(确保整数)
- * 现在processes实际是allocations(分配)
- */
- private calculateProductSubtotal(processes: any): number {
- let subtotal = 0;
- for (const allocation of Object.values(processes)) {
- const alloc = allocation as any;
- if (alloc.enabled) {
- subtotal += Math.round(alloc.amount);
- }
- }
- return Math.round(subtotal);
- }
- /**
- * 更新产品占比明细
- */
- private updateProductBreakdown(): void {
- this.quotation.spaceBreakdown = this.quotation.spaces.map((space: any) => ({
- spaceName: space.name,
- spaceId: space.productId || '',
- amount: Math.round(space.subtotal),
- percentage: this.quotation.total > 0 ? Math.round((space.subtotal / this.quotation.total) * 100) : 0
- }));
- }
- /**
- * 保存报价到项目
- */
- private async saveQuotationToProject(): Promise<void> {
- if (!this.project) return;
- try {
- const data = this.project.get('data') || {};
- data.quotation = this.quotation;
- this.project.set('data', data);
- await this.project.save();
- this.quotationChange.emit(this.quotation);
- } catch (error) {
- console.error('保存报价失败:', error);
- }
- }
- // ============ UI交互方法 ============
- /**
- * 展开所有产品
- */
- expandAll() {
- this.quotation.spaces.forEach((space: any) => {
- this.expandedProducts.add(space.name);
- });
- }
- /**
- * 折叠所有产品
- */
- collapseAll() {
- this.expandedProducts.clear();
- }
- /**
- * 切换分配启用状态
- */
- toggleProcess(space: any, processKey: string) {
- const allocation = space.processes[processKey];
- allocation.enabled = !allocation.enabled;
- if (!allocation.enabled) {
- allocation.amount = 0;
- }
- this.calculateTotal();
- }
- /**
- * 工序价格或数量变化
- */
- onProcessChange() {
- this.calculateTotal();
- }
- /**
- * 计算报价总额(确保所有价格为整数)
- */
- calculateTotal() {
- let total = 0;
- for (const space of this.quotation.spaces) {
- for (const processKey of Object.keys(space.processes)) {
- const allocation = space.processes[processKey];
- if (allocation.enabled) {
- // 确保分配金额是整数
- const amount = Math.round(allocation.amount);
- total += amount;
- }
- }
- }
- this.quotation.total = Math.round(total);
- // 自动计算项目级别的内部执行分配(确保分配金额为整数)
- this.quotation.allocation = calculateAllocation(this.quotation.total);
- // 更新产品级别的分配
- this.updateProductsAllocation();
- this.quotationChange.emit(this.quotation);
- this.totalChange.emit(this.quotation.total);
- }
- /**
- * 更新所有产品的内部分配(确保整数)
- */
- private updateProductsAllocation() {
- for (const space of this.quotation.spaces) {
- const product = this.products.find(p => p.id === space.productId);
- if (product) {
- const productPrice = Math.round(this.calculateSpaceSubtotal(space));
- const quotation = product.get('quotation') || {};
- quotation.allocation = calculateProductAllocation(productPrice);
- product.set('quotation', quotation);
- }
- }
- }
- /**
- * 计算空间小计
- */
- calculateSpaceSubtotal(space: any): number {
- return Math.round(this.calculateProductSubtotal(space.processes));
- }
- /**
- * 计算分配金额
- */
- calculateProcessSubtotal(space: any, processKey: string): number {
- const allocation = space.processes?.[processKey];
- const amount = Math.round(allocation?.amount || 0);
- return amount;
- }
- // ============ 辅助方法 ============
- isProcessEnabled(space: any, processKey: string): boolean {
- const allocation = space.processes?.[processKey];
- return allocation?.enabled || false;
- }
- setAllocationAmount(space: any, allocationKey: string, value: any): void {
- const allocation = space.processes?.[allocationKey];
- if (allocation) {
- allocation.amount = Math.round(Number(value) || 0);
- }
- }
- getAllocationAmount(space: any, allocationKey: string): number {
- const allocation = space.processes?.[allocationKey];
- return Math.round(allocation?.amount || 0);
- }
- getAllocationPercentage(space: any, allocationKey: string): number {
- const allocation = space.processes?.[allocationKey];
- return allocation?.percentage || 0;
- }
- getAllocationDescription(space: any, allocationKey: string): string {
- const allocation = space.processes?.[allocationKey];
- return allocation?.description || '';
- }
- // ============ 产品管理方法 ============
- /**
- * 添加新产品(简化版,用于快速添加)
- */
- async addProduct(productName?: string): Promise<void> {
- if (!this.project) return;
- const name = productName ||await window?.fmode?.input('请输入产品名称:');
- if (!name) return;
- try {
- await this.createProduct(name);
- await this.loadProjectProducts();
- await this.generateQuotationFromProducts();
- } catch (error) {
- console.error('添加产品失败:', error);
- window?.fmode?.alert('添加失败,请重试');
- }
- }
- /**
- * 编辑产品名称
- */
- async editProduct(productId: string): Promise<void> {
- const product = this.products.find(p => p.id === productId);
- if (!product) return;
- const newName =await window?.fmode?.input('修改产品名称:', product.get('productName'));
- if (!newName || newName === product.get('productName')) return;
- try {
- product.set('productName', newName);
- product.set('productType', this.inferProductType(newName));
- await product.save();
- const spaceData = this.quotation.spaces.find((s: any) => s.productId === productId);
- if (spaceData) {
- spaceData.name = newName;
- }
- await this.saveQuotationToProject();
- await this.loadProjectProducts();
- } catch (error) {
- console.error('更新产品失败:', error);
- window?.fmode?.alert('更新失败,请重试');
- }
- }
- /**
- * 删除产品
- */
- async deleteProduct(productId: string): Promise<void> {
- if (!await window?.fmode?.confirm('确定要删除这个产品吗?相关数据将被清除。')) return;
- try {
- const product = this.products.find(p => p.id === productId);
- if (product) {
- await product.destroy();
- }
- // 更新本地产品列表(不重新加载,避免触发createDefaultProducts)
- this.products = this.products.filter(p => p.id !== productId);
- this.productsChange.emit(this.products);
- // 更新报价spaces
- this.quotation.spaces = this.quotation.spaces.filter((s: any) => s.productId !== productId);
- // 重新计算总价
- this.calculateTotal();
- this.updateProductBreakdown();
- // 保存报价
- await this.saveQuotationToProject();
- } catch (error) {
- console.error('删除产品失败:', error);
- window?.fmode?.alert('删除失败,请重试');
- }
- }
- /**
- * 保存报价
- */
- async saveQuotation(): Promise<void> {
- if (!this.canEdit) return;
- try {
- await this.saveQuotationToProject();
- window?.fmode?.alert('保存成功');
- } catch (error) {
- console.error('保存失败:', error);
- window?.fmode?.alert('保存失败');
- }
- }
- /**
- * 切换内部执行分配显示
- */
- toggleAllocation() {
- this.showAllocation = !this.showAllocation;
- }
- toggleProductExpand(spaceName: string): void {
- if (this.expandedProducts.has(spaceName)) {
- this.expandedProducts.delete(spaceName);
- } else {
- this.expandedProducts.add(spaceName);
- }
- }
- isProductExpanded(spaceName: string): boolean {
- return this.expandedProducts.has(spaceName);
- }
- // ============ 辅助显示方法 ============
- getProductDesigner(product: any): string {
- const profile = product.get('profile');
- return profile ? profile.get('name') : '未分配';
- }
- getProductStatus(product: any): string {
- return product.get('status') || 'not_started';
- }
- getProductStatusColor(status: string): string {
- const colorMap: Record<string, string> = {
- 'not_started': 'medium',
- 'in_progress': 'warning',
- 'awaiting_review': 'info',
- 'completed': 'success',
- 'blocked': 'danger',
- 'delayed': 'danger'
- };
- return colorMap[status] || 'medium';
- }
- getProductStatusText(status: string): string {
- const textMap: Record<string, string> = {
- 'not_started': '未开始',
- 'in_progress': '进行中',
- 'awaiting_review': '待审核',
- 'completed': '已完成',
- 'blocked': '已阻塞',
- 'delayed': '已延期'
- };
- return textMap[status] || status;
- }
- getProductIcon(productType: string): string {
- const iconMap: Record<string, string> = {
- 'living_room': 'living-room',
- 'bedroom': 'bedroom',
- 'kitchen': 'kitchen',
- 'bathroom': 'bathroom',
- 'dining_room': 'dining-room',
- 'study': 'study',
- 'balcony': 'balcony',
- 'corridor': 'corridor',
- 'storage': 'storage',
- 'entrance': 'entrance',
- 'other': 'room'
- };
- return iconMap[productType] || 'room';
- }
- formatPrice(price: number): string {
- // 确保价格为整数
- return `¥${Math.round(price)}`;
- }
- forSpacePrice(space,allocationType){
- let price = Math.round((this.getProductForSpace(space.productId)?.get('quotation')?.basePrice || 0) * allocationType.percentage / 100)
- this.formatPercentage(price)
- }
- formatPercentage(value: number): string {
- return `${Math.round(value)}%`;
- }
- getProductTypeForSpace(spaceName: string): string {
- return this.inferProductType(spaceName);
- }
- getProductIconForSpace(spaceName: string): string {
- const productType = this.getProductTypeForSpace(spaceName);
- return this.getProductIcon(productType);
- }
- getStatusColorForSpace(spaceId: string): string {
- const product = this.products.find(p => p.id === spaceId);
- if (product) {
- return this.getProductStatusColor(product.get('status'));
- }
- return 'medium';
- }
- getStatusTextForSpace(spaceId: string): string {
- const product = this.products.find(p => p.id === spaceId);
- if (product) {
- return this.getProductStatusText(product.get('status'));
- }
- return '未开始';
- }
- getDesignerNameForSpace(spaceId: string): string {
- const product = this.products.find(p => p.id === spaceId);
- if (product) {
- return this.getProductDesigner(product);
- }
- return '未分配';
- }
- getProductForSpace(productId: string): any {
- return this.products.find(p => p.id === productId) || null;
- }
- getSpacePercentage(spaceId: string): number {
- if (!this.quotation.spaceBreakdown) return 0;
- const breakdown = this.quotation.spaceBreakdown.find((b: any) => b.spaceId === spaceId);
- return Math.round(breakdown?.percentage || 0);
- }
- // ============ 产品添加模态框方法 ============
- /**
- * 打开添加产品模态框
- */
- openAddProductModal(): void {
- // 重置表单
- this.resetNewProductForm();
- this.isEditMode = false;
- this.editingProductId = '';
- this.showAddProductModal = true;
- }
- /**
- * 打开编辑产品模态框
- */
- openEditProductModal(productId: string): void {
- const product = this.products.find(p => p.id === productId);
- if (!product) return;
- // 预填充产品数据
- const space = product.get('space') || {};
- const quotation = product.get('quotation') || {};
- const adjustments = quotation.adjustments || {};
- // 判断是否为自定义名称
- const presetScenes = this.getPresetScenes();
- const isPreset = presetScenes.includes(product.get('productName'));
- this.newProduct = {
- isCustom: !isPreset,
- sceneName: isPreset ? product.get('productName') : '',
- productName: product.get('productName'),
- spaceType: space.spaceType || this.getDefaultSpaceType(),
- styleLevel: space.styleLevel || '基础风格组',
- businessType: space.businessType || '办公空间',
- architectureType: space.architectureType || '门头',
- adjustments: {
- extraFunction: adjustments.extraFunction || 0,
- complexity: adjustments.complexity || 0,
- design: adjustments.design || false,
- panoramic: adjustments.panoramic || false
- }
- };
- this.isEditMode = true;
- this.editingProductId = productId;
- this.showAddProductModal = true;
- }
- /**
- * 关闭添加/编辑产品模态框
- */
- closeAddProductModal(): void {
- this.showAddProductModal = false;
- this.isEditMode = false;
- this.editingProductId = '';
- this.resetNewProductForm();
- }
- /**
- * 重置新产品表单
- */
- private resetNewProductForm(): void {
- this.newProduct = {
- isCustom: false,
- sceneName: '',
- productName: '',
- spaceType: this.getDefaultSpaceType(),
- styleLevel: '基础风格组',
- businessType: '办公空间',
- architectureType: '门头',
- adjustments: {
- extraFunction: 0,
- complexity: 0,
- design: false,
- panoramic: false
- }
- };
- }
- /**
- * 获取预设场景列表
- */
- getPresetScenes(): string[] {
- return this.presetScenes[this.projectInfo.projectType] || [];
- }
- /**
- * 选择预设场景
- */
- selectScene(scene: string): void {
- this.newProduct.isCustom = false;
- this.newProduct.sceneName = scene;
- this.newProduct.productName = scene;
- }
- /**
- * 选择自定义场景
- */
- selectCustomScene(): void {
- this.newProduct.isCustom = true;
- this.newProduct.sceneName = '';
- this.newProduct.productName = '';
- }
- /**
- * 计算预览基础价格
- */
- calculatePreviewBasePrice(): number {
- return this.calculateBasePrice({
- priceLevel: this.projectInfo.priceLevel,
- projectType: this.projectInfo.projectType,
- renderType: this.projectInfo.renderType,
- spaceType: this.newProduct.spaceType,
- styleLevel: this.newProduct.styleLevel,
- businessType: this.newProduct.businessType,
- architectureType: this.newProduct.architectureType
- });
- }
- /**
- * 计算预览加价总额
- */
- calculatePreviewAdjustmentTotal(): number {
- const basePrice = this.calculatePreviewBasePrice();
- const adjustments = this.newProduct.adjustments;
- let total = 0;
- // 功能区加价
- if (adjustments.extraFunction > 0) {
- if (this.projectInfo.projectType === '家装') {
- total += adjustments.extraFunction * 100;
- } else if (this.projectInfo.projectType === '工装') {
- total += adjustments.extraFunction * 400;
- }
- }
- // 造型复杂度加价
- if (adjustments.complexity > 0) {
- total += adjustments.complexity;
- }
- // 设计服务倍增
- let multiplier = 1;
- if (adjustments.design) {
- multiplier *= 2;
- }
- // 全景渲染倍增(仅工装)
- if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
- multiplier *= 2;
- }
- // 如果有倍增,计算额外的加价
- if (multiplier > 1) {
- total = (basePrice + total) * multiplier - basePrice;
- }
- return Math.round(total);
- }
- /**
- * 计算预览最终价格
- */
- calculatePreviewFinalPrice(): number {
- const basePrice = this.calculatePreviewBasePrice();
- const adjustments = this.newProduct.adjustments;
- let price = basePrice;
- // 功能区加价
- if (adjustments.extraFunction > 0) {
- if (this.projectInfo.projectType === '家装') {
- price += adjustments.extraFunction * 100;
- } else if (this.projectInfo.projectType === '工装') {
- price += adjustments.extraFunction * 400;
- }
- }
- // 造型复杂度加价
- if (adjustments.complexity > 0) {
- price += adjustments.complexity;
- }
- // 设计服务倍增
- if (adjustments.design) {
- price *= 2;
- }
- // 全景渲染倍增(仅工装)
- if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
- price *= 2;
- }
- return Math.round(price);
- }
- /**
- * 验证新产品表单是否有效
- */
- isNewProductValid(): boolean {
- if (this.newProduct.isCustom) {
- return this.newProduct.productName.trim().length > 0;
- }
- return this.newProduct.sceneName.length > 0;
- }
- /**
- * 确认添加/编辑产品
- */
- async confirmAddProduct(): Promise<void> {
- if (!this.isNewProductValid()) {
- window?.fmode?.alert('请完整填写产品信息');
- return;
- }
- try {
- if (this.isEditMode) {
- await this.updateExistingProduct();
- } else {
- await this.createNewProduct();
- }
- } catch (error) {
- console.error(this.isEditMode ? '编辑产品失败:' : '添加产品失败:', error);
- window?.fmode?.alert(this.isEditMode ? '编辑产品失败,请重试' : '添加产品失败,请重试');
- }
- }
- /**
- * 创建新产品
- */
- private async createNewProduct(): Promise<void> {
- const productName = this.newProduct.isCustom
- ? this.newProduct.productName
- : this.newProduct.sceneName;
- const config: any = {
- spaceType: this.newProduct.spaceType
- };
- if (this.projectInfo.projectType === '家装') {
- config.styleLevel = this.newProduct.styleLevel;
- } else if (this.projectInfo.projectType === '工装') {
- config.businessType = this.newProduct.businessType;
- } else if (this.projectInfo.projectType === '建筑类') {
- config.architectureType = this.newProduct.architectureType;
- }
- // 创建产品
- const product = await this.createProductWithAdjustments(productName, config);
- if (product) {
- // 重新加载产品列表
- await this.loadProjectProducts();
- // 重新生成报价
- await this.generateQuotationFromProducts();
- // 关闭模态框
- this.closeAddProductModal();
- window?.fmode?.alert(`成功添加产品: ${productName}`);
- } else {
- throw new Error('创建产品失败');
- }
- }
- /**
- * 更新现有产品
- */
- private async updateExistingProduct(): Promise<void> {
- const product = this.products.find(p => p.id === this.editingProductId);
- if (!product) {
- throw new Error('找不到要编辑的产品');
- }
- const productName = this.newProduct.isCustom
- ? this.newProduct.productName
- : this.newProduct.sceneName;
- // 更新产品名称和类型
- product.set('productName', productName);
- product.set('productType', this.inferProductType(productName));
- // 确定配置
- const spaceType = this.newProduct.spaceType;
- const styleLevel = this.newProduct.styleLevel;
- const businessType = this.newProduct.businessType;
- const architectureType = this.newProduct.architectureType;
- // 更新空间信息
- const space = product.get('space') || {};
- space.spaceName = productName;
- space.spaceType = spaceType;
- space.styleLevel = styleLevel;
- space.businessType = businessType;
- space.architectureType = architectureType;
- product.set('space', space);
- // 重新计算基础价格
- let basePrice = this.calculateBasePrice({
- priceLevel: this.projectInfo.priceLevel,
- projectType: this.projectInfo.projectType,
- renderType: this.projectInfo.renderType,
- spaceType: spaceType,
- styleLevel: styleLevel,
- businessType: businessType,
- architectureType: architectureType
- });
- // 应用加价规则
- let finalPrice = basePrice;
- const adjustments = this.newProduct.adjustments;
- // 功能区加价
- if (adjustments.extraFunction > 0) {
- if (this.projectInfo.projectType === '家装') {
- finalPrice += adjustments.extraFunction * 100;
- } else if (this.projectInfo.projectType === '工装') {
- finalPrice += adjustments.extraFunction * 400;
- }
- }
- // 造型复杂度加价
- if (adjustments.complexity > 0) {
- finalPrice += adjustments.complexity;
- }
- // 设计服务倍增
- if (adjustments.design) {
- finalPrice *= 2;
- }
- // 全景渲染倍增(仅工装)
- if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
- finalPrice *= 2;
- }
- finalPrice = Math.round(finalPrice);
- // 更新报价信息
- const quotation = product.get('quotation') || {};
- quotation.basePrice = Math.round(basePrice);
- quotation.price = finalPrice;
- quotation.adjustments = {
- extraFunction: adjustments.extraFunction,
- complexity: adjustments.complexity,
- design: adjustments.design,
- panoramic: adjustments.panoramic
- };
- quotation.breakdown = this.calculatePriceBreakdown(finalPrice);
- product.set('quotation', quotation);
- // 保存产品
- await product.save();
- // 更新报价中的空间数据
- const spaceData = this.quotation.spaces.find((s: any) => s.productId === this.editingProductId);
- if (spaceData) {
- spaceData.name = productName;
- // 重新生成该空间的分配
- spaceData.processes = this.generateDefaultProcesses(finalPrice);
- }
- // 重新加载产品列表
- await this.loadProjectProducts();
- // 重新生成报价
- await this.generateQuotationFromProducts();
- // 关闭模态框
- this.closeAddProductModal();
- window?.fmode?.alert(`成功更新产品: ${productName}`);
- }
- /**
- * 创建产品(带加价规则)
- */
- private async createProductWithAdjustments(
- productName: string,
- config: {
- spaceType?: string;
- styleLevel?: string;
- businessType?: string;
- architectureType?: string;
- }
- ): Promise<any> {
- if (!this.project) return null;
- try {
- const product = new Parse.Object('Product');
- product.set('project', this.project.toPointer());
- product.set('productName', productName);
- product.set('productType', this.inferProductType(productName));
- // 确定配置
- const spaceType = config?.spaceType || this.getDefaultSpaceType();
- const styleLevel = config?.styleLevel || '基础风格组';
- const businessType = config?.businessType || '办公空间';
- const architectureType = config?.architectureType;
- // 设置空间信息
- product.set('space', {
- spaceName: productName,
- area: 0,
- dimensions: { length: 0, width: 0, height: 0 },
- spaceType: spaceType,
- styleLevel: styleLevel,
- businessType: businessType,
- architectureType: architectureType,
- features: [],
- constraints: [],
- priority: 5,
- complexity: 'medium'
- });
- // 计算基础价格
- let basePrice = this.calculateBasePrice({
- priceLevel: this.projectInfo.priceLevel,
- projectType: this.projectInfo.projectType,
- renderType: this.projectInfo.renderType,
- spaceType: spaceType,
- styleLevel: styleLevel,
- businessType: businessType,
- architectureType: architectureType
- });
- // 应用加价规则
- let finalPrice = basePrice;
- const adjustments = this.newProduct.adjustments;
- // 功能区加价
- if (adjustments.extraFunction > 0) {
- if (this.projectInfo.projectType === '家装') {
- finalPrice += adjustments.extraFunction * 100;
- } else if (this.projectInfo.projectType === '工装') {
- finalPrice += adjustments.extraFunction * 400;
- }
- }
- // 造型复杂度加价
- if (adjustments.complexity > 0) {
- finalPrice += adjustments.complexity;
- }
- // 设计服务倍增
- if (adjustments.design) {
- finalPrice *= 2;
- }
- // 全景渲染倍增(仅工装)
- if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
- finalPrice *= 2;
- }
- finalPrice = Math.round(finalPrice);
- // 设置报价信息
- product.set('quotation', {
- priceLevel: this.projectInfo.priceLevel,
- basePrice: Math.round(basePrice),
- price: finalPrice,
- currency: 'CNY',
- adjustments: {
- extraFunction: adjustments.extraFunction,
- complexity: adjustments.complexity,
- design: adjustments.design,
- panoramic: adjustments.panoramic
- },
- breakdown: this.calculatePriceBreakdown(finalPrice),
- status: 'draft',
- validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
- });
- // 设置需求信息
- product.set('requirements', {
- colorRequirement: {},
- materialRequirement: {},
- lightingRequirement: {},
- specificRequirements: [],
- constraints: {}
- });
- product.set('status', 'not_started');
- await product.save();
- return product;
- } catch (error) {
- console.error('创建产品失败:', error);
- return null;
- }
- }
- }
|