quotation-editor.component.ts 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428
  1. import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { FmodeParse } from 'fmode-ng/parse';
  5. import { Subscription } from 'rxjs';
  6. import {
  7. calculateAllocation,
  8. calculateProductAllocation,
  9. ALLOCATION_RULES,
  10. getBasePrice,
  11. STYLE_LEVELS,
  12. SPACE_TYPES,
  13. BUSINESS_TYPES,
  14. HOME_DEFAULT_ROOMS,
  15. ARCHITECTURE_TYPES,
  16. type Allocation
  17. } from '../config/quotation-rules';
  18. const Parse = FmodeParse.with('nova');
  19. /**
  20. * 基于Product表的报价编辑器组件(重构版)
  21. *
  22. * 核心改进:
  23. * 1. 严格按照docs/data/quotation.md规则计算基础价格
  24. * 2. 每个空间都是独立的设计产品服务
  25. * 3. 添加产品支持预设场景选择
  26. * 4. 所有价格均为整数(无小数)
  27. * 5. 自动计算内部执行分配(10%-40%-50%)
  28. */
  29. @Component({
  30. selector: 'app-quotation-editor',
  31. standalone: true,
  32. imports: [CommonModule, FormsModule],
  33. templateUrl: './quotation-editor.component.html',
  34. styleUrls: ['./quotation-editor.component.scss']
  35. })
  36. export class QuotationEditorComponent implements OnInit, OnChanges, OnDestroy {
  37. // 输入属性
  38. @Input() projectId: string = '';
  39. @Input() project: any = null;
  40. @Input() canEdit: boolean = false;
  41. @Input() viewMode: 'table' | 'card' = 'card';
  42. @Input() currentUser: any = null;
  43. // 输出事件
  44. @Output() quotationChange = new EventEmitter<any>();
  45. @Output() totalChange = new EventEmitter<number>();
  46. @Output() loadingChange = new EventEmitter<boolean>();
  47. @Output() productsChange = new EventEmitter<any[]>();
  48. // 数据状态
  49. loading: boolean = false;
  50. products: any[] = [];
  51. projectInfo: any = {
  52. title: '',
  53. projectType: '家装', // 家装 | 工装 | 建筑类
  54. renderType: '静态单张', // 静态单张 | 360全景
  55. deadline: '',
  56. description: '',
  57. priceLevel: '一级', // 一级(老客户) | 二级(中端组) | 三级(高端组)
  58. };
  59. // 报价数据结构
  60. quotation: any = {
  61. spaces: [],
  62. total: 0,
  63. spaceBreakdown: [],
  64. allocation: null as Allocation | null,
  65. generatedAt: null,
  66. validUntil: null
  67. };
  68. // 分配类型定义(3个分配:建模阶段10%、软装渲染40%、公司分配50%)
  69. allocationTypes = [
  70. { key: 'modeling', name: '建模阶段', percentage: 10, color: 'primary', icon: 'cube-outline', description: '3D模型构建' },
  71. { key: 'decoration', name: '软装渲染', percentage: 40, color: 'warning', icon: 'color-palette-outline', description: '软装搭配+效果图渲染' },
  72. { key: 'company', name: '公司分配', percentage: 50, color: 'success', icon: 'business-outline', description: '公司运营与利润' }
  73. ];
  74. // 折叠状态
  75. expandedProducts: Set<string> = new Set();
  76. // UI状态
  77. showBreakdown: boolean = false;
  78. showAllocation: boolean = false;
  79. // 分配规则配置
  80. allocationRules = ALLOCATION_RULES;
  81. // 报价配置(从quotation-rules导入)
  82. styleLevels = STYLE_LEVELS;
  83. spaceTypes = SPACE_TYPES;
  84. businessTypes = BUSINESS_TYPES;
  85. architectureTypes = ARCHITECTURE_TYPES;
  86. homeDefaultRooms = HOME_DEFAULT_ROOMS;
  87. // 订阅管理
  88. private subscriptions: Subscription[] = [];
  89. // 预设场景列表
  90. presetScenes: { [key: string]: string[] } = {
  91. '家装': ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'],
  92. '工装': ['大堂', '接待区', '会议室', '办公区', '休息区', '展示区', '洽谈区'],
  93. '建筑类': ['门头', '小型单体', '大型单体', '鸟瞰']
  94. };
  95. // 产品添加/编辑模态框状态
  96. showAddProductModal: boolean = false;
  97. isEditMode: boolean = false; // 是否为编辑模式
  98. editingProductId: string = ''; // 正在编辑的产品ID
  99. newProduct: any = {
  100. isCustom: false,
  101. sceneName: '',
  102. productName: '',
  103. spaceType: '平层',
  104. styleLevel: '基础风格组',
  105. businessType: '办公空间',
  106. architectureType: '门头',
  107. adjustments: {
  108. extraFunction: 0,
  109. complexity: 0,
  110. design: false,
  111. panoramic: false
  112. }
  113. };
  114. ngOnInit() {
  115. if (this.project) {
  116. this.loadProjectDataFromProject();
  117. } else if (this.projectId) {
  118. this.loadProjectData();
  119. }
  120. }
  121. ngOnChanges(changes: SimpleChanges) {
  122. if (changes['project'] && changes['project'].currentValue) {
  123. this.loadProjectDataFromProject();
  124. } else if (changes['projectId'] && changes['projectId'].currentValue) {
  125. this.loadProjectData();
  126. }
  127. if (changes['quotation'] && this.quotation?.spaces?.length > 0) {
  128. if (this.expandedProducts.size === 0) {
  129. this.expandedProducts.add(this.quotation.spaces[0].name);
  130. }
  131. }
  132. }
  133. ngOnDestroy() {
  134. this.subscriptions.forEach(sub => sub.unsubscribe());
  135. }
  136. /**
  137. * 加载项目数据
  138. */
  139. private async loadProjectData(): Promise<void> {
  140. if (!this.projectId) return;
  141. try {
  142. this.loading = true;
  143. this.loadingChange.emit(true);
  144. const projectQuery = new Parse.Query('Project');
  145. projectQuery.include('customer', 'assignee', 'department');
  146. this.project = await projectQuery.get(this.projectId);
  147. if (this.project) {
  148. this.projectInfo.title = this.project.get('title') || '';
  149. this.projectInfo.projectType = this.project.get('projectType') || '家装';
  150. this.projectInfo.renderType = this.project.get('renderType') || '静态单张';
  151. this.projectInfo.deadline = this.project.get('deadline') || '';
  152. this.projectInfo.description = this.project.get('description') || '';
  153. const data = this.project.get('data') || {};
  154. if (data.priceLevel) {
  155. this.projectInfo.priceLevel = data.priceLevel;
  156. }
  157. await this.loadProjectProducts();
  158. if (data.quotation) {
  159. this.quotation = data.quotation;
  160. this.updateProductsFromQuotation();
  161. }
  162. }
  163. } catch (error) {
  164. console.error('加载项目数据失败:', error);
  165. } finally {
  166. this.loading = false;
  167. this.loadingChange.emit(false);
  168. }
  169. }
  170. /**
  171. * 从传入的项目对象初始化数据
  172. */
  173. private async loadProjectDataFromProject(): Promise<void> {
  174. if (!this.project) return;
  175. try {
  176. this.loading = true;
  177. this.loadingChange.emit(true);
  178. this.projectInfo.title = this.project.get('title') || '';
  179. this.projectInfo.projectType = this.project.get('projectType') || '家装';
  180. this.projectInfo.renderType = this.project.get('renderType') || '静态单张';
  181. this.projectInfo.deadline = this.project.get('deadline') || '';
  182. this.projectInfo.description = this.project.get('description') || '';
  183. const data = this.project.get('data') || {};
  184. if (data.priceLevel) {
  185. this.projectInfo.priceLevel = data.priceLevel;
  186. }
  187. await this.loadProjectProducts();
  188. if (data.quotation) {
  189. this.quotation = data.quotation;
  190. this.updateProductsFromQuotation();
  191. }
  192. } catch (error) {
  193. console.error('从项目对象加载数据失败:', error);
  194. } finally {
  195. this.loading = false;
  196. this.loadingChange.emit(false);
  197. }
  198. }
  199. /**
  200. * 加载项目产品列表
  201. */
  202. private async loadProjectProducts(): Promise<void> {
  203. if (!this.project) return;
  204. try {
  205. const productQuery = new Parse.Query('Product');
  206. productQuery.equalTo('project', this.project.toPointer());
  207. productQuery.include('profile');
  208. productQuery.ascending('productName');
  209. this.products = await productQuery.find();
  210. this.productsChange.emit(this.products);
  211. if (this.products.length === 0) {
  212. await this.createDefaultProducts();
  213. }
  214. } catch (error) {
  215. console.error('加载产品列表失败:', error);
  216. }
  217. }
  218. /**
  219. * 创建默认产品
  220. */
  221. private async createDefaultProducts(): Promise<void> {
  222. if (!this.project || !this.projectInfo.projectType) return;
  223. try {
  224. const defaultRooms = this.getDefaultRoomsForProjectType();
  225. for (const roomName of defaultRooms) {
  226. await this.createProduct(roomName);
  227. }
  228. await this.loadProjectProducts();
  229. } catch (error) {
  230. console.error('创建默认产品失败:', error);
  231. }
  232. }
  233. /**
  234. * 根据项目类型获取默认房间
  235. */
  236. private getDefaultRoomsForProjectType(): string[] {
  237. return this.presetScenes[this.projectInfo.projectType] || ['空间1'];
  238. }
  239. /**
  240. * 创建产品(重构版)
  241. * 支持传入完整配置,根据报价规则自动计算基础价格
  242. */
  243. private async createProduct(
  244. productName: string,
  245. config?: {
  246. spaceType?: string;
  247. styleLevel?: string;
  248. businessType?: string;
  249. architectureType?: string;
  250. }
  251. ): Promise<any> {
  252. if (!this.project) return null;
  253. try {
  254. const product = new Parse.Object('Product');
  255. product.set('project', this.project.toPointer());
  256. product.set('productName', productName);
  257. product.set('productType', this.inferProductType(productName));
  258. // 确定配置
  259. const spaceType = config?.spaceType || this.getDefaultSpaceType();
  260. const styleLevel = config?.styleLevel || '基础风格组';
  261. const businessType = config?.businessType || '办公空间';
  262. const architectureType = config?.architectureType;
  263. // 设置空间信息
  264. product.set('space', {
  265. spaceName: productName,
  266. area: 0,
  267. dimensions: { length: 0, width: 0, height: 0 },
  268. spaceType: spaceType,
  269. styleLevel: styleLevel,
  270. businessType: businessType,
  271. architectureType: architectureType,
  272. features: [],
  273. constraints: [],
  274. priority: 5,
  275. complexity: 'medium'
  276. });
  277. // 计算基础价格(使用重构后的方法)
  278. const basePrice = this.calculateBasePrice({
  279. priceLevel: this.projectInfo.priceLevel,
  280. projectType: this.projectInfo.projectType,
  281. renderType: this.projectInfo.renderType,
  282. spaceType: spaceType,
  283. styleLevel: styleLevel,
  284. businessType: businessType,
  285. architectureType: architectureType
  286. });
  287. // 设置报价信息
  288. product.set('quotation', {
  289. priceLevel: this.projectInfo.priceLevel,
  290. basePrice: Math.round(basePrice),
  291. price: Math.round(basePrice),
  292. currency: 'CNY',
  293. breakdown: this.calculatePriceBreakdown(basePrice),
  294. status: 'draft',
  295. validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
  296. });
  297. // 设置需求信息
  298. product.set('requirements', {
  299. colorRequirement: {},
  300. materialRequirement: {},
  301. lightingRequirement: {},
  302. specificRequirements: [],
  303. constraints: {}
  304. });
  305. product.set('status', 'not_started');
  306. await product.save();
  307. return product;
  308. } catch (error) {
  309. console.error('创建产品失败:', error);
  310. return null;
  311. }
  312. }
  313. /**
  314. * 计算基础价格(重构版)
  315. * 严格按照 docs/data/quotation.md 规则
  316. */
  317. private calculateBasePrice(config: {
  318. priceLevel: string;
  319. projectType: string;
  320. renderType: string;
  321. spaceType: string;
  322. styleLevel?: string;
  323. businessType?: string;
  324. architectureType?: string;
  325. }): number {
  326. const {
  327. priceLevel,
  328. projectType,
  329. renderType,
  330. spaceType,
  331. styleLevel,
  332. businessType,
  333. architectureType
  334. } = config;
  335. // 使用quotation-rules.ts中的getBasePrice函数
  336. const basePrice = getBasePrice(
  337. priceLevel,
  338. projectType as '家装' | '工装' | '建筑类',
  339. renderType,
  340. spaceType,
  341. styleLevel,
  342. businessType,
  343. architectureType
  344. );
  345. // 确保返回整数
  346. return Math.round(basePrice);
  347. }
  348. /**
  349. * 获取默认空间类型
  350. */
  351. private getDefaultSpaceType(): string {
  352. if (this.projectInfo.projectType === '家装') {
  353. return '平层';
  354. } else if (this.projectInfo.projectType === '工装') {
  355. return '门厅空间';
  356. } else if (this.projectInfo.projectType === '建筑类') {
  357. return '门头';
  358. }
  359. return '平层';
  360. }
  361. /**
  362. * 推断产品类型
  363. */
  364. private inferProductType(roomName: string): string {
  365. const name = roomName.toLowerCase();
  366. if (name.includes('客厅') || name.includes('起居')) return 'living_room';
  367. if (name.includes('卧室') || name.includes('主卧') || name.includes('次卧')) return 'bedroom';
  368. if (name.includes('厨房')) return 'kitchen';
  369. if (name.includes('卫生间') || name.includes('浴室')) return 'bathroom';
  370. if (name.includes('餐厅')) return 'dining_room';
  371. if (name.includes('书房') || name.includes('工作室')) return 'study';
  372. if (name.includes('阳台')) return 'balcony';
  373. if (name.includes('玄关') || name.includes('走廊')) return 'corridor';
  374. return 'other';
  375. }
  376. /**
  377. * 计算价格明细
  378. */
  379. private calculatePriceBreakdown(basePrice: number): any {
  380. // 确保所有价格都是整数
  381. return {
  382. design: Math.round(basePrice * 0.3),
  383. modeling: Math.round(basePrice * 0.25),
  384. rendering: Math.round(basePrice * 0.25),
  385. softDecor: Math.round(basePrice * 0.15),
  386. postProcess: Math.round(basePrice * 0.05)
  387. };
  388. }
  389. /**
  390. * 从报价数据更新产品
  391. */
  392. private updateProductsFromQuotation(): void {
  393. if (!this.quotation.spaces || !this.products.length) return;
  394. this.quotation.spaces.forEach((space: any) => {
  395. const product = this.products.find(p =>
  396. p.get('productName') === space.name ||
  397. p.get('productName').includes(space.name)
  398. );
  399. if (product) {
  400. const quotation = product.get('quotation') || {};
  401. quotation.price = Math.round(space.subtotal || 0);
  402. quotation.processes = space.processes;
  403. product.set('quotation', quotation);
  404. }
  405. });
  406. }
  407. // ============ 报价管理核心方法 ============
  408. /**
  409. * 生成基于产品的报价
  410. */
  411. async generateQuotationFromProducts(): Promise<void> {
  412. if (!this.products.length) return;
  413. this.quotation.spaces = [];
  414. this.quotation.generatedAt = new Date();
  415. this.quotation.validUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
  416. for (const product of this.products) {
  417. const productName = product.get('productName');
  418. const quotation = product.get('quotation') || {};
  419. const basePrice = quotation.price || this.calculateBasePrice({
  420. priceLevel: this.projectInfo.priceLevel,
  421. projectType: this.projectInfo.projectType,
  422. renderType: this.projectInfo.renderType,
  423. spaceType: product.get('space')?.spaceType || this.getDefaultSpaceType(),
  424. styleLevel: product.get('space')?.styleLevel,
  425. businessType: product.get('space')?.businessType,
  426. architectureType: product.get('space')?.architectureType
  427. });
  428. // 生成工序明细(确保价格为整数)
  429. const processes = this.generateDefaultProcesses(Math.round(basePrice));
  430. const spaceData = {
  431. name: productName,
  432. productId: product.id,
  433. processes: processes,
  434. subtotal: this.calculateProductSubtotal(processes)
  435. };
  436. this.quotation.spaces.push(spaceData);
  437. }
  438. this.calculateTotal();
  439. this.updateProductBreakdown();
  440. await this.saveQuotationToProject();
  441. }
  442. /**
  443. * 生成默认分配(所有价格为整数)
  444. * 基于设计图总价按比例分配:建模阶段10%、软装渲染40%、公司分配50%
  445. */
  446. private generateDefaultProcesses(basePrice: number): any {
  447. return {
  448. modeling: {
  449. enabled: true,
  450. amount: Math.round(basePrice * 0.1),
  451. percentage: 10,
  452. description: '3D模型构建'
  453. },
  454. decoration: {
  455. enabled: true,
  456. amount: Math.round(basePrice * 0.4),
  457. percentage: 40,
  458. description: '软装搭配+效果图渲染'
  459. },
  460. company: {
  461. enabled: true,
  462. amount: Math.round(basePrice * 0.5),
  463. percentage: 50,
  464. description: '公司运营与利润'
  465. }
  466. };
  467. }
  468. /**
  469. * 计算产品小计(确保整数)
  470. * 现在processes实际是allocations(分配)
  471. */
  472. private calculateProductSubtotal(processes: any): number {
  473. let subtotal = 0;
  474. for (const allocation of Object.values(processes)) {
  475. const alloc = allocation as any;
  476. if (alloc.enabled) {
  477. subtotal += Math.round(alloc.amount);
  478. }
  479. }
  480. return Math.round(subtotal);
  481. }
  482. /**
  483. * 更新产品占比明细
  484. */
  485. private updateProductBreakdown(): void {
  486. this.quotation.spaceBreakdown = this.quotation.spaces.map((space: any) => ({
  487. spaceName: space.name,
  488. spaceId: space.productId || '',
  489. amount: Math.round(space.subtotal),
  490. percentage: this.quotation.total > 0 ? Math.round((space.subtotal / this.quotation.total) * 100) : 0
  491. }));
  492. }
  493. /**
  494. * 保存报价到项目
  495. */
  496. private async saveQuotationToProject(): Promise<void> {
  497. if (!this.project) return;
  498. try {
  499. const data = this.project.get('data') || {};
  500. data.quotation = this.quotation;
  501. this.project.set('data', data);
  502. await this.project.save();
  503. this.quotationChange.emit(this.quotation);
  504. } catch (error) {
  505. console.error('保存报价失败:', error);
  506. }
  507. }
  508. // ============ UI交互方法 ============
  509. /**
  510. * 展开所有产品
  511. */
  512. expandAll() {
  513. this.quotation.spaces.forEach((space: any) => {
  514. this.expandedProducts.add(space.name);
  515. });
  516. }
  517. /**
  518. * 折叠所有产品
  519. */
  520. collapseAll() {
  521. this.expandedProducts.clear();
  522. }
  523. /**
  524. * 切换分配启用状态
  525. */
  526. toggleProcess(space: any, processKey: string) {
  527. const allocation = space.processes[processKey];
  528. allocation.enabled = !allocation.enabled;
  529. if (!allocation.enabled) {
  530. allocation.amount = 0;
  531. }
  532. this.calculateTotal();
  533. }
  534. /**
  535. * 工序价格或数量变化
  536. */
  537. onProcessChange() {
  538. this.calculateTotal();
  539. }
  540. /**
  541. * 计算报价总额(确保所有价格为整数)
  542. */
  543. calculateTotal() {
  544. let total = 0;
  545. for (const space of this.quotation.spaces) {
  546. for (const processKey of Object.keys(space.processes)) {
  547. const allocation = space.processes[processKey];
  548. if (allocation.enabled) {
  549. // 确保分配金额是整数
  550. const amount = Math.round(allocation.amount);
  551. total += amount;
  552. }
  553. }
  554. }
  555. this.quotation.total = Math.round(total);
  556. // 自动计算项目级别的内部执行分配(确保分配金额为整数)
  557. this.quotation.allocation = calculateAllocation(this.quotation.total);
  558. // 更新产品级别的分配
  559. this.updateProductsAllocation();
  560. this.quotationChange.emit(this.quotation);
  561. this.totalChange.emit(this.quotation.total);
  562. }
  563. /**
  564. * 更新所有产品的内部分配(确保整数)
  565. */
  566. private updateProductsAllocation() {
  567. for (const space of this.quotation.spaces) {
  568. const product = this.products.find(p => p.id === space.productId);
  569. if (product) {
  570. const productPrice = Math.round(this.calculateSpaceSubtotal(space));
  571. const quotation = product.get('quotation') || {};
  572. quotation.allocation = calculateProductAllocation(productPrice);
  573. product.set('quotation', quotation);
  574. }
  575. }
  576. }
  577. /**
  578. * 计算空间小计
  579. */
  580. calculateSpaceSubtotal(space: any): number {
  581. return Math.round(this.calculateProductSubtotal(space.processes));
  582. }
  583. /**
  584. * 计算分配金额
  585. */
  586. calculateProcessSubtotal(space: any, processKey: string): number {
  587. const allocation = space.processes?.[processKey];
  588. const amount = Math.round(allocation?.amount || 0);
  589. return amount;
  590. }
  591. // ============ 辅助方法 ============
  592. isProcessEnabled(space: any, processKey: string): boolean {
  593. const allocation = space.processes?.[processKey];
  594. return allocation?.enabled || false;
  595. }
  596. setAllocationAmount(space: any, allocationKey: string, value: any): void {
  597. const allocation = space.processes?.[allocationKey];
  598. if (allocation) {
  599. allocation.amount = Math.round(Number(value) || 0);
  600. }
  601. }
  602. getAllocationAmount(space: any, allocationKey: string): number {
  603. const allocation = space.processes?.[allocationKey];
  604. return Math.round(allocation?.amount || 0);
  605. }
  606. getAllocationPercentage(space: any, allocationKey: string): number {
  607. const allocation = space.processes?.[allocationKey];
  608. return allocation?.percentage || 0;
  609. }
  610. getAllocationDescription(space: any, allocationKey: string): string {
  611. const allocation = space.processes?.[allocationKey];
  612. return allocation?.description || '';
  613. }
  614. // ============ 产品管理方法 ============
  615. /**
  616. * 添加新产品(简化版,用于快速添加)
  617. */
  618. async addProduct(productName?: string): Promise<void> {
  619. if (!this.project) return;
  620. const name = productName ||await window?.fmode?.input('请输入产品名称:');
  621. if (!name) return;
  622. try {
  623. await this.createProduct(name);
  624. await this.loadProjectProducts();
  625. await this.generateQuotationFromProducts();
  626. } catch (error) {
  627. console.error('添加产品失败:', error);
  628. window?.fmode?.alert('添加失败,请重试');
  629. }
  630. }
  631. /**
  632. * 编辑产品名称
  633. */
  634. async editProduct(productId: string): Promise<void> {
  635. const product = this.products.find(p => p.id === productId);
  636. if (!product) return;
  637. const newName =await window?.fmode?.input('修改产品名称:', product.get('productName'));
  638. if (!newName || newName === product.get('productName')) return;
  639. try {
  640. product.set('productName', newName);
  641. product.set('productType', this.inferProductType(newName));
  642. await product.save();
  643. const spaceData = this.quotation.spaces.find((s: any) => s.productId === productId);
  644. if (spaceData) {
  645. spaceData.name = newName;
  646. }
  647. await this.saveQuotationToProject();
  648. await this.loadProjectProducts();
  649. } catch (error) {
  650. console.error('更新产品失败:', error);
  651. window?.fmode?.alert('更新失败,请重试');
  652. }
  653. }
  654. /**
  655. * 删除产品
  656. */
  657. async deleteProduct(productId: string): Promise<void> {
  658. if (!await window?.fmode?.confirm('确定要删除这个产品吗?相关数据将被清除。')) return;
  659. try {
  660. const product = this.products.find(p => p.id === productId);
  661. if (product) {
  662. await product.destroy();
  663. }
  664. // 更新本地产品列表(不重新加载,避免触发createDefaultProducts)
  665. this.products = this.products.filter(p => p.id !== productId);
  666. this.productsChange.emit(this.products);
  667. // 更新报价spaces
  668. this.quotation.spaces = this.quotation.spaces.filter((s: any) => s.productId !== productId);
  669. // 重新计算总价
  670. this.calculateTotal();
  671. this.updateProductBreakdown();
  672. // 保存报价
  673. await this.saveQuotationToProject();
  674. } catch (error) {
  675. console.error('删除产品失败:', error);
  676. window?.fmode?.alert('删除失败,请重试');
  677. }
  678. }
  679. /**
  680. * 保存报价
  681. */
  682. async saveQuotation(): Promise<void> {
  683. if (!this.canEdit) return;
  684. try {
  685. await this.saveQuotationToProject();
  686. window?.fmode?.alert('保存成功');
  687. } catch (error) {
  688. console.error('保存失败:', error);
  689. window?.fmode?.alert('保存失败');
  690. }
  691. }
  692. /**
  693. * 切换内部执行分配显示
  694. */
  695. toggleAllocation() {
  696. this.showAllocation = !this.showAllocation;
  697. }
  698. toggleProductExpand(spaceName: string): void {
  699. if (this.expandedProducts.has(spaceName)) {
  700. this.expandedProducts.delete(spaceName);
  701. } else {
  702. this.expandedProducts.add(spaceName);
  703. }
  704. }
  705. isProductExpanded(spaceName: string): boolean {
  706. return this.expandedProducts.has(spaceName);
  707. }
  708. // ============ 辅助显示方法 ============
  709. getProductDesigner(product: any): string {
  710. const profile = product.get('profile');
  711. return profile ? profile.get('name') : '未分配';
  712. }
  713. getProductStatus(product: any): string {
  714. return product.get('status') || 'not_started';
  715. }
  716. getProductStatusColor(status: string): string {
  717. const colorMap: Record<string, string> = {
  718. 'not_started': 'medium',
  719. 'in_progress': 'warning',
  720. 'awaiting_review': 'info',
  721. 'completed': 'success',
  722. 'blocked': 'danger',
  723. 'delayed': 'danger'
  724. };
  725. return colorMap[status] || 'medium';
  726. }
  727. getProductStatusText(status: string): string {
  728. const textMap: Record<string, string> = {
  729. 'not_started': '未开始',
  730. 'in_progress': '进行中',
  731. 'awaiting_review': '待审核',
  732. 'completed': '已完成',
  733. 'blocked': '已阻塞',
  734. 'delayed': '已延期'
  735. };
  736. return textMap[status] || status;
  737. }
  738. getProductIcon(productType: string): string {
  739. const iconMap: Record<string, string> = {
  740. 'living_room': 'living-room',
  741. 'bedroom': 'bedroom',
  742. 'kitchen': 'kitchen',
  743. 'bathroom': 'bathroom',
  744. 'dining_room': 'dining-room',
  745. 'study': 'study',
  746. 'balcony': 'balcony',
  747. 'corridor': 'corridor',
  748. 'storage': 'storage',
  749. 'entrance': 'entrance',
  750. 'other': 'room'
  751. };
  752. return iconMap[productType] || 'room';
  753. }
  754. formatPrice(price: number): string {
  755. // 确保价格为整数
  756. return `¥${Math.round(price)}`;
  757. }
  758. forSpacePrice(space,allocationType){
  759. let price = Math.round((this.getProductForSpace(space.productId)?.get('quotation')?.basePrice || 0) * allocationType.percentage / 100)
  760. this.formatPercentage(price)
  761. }
  762. formatPercentage(value: number): string {
  763. return `${Math.round(value)}%`;
  764. }
  765. getProductTypeForSpace(spaceName: string): string {
  766. return this.inferProductType(spaceName);
  767. }
  768. getProductIconForSpace(spaceName: string): string {
  769. const productType = this.getProductTypeForSpace(spaceName);
  770. return this.getProductIcon(productType);
  771. }
  772. getStatusColorForSpace(spaceId: string): string {
  773. const product = this.products.find(p => p.id === spaceId);
  774. if (product) {
  775. return this.getProductStatusColor(product.get('status'));
  776. }
  777. return 'medium';
  778. }
  779. getStatusTextForSpace(spaceId: string): string {
  780. const product = this.products.find(p => p.id === spaceId);
  781. if (product) {
  782. return this.getProductStatusText(product.get('status'));
  783. }
  784. return '未开始';
  785. }
  786. getDesignerNameForSpace(spaceId: string): string {
  787. const product = this.products.find(p => p.id === spaceId);
  788. if (product) {
  789. return this.getProductDesigner(product);
  790. }
  791. return '未分配';
  792. }
  793. getProductForSpace(productId: string): any {
  794. return this.products.find(p => p.id === productId) || null;
  795. }
  796. getSpacePercentage(spaceId: string): number {
  797. if (!this.quotation.spaceBreakdown) return 0;
  798. const breakdown = this.quotation.spaceBreakdown.find((b: any) => b.spaceId === spaceId);
  799. return Math.round(breakdown?.percentage || 0);
  800. }
  801. // ============ 产品添加模态框方法 ============
  802. /**
  803. * 打开添加产品模态框
  804. */
  805. openAddProductModal(): void {
  806. // 重置表单
  807. this.resetNewProductForm();
  808. this.isEditMode = false;
  809. this.editingProductId = '';
  810. this.showAddProductModal = true;
  811. }
  812. /**
  813. * 打开编辑产品模态框
  814. */
  815. openEditProductModal(productId: string): void {
  816. const product = this.products.find(p => p.id === productId);
  817. if (!product) return;
  818. // 预填充产品数据
  819. const space = product.get('space') || {};
  820. const quotation = product.get('quotation') || {};
  821. const adjustments = quotation.adjustments || {};
  822. // 判断是否为自定义名称
  823. const presetScenes = this.getPresetScenes();
  824. const isPreset = presetScenes.includes(product.get('productName'));
  825. this.newProduct = {
  826. isCustom: !isPreset,
  827. sceneName: isPreset ? product.get('productName') : '',
  828. productName: product.get('productName'),
  829. spaceType: space.spaceType || this.getDefaultSpaceType(),
  830. styleLevel: space.styleLevel || '基础风格组',
  831. businessType: space.businessType || '办公空间',
  832. architectureType: space.architectureType || '门头',
  833. adjustments: {
  834. extraFunction: adjustments.extraFunction || 0,
  835. complexity: adjustments.complexity || 0,
  836. design: adjustments.design || false,
  837. panoramic: adjustments.panoramic || false
  838. }
  839. };
  840. this.isEditMode = true;
  841. this.editingProductId = productId;
  842. this.showAddProductModal = true;
  843. }
  844. /**
  845. * 关闭添加/编辑产品模态框
  846. */
  847. closeAddProductModal(): void {
  848. this.showAddProductModal = false;
  849. this.isEditMode = false;
  850. this.editingProductId = '';
  851. this.resetNewProductForm();
  852. }
  853. /**
  854. * 重置新产品表单
  855. */
  856. private resetNewProductForm(): void {
  857. this.newProduct = {
  858. isCustom: false,
  859. sceneName: '',
  860. productName: '',
  861. spaceType: this.getDefaultSpaceType(),
  862. styleLevel: '基础风格组',
  863. businessType: '办公空间',
  864. architectureType: '门头',
  865. adjustments: {
  866. extraFunction: 0,
  867. complexity: 0,
  868. design: false,
  869. panoramic: false
  870. }
  871. };
  872. }
  873. /**
  874. * 获取预设场景列表
  875. */
  876. getPresetScenes(): string[] {
  877. return this.presetScenes[this.projectInfo.projectType] || [];
  878. }
  879. /**
  880. * 选择预设场景
  881. */
  882. selectScene(scene: string): void {
  883. this.newProduct.isCustom = false;
  884. this.newProduct.sceneName = scene;
  885. this.newProduct.productName = scene;
  886. }
  887. /**
  888. * 选择自定义场景
  889. */
  890. selectCustomScene(): void {
  891. this.newProduct.isCustom = true;
  892. this.newProduct.sceneName = '';
  893. this.newProduct.productName = '';
  894. }
  895. /**
  896. * 计算预览基础价格
  897. */
  898. calculatePreviewBasePrice(): number {
  899. return this.calculateBasePrice({
  900. priceLevel: this.projectInfo.priceLevel,
  901. projectType: this.projectInfo.projectType,
  902. renderType: this.projectInfo.renderType,
  903. spaceType: this.newProduct.spaceType,
  904. styleLevel: this.newProduct.styleLevel,
  905. businessType: this.newProduct.businessType,
  906. architectureType: this.newProduct.architectureType
  907. });
  908. }
  909. /**
  910. * 计算预览加价总额
  911. */
  912. calculatePreviewAdjustmentTotal(): number {
  913. const basePrice = this.calculatePreviewBasePrice();
  914. const adjustments = this.newProduct.adjustments;
  915. let total = 0;
  916. // 功能区加价
  917. if (adjustments.extraFunction > 0) {
  918. if (this.projectInfo.projectType === '家装') {
  919. total += adjustments.extraFunction * 100;
  920. } else if (this.projectInfo.projectType === '工装') {
  921. total += adjustments.extraFunction * 400;
  922. }
  923. }
  924. // 造型复杂度加价
  925. if (adjustments.complexity > 0) {
  926. total += adjustments.complexity;
  927. }
  928. // 设计服务倍增
  929. let multiplier = 1;
  930. if (adjustments.design) {
  931. multiplier *= 2;
  932. }
  933. // 全景渲染倍增(仅工装)
  934. if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
  935. multiplier *= 2;
  936. }
  937. // 如果有倍增,计算额外的加价
  938. if (multiplier > 1) {
  939. total = (basePrice + total) * multiplier - basePrice;
  940. }
  941. return Math.round(total);
  942. }
  943. /**
  944. * 计算预览最终价格
  945. */
  946. calculatePreviewFinalPrice(): number {
  947. const basePrice = this.calculatePreviewBasePrice();
  948. const adjustments = this.newProduct.adjustments;
  949. let price = basePrice;
  950. // 功能区加价
  951. if (adjustments.extraFunction > 0) {
  952. if (this.projectInfo.projectType === '家装') {
  953. price += adjustments.extraFunction * 100;
  954. } else if (this.projectInfo.projectType === '工装') {
  955. price += adjustments.extraFunction * 400;
  956. }
  957. }
  958. // 造型复杂度加价
  959. if (adjustments.complexity > 0) {
  960. price += adjustments.complexity;
  961. }
  962. // 设计服务倍增
  963. if (adjustments.design) {
  964. price *= 2;
  965. }
  966. // 全景渲染倍增(仅工装)
  967. if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
  968. price *= 2;
  969. }
  970. return Math.round(price);
  971. }
  972. /**
  973. * 验证新产品表单是否有效
  974. */
  975. isNewProductValid(): boolean {
  976. if (this.newProduct.isCustom) {
  977. return this.newProduct.productName.trim().length > 0;
  978. }
  979. return this.newProduct.sceneName.length > 0;
  980. }
  981. /**
  982. * 确认添加/编辑产品
  983. */
  984. async confirmAddProduct(): Promise<void> {
  985. if (!this.isNewProductValid()) {
  986. window?.fmode?.alert('请完整填写产品信息');
  987. return;
  988. }
  989. try {
  990. if (this.isEditMode) {
  991. await this.updateExistingProduct();
  992. } else {
  993. await this.createNewProduct();
  994. }
  995. } catch (error) {
  996. console.error(this.isEditMode ? '编辑产品失败:' : '添加产品失败:', error);
  997. window?.fmode?.alert(this.isEditMode ? '编辑产品失败,请重试' : '添加产品失败,请重试');
  998. }
  999. }
  1000. /**
  1001. * 创建新产品
  1002. */
  1003. private async createNewProduct(): Promise<void> {
  1004. const productName = this.newProduct.isCustom
  1005. ? this.newProduct.productName
  1006. : this.newProduct.sceneName;
  1007. const config: any = {
  1008. spaceType: this.newProduct.spaceType
  1009. };
  1010. if (this.projectInfo.projectType === '家装') {
  1011. config.styleLevel = this.newProduct.styleLevel;
  1012. } else if (this.projectInfo.projectType === '工装') {
  1013. config.businessType = this.newProduct.businessType;
  1014. } else if (this.projectInfo.projectType === '建筑类') {
  1015. config.architectureType = this.newProduct.architectureType;
  1016. }
  1017. // 创建产品
  1018. const product = await this.createProductWithAdjustments(productName, config);
  1019. if (product) {
  1020. // 重新加载产品列表
  1021. await this.loadProjectProducts();
  1022. // 重新生成报价
  1023. await this.generateQuotationFromProducts();
  1024. // 关闭模态框
  1025. this.closeAddProductModal();
  1026. window?.fmode?.alert(`成功添加产品: ${productName}`);
  1027. } else {
  1028. throw new Error('创建产品失败');
  1029. }
  1030. }
  1031. /**
  1032. * 更新现有产品
  1033. */
  1034. private async updateExistingProduct(): Promise<void> {
  1035. const product = this.products.find(p => p.id === this.editingProductId);
  1036. if (!product) {
  1037. throw new Error('找不到要编辑的产品');
  1038. }
  1039. const productName = this.newProduct.isCustom
  1040. ? this.newProduct.productName
  1041. : this.newProduct.sceneName;
  1042. // 更新产品名称和类型
  1043. product.set('productName', productName);
  1044. product.set('productType', this.inferProductType(productName));
  1045. // 确定配置
  1046. const spaceType = this.newProduct.spaceType;
  1047. const styleLevel = this.newProduct.styleLevel;
  1048. const businessType = this.newProduct.businessType;
  1049. const architectureType = this.newProduct.architectureType;
  1050. // 更新空间信息
  1051. const space = product.get('space') || {};
  1052. space.spaceName = productName;
  1053. space.spaceType = spaceType;
  1054. space.styleLevel = styleLevel;
  1055. space.businessType = businessType;
  1056. space.architectureType = architectureType;
  1057. product.set('space', space);
  1058. // 重新计算基础价格
  1059. let basePrice = this.calculateBasePrice({
  1060. priceLevel: this.projectInfo.priceLevel,
  1061. projectType: this.projectInfo.projectType,
  1062. renderType: this.projectInfo.renderType,
  1063. spaceType: spaceType,
  1064. styleLevel: styleLevel,
  1065. businessType: businessType,
  1066. architectureType: architectureType
  1067. });
  1068. // 应用加价规则
  1069. let finalPrice = basePrice;
  1070. const adjustments = this.newProduct.adjustments;
  1071. // 功能区加价
  1072. if (adjustments.extraFunction > 0) {
  1073. if (this.projectInfo.projectType === '家装') {
  1074. finalPrice += adjustments.extraFunction * 100;
  1075. } else if (this.projectInfo.projectType === '工装') {
  1076. finalPrice += adjustments.extraFunction * 400;
  1077. }
  1078. }
  1079. // 造型复杂度加价
  1080. if (adjustments.complexity > 0) {
  1081. finalPrice += adjustments.complexity;
  1082. }
  1083. // 设计服务倍增
  1084. if (adjustments.design) {
  1085. finalPrice *= 2;
  1086. }
  1087. // 全景渲染倍增(仅工装)
  1088. if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
  1089. finalPrice *= 2;
  1090. }
  1091. finalPrice = Math.round(finalPrice);
  1092. // 更新报价信息
  1093. const quotation = product.get('quotation') || {};
  1094. quotation.basePrice = Math.round(basePrice);
  1095. quotation.price = finalPrice;
  1096. quotation.adjustments = {
  1097. extraFunction: adjustments.extraFunction,
  1098. complexity: adjustments.complexity,
  1099. design: adjustments.design,
  1100. panoramic: adjustments.panoramic
  1101. };
  1102. quotation.breakdown = this.calculatePriceBreakdown(finalPrice);
  1103. product.set('quotation', quotation);
  1104. // 保存产品
  1105. await product.save();
  1106. // 更新报价中的空间数据
  1107. const spaceData = this.quotation.spaces.find((s: any) => s.productId === this.editingProductId);
  1108. if (spaceData) {
  1109. spaceData.name = productName;
  1110. // 重新生成该空间的分配
  1111. spaceData.processes = this.generateDefaultProcesses(finalPrice);
  1112. }
  1113. // 重新加载产品列表
  1114. await this.loadProjectProducts();
  1115. // 重新生成报价
  1116. await this.generateQuotationFromProducts();
  1117. // 关闭模态框
  1118. this.closeAddProductModal();
  1119. window?.fmode?.alert(`成功更新产品: ${productName}`);
  1120. }
  1121. /**
  1122. * 创建产品(带加价规则)
  1123. */
  1124. private async createProductWithAdjustments(
  1125. productName: string,
  1126. config: {
  1127. spaceType?: string;
  1128. styleLevel?: string;
  1129. businessType?: string;
  1130. architectureType?: string;
  1131. }
  1132. ): Promise<any> {
  1133. if (!this.project) return null;
  1134. try {
  1135. const product = new Parse.Object('Product');
  1136. product.set('project', this.project.toPointer());
  1137. product.set('productName', productName);
  1138. product.set('productType', this.inferProductType(productName));
  1139. // 确定配置
  1140. const spaceType = config?.spaceType || this.getDefaultSpaceType();
  1141. const styleLevel = config?.styleLevel || '基础风格组';
  1142. const businessType = config?.businessType || '办公空间';
  1143. const architectureType = config?.architectureType;
  1144. // 设置空间信息
  1145. product.set('space', {
  1146. spaceName: productName,
  1147. area: 0,
  1148. dimensions: { length: 0, width: 0, height: 0 },
  1149. spaceType: spaceType,
  1150. styleLevel: styleLevel,
  1151. businessType: businessType,
  1152. architectureType: architectureType,
  1153. features: [],
  1154. constraints: [],
  1155. priority: 5,
  1156. complexity: 'medium'
  1157. });
  1158. // 计算基础价格
  1159. let basePrice = this.calculateBasePrice({
  1160. priceLevel: this.projectInfo.priceLevel,
  1161. projectType: this.projectInfo.projectType,
  1162. renderType: this.projectInfo.renderType,
  1163. spaceType: spaceType,
  1164. styleLevel: styleLevel,
  1165. businessType: businessType,
  1166. architectureType: architectureType
  1167. });
  1168. // 应用加价规则
  1169. let finalPrice = basePrice;
  1170. const adjustments = this.newProduct.adjustments;
  1171. // 功能区加价
  1172. if (adjustments.extraFunction > 0) {
  1173. if (this.projectInfo.projectType === '家装') {
  1174. finalPrice += adjustments.extraFunction * 100;
  1175. } else if (this.projectInfo.projectType === '工装') {
  1176. finalPrice += adjustments.extraFunction * 400;
  1177. }
  1178. }
  1179. // 造型复杂度加价
  1180. if (adjustments.complexity > 0) {
  1181. finalPrice += adjustments.complexity;
  1182. }
  1183. // 设计服务倍增
  1184. if (adjustments.design) {
  1185. finalPrice *= 2;
  1186. }
  1187. // 全景渲染倍增(仅工装)
  1188. if (this.projectInfo.projectType === '工装' && adjustments.panoramic) {
  1189. finalPrice *= 2;
  1190. }
  1191. finalPrice = Math.round(finalPrice);
  1192. // 设置报价信息
  1193. product.set('quotation', {
  1194. priceLevel: this.projectInfo.priceLevel,
  1195. basePrice: Math.round(basePrice),
  1196. price: finalPrice,
  1197. currency: 'CNY',
  1198. adjustments: {
  1199. extraFunction: adjustments.extraFunction,
  1200. complexity: adjustments.complexity,
  1201. design: adjustments.design,
  1202. panoramic: adjustments.panoramic
  1203. },
  1204. breakdown: this.calculatePriceBreakdown(finalPrice),
  1205. status: 'draft',
  1206. validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
  1207. });
  1208. // 设置需求信息
  1209. product.set('requirements', {
  1210. colorRequirement: {},
  1211. materialRequirement: {},
  1212. lightingRequirement: {},
  1213. specificRequirements: [],
  1214. constraints: {}
  1215. });
  1216. product.set('status', 'not_started');
  1217. await product.save();
  1218. return product;
  1219. } catch (error) {
  1220. console.error('创建产品失败:', error);
  1221. return null;
  1222. }
  1223. }
  1224. }