consultation-order.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { Component, signal, Inject } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
  4. import { RouterModule } from '@angular/router';
  5. import { ProjectService } from '../../../services/project.service';
  6. import { MatChipInputEvent } from '@angular/material/chips';
  7. import { COMMA, ENTER } from '@angular/cdk/keycodes';
  8. import { MatChipsModule } from '@angular/material/chips';
  9. import { MatIconModule } from '@angular/material/icon';
  10. import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
  11. import { MatDialog, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
  12. import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
  13. import { ProjectGroupDialog } from './project-group-dialog.component';
  14. // 定义客户信息接口
  15. interface Customer {
  16. id: string;
  17. name: string;
  18. phone: string;
  19. wechat?: string;
  20. avatar?: string;
  21. customerType?: string; // 新客户/老客户/VIP客户
  22. source?: string; // 来源渠道
  23. remark?: string;
  24. // 客户标签信息
  25. demandType?: string;
  26. preferenceTags?: string[];
  27. followUpStatus?: string;
  28. }
  29. // 定义需求信息接口
  30. interface Requirement {
  31. style: string;
  32. budget: string;
  33. area: number;
  34. houseType: string;
  35. floor: number;
  36. decorationType: string;
  37. preferredDesigner?: string;
  38. specialRequirements?: string;
  39. referenceCases?: string[];
  40. }
  41. // 标签选项定义
  42. const DEMAND_TYPES = [
  43. { value: 'price', label: '价格敏感' },
  44. { value: 'quality', label: '质量敏感' },
  45. { value: 'comprehensive', label: '综合要求' }
  46. ];
  47. const FOLLOW_UP_STATUS = [
  48. { value: 'quotation', label: '待报价' },
  49. { value: 'confirm', label: '待确认需求' },
  50. { value: 'lost', label: '已失联' }
  51. ];
  52. // 预设的偏好标签选项
  53. const PREFERENCE_TAG_OPTIONS = [
  54. // 颜色偏好
  55. '柔和色系', '明亮色系', '深色系', '中性色系',
  56. // 材质偏好
  57. '环保材料', '实木', '大理石', '瓷砖', '地板', '墙纸',
  58. // 风格偏好
  59. '现代简约', '北欧风格', '中式风格', '美式风格', '工业风',
  60. // 其他偏好
  61. '智能家电', '收纳空间', '开放式厨房', '大窗户'
  62. ];
  63. @Component({
  64. selector: 'app-consultation-order',
  65. standalone: true,
  66. imports: [
  67. CommonModule,
  68. FormsModule,
  69. ReactiveFormsModule,
  70. RouterModule,
  71. MatChipsModule,
  72. MatIconModule,
  73. MatSnackBarModule,
  74. MatDialogModule,
  75. MatProgressSpinnerModule
  76. ],
  77. templateUrl: './consultation-order.html',
  78. styleUrls: ['./consultation-order.scss', '../customer-service-styles.scss']
  79. })
  80. export class ConsultationOrder {
  81. // 搜索客户关键词
  82. searchKeyword = signal('');
  83. // 搜索结果列表
  84. searchResults = signal<Customer[]>([]);
  85. // 选中的客户
  86. selectedCustomer = signal<Customer | null>(null);
  87. // 报价范围
  88. estimatedPriceRange = signal<string>('');
  89. // 匹配的案例
  90. matchedCases = signal<any[]>([]);
  91. // 表单提交状态
  92. isSubmitting = signal(false);
  93. // 成功提示显示状态
  94. showSuccessMessage = signal(false);
  95. // 需求表单
  96. requirementForm: FormGroup;
  97. // 客户表单
  98. customerForm: FormGroup;
  99. // 样式选项
  100. styleOptions = [
  101. '现代简约', '北欧风', '工业风', '新中式', '法式轻奢', '日式', '美式', '混搭'
  102. ];
  103. // 户型选项
  104. houseTypeOptions = [
  105. '一室一厅', '两室一厅', '两室两厅', '三室一厅', '三室两厅', '四室两厅', '复式', '别墅', '其他'
  106. ];
  107. // 装修类型选项
  108. decorationTypeOptions = [
  109. '全包', '半包', '清包', '旧房翻新', '局部改造'
  110. ];
  111. // 标签系统
  112. demandTypes = DEMAND_TYPES;
  113. followUpStatus = FOLLOW_UP_STATUS;
  114. preferenceTagOptions = PREFERENCE_TAG_OPTIONS;
  115. addOnBlur = true;
  116. readonly separatorKeysCodes = [ENTER, COMMA] as const;
  117. preferenceTags: string[] = [];
  118. constructor(
  119. private fb: FormBuilder,
  120. private projectService: ProjectService,
  121. private snackBar: MatSnackBar,
  122. private dialog: MatDialog
  123. ) {
  124. // 初始化需求表单
  125. this.requirementForm = this.fb.group({
  126. style: ['', Validators.required],
  127. budget: ['', Validators.required],
  128. area: ['', [Validators.required, Validators.min(1)]],
  129. houseType: ['', Validators.required],
  130. floor: ['', Validators.min(1)],
  131. decorationType: ['', Validators.required],
  132. preferredDesigner: [''],
  133. specialRequirements: [''],
  134. referenceCases: [[]]
  135. });
  136. // 初始化客户表单
  137. this.customerForm = this.fb.group({
  138. name: ['', Validators.required],
  139. phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
  140. wechat: [''],
  141. customerType: ['新客户'],
  142. source: [''],
  143. remark: [''],
  144. demandType: [''],
  145. followUpStatus: ['']
  146. });
  147. // 监听表单值变化,自动计算报价和匹配案例
  148. this.requirementForm.valueChanges.subscribe(() => {
  149. this.calculateEstimatedPrice();
  150. this.matchCases();
  151. });
  152. }
  153. // 添加偏好标签
  154. addPreferenceTag(event: MatChipInputEvent): void {
  155. const value = (event.value || '').trim();
  156. // 添加标签,如果它不是空的,并且不在已有标签中
  157. if (value && !this.preferenceTags.includes(value)) {
  158. this.preferenceTags.push(value);
  159. }
  160. // 清空输入框
  161. if (event.input) {
  162. event.input.value = '';
  163. }
  164. }
  165. // 删除偏好标签
  166. removePreferenceTag(tag: string): void {
  167. const index = this.preferenceTags.indexOf(tag);
  168. if (index >= 0) {
  169. this.preferenceTags.splice(index, 1);
  170. }
  171. }
  172. // 从预设选项中添加标签
  173. addFromPreset(tag: string): void {
  174. if (!this.preferenceTags.includes(tag)) {
  175. this.preferenceTags.push(tag);
  176. }
  177. }
  178. // 搜索客户
  179. searchCustomer() {
  180. if (this.searchKeyword().length >= 2) {
  181. // 模拟搜索结果
  182. this.searchResults.set([
  183. {
  184. id: '1',
  185. name: '张先生',
  186. phone: '138****5678',
  187. customerType: '老客户',
  188. source: '官网咨询',
  189. avatar: "data:image/svg+xml,%3Csvg width='64' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23E6E6E6'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E"
  190. },
  191. {
  192. id: '2',
  193. name: '李女士',
  194. phone: '139****1234',
  195. customerType: 'VIP客户',
  196. source: '推荐介绍',
  197. avatar: "data:image/svg+xml,%3Csvg width='65' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23DCDCDC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E"
  198. }
  199. ]);
  200. }
  201. }
  202. // 选择客户
  203. selectCustomer(customer: Customer) {
  204. this.selectedCustomer.set(customer);
  205. // 填充客户表单
  206. this.customerForm.patchValue({
  207. name: customer.name,
  208. phone: customer.phone,
  209. wechat: customer.wechat || '',
  210. customerType: customer.customerType || '新客户',
  211. source: customer.source || '',
  212. remark: customer.remark || ''
  213. });
  214. // 清空搜索结果
  215. this.searchResults.set([]);
  216. this.searchKeyword.set('');
  217. }
  218. // 清除选中的客户
  219. clearSelectedCustomer() {
  220. this.selectedCustomer.set(null);
  221. this.customerForm.reset({
  222. customerType: '新客户'
  223. });
  224. }
  225. // 计算预估报价
  226. calculateEstimatedPrice() {
  227. const { area, decorationType, style } = this.requirementForm.value;
  228. if (area && decorationType) {
  229. // 模拟报价计算逻辑
  230. let basePrice = 0;
  231. switch (decorationType) {
  232. case '全包':
  233. basePrice = 1800;
  234. break;
  235. case '半包':
  236. basePrice = 1200;
  237. break;
  238. case '清包':
  239. basePrice = 800;
  240. break;
  241. case '旧房翻新':
  242. basePrice = 2000;
  243. break;
  244. case '局部改造':
  245. basePrice = 1500;
  246. break;
  247. default:
  248. basePrice = 1200;
  249. break;
  250. }
  251. // 风格加价
  252. const stylePremium = ['法式轻奢', '新中式', '日式'].includes(style) ? 0.2 : 0;
  253. const totalPrice = area * basePrice * (1 + stylePremium);
  254. const lowerBound = Math.floor(totalPrice * 0.9);
  255. const upperBound = Math.ceil(totalPrice * 1.1);
  256. this.estimatedPriceRange.set(
  257. `¥${lowerBound.toLocaleString()} - ¥${upperBound.toLocaleString()}`
  258. );
  259. }
  260. }
  261. // 匹配案例
  262. matchCases() {
  263. const { style, houseType, area } = this.requirementForm.value;
  264. if (style && houseType && area) {
  265. // 模拟匹配案例
  266. this.matchedCases.set([
  267. {
  268. id: '101',
  269. name: `${style}风格 ${houseType}设计`,
  270. imageUrl: `https://picsum.photos/id/${30 + Math.floor(Math.random() * 10)}/300/200`,
  271. designer: '王设计师',
  272. area: area + '㎡',
  273. similarity: 92
  274. },
  275. {
  276. id: '102',
  277. name: `${houseType} ${style}案例展示`,
  278. imageUrl: `https://picsum.photos/id/${40 + Math.floor(Math.random() * 10)}/300/200`,
  279. designer: '张设计师',
  280. area: (area + 10) + '㎡',
  281. similarity: 85
  282. }
  283. ]);
  284. }
  285. }
  286. // 选择参考案例
  287. selectReferenceCase(caseItem: any) {
  288. const currentCases = this.requirementForm.get('referenceCases')?.value || [];
  289. if (!currentCases.includes(caseItem.id)) {
  290. this.requirementForm.patchValue({
  291. referenceCases: [...currentCases, caseItem.id]
  292. });
  293. }
  294. }
  295. // 移除参考案例
  296. removeReferenceCase(caseId: string) {
  297. const currentCases = this.requirementForm.get('referenceCases')?.value || [];
  298. this.requirementForm.patchValue({
  299. referenceCases: currentCases.filter((id: string) => id !== caseId)
  300. });
  301. }
  302. // 提交表单
  303. submitForm() {
  304. if (this.requirementForm.valid && this.customerForm.valid) {
  305. this.isSubmitting.set(true);
  306. const formData = {
  307. customerInfo: this.customerForm.value,
  308. requirementInfo: this.requirementForm.value,
  309. estimatedPriceRange: this.estimatedPriceRange(),
  310. createdAt: new Date()
  311. };
  312. // 模拟提交请求
  313. setTimeout(() => {
  314. console.log('提交的表单数据:', formData);
  315. this.isSubmitting.set(false);
  316. this.showSuccessMessage.set(true);
  317. // 3秒后隐藏成功提示
  318. setTimeout(() => {
  319. this.showSuccessMessage.set(false);
  320. }, 3000);
  321. }, 1500);
  322. }
  323. }
  324. // 创建项目
  325. createProject() {
  326. if (!this.selectedCustomer()) {
  327. this.snackBar.open('请先选择客户', '关闭', { duration: 3000 });
  328. return;
  329. }
  330. if (this.requirementForm.invalid) {
  331. this.snackBar.open('请完善需求信息', '关闭', { duration: 3000 });
  332. return;
  333. }
  334. const selectedCustomer = this.selectedCustomer()!;
  335. const projectData = {
  336. customerId: selectedCustomer.id,
  337. customerName: selectedCustomer.name,
  338. requirement: this.requirementForm.value,
  339. referenceCases: this.requirementForm.get('referenceCases')?.value || [],
  340. tags: {
  341. demandType: this.customerForm.get('demandType')?.value,
  342. preferenceTags: this.preferenceTags,
  343. followUpStatus: this.customerForm.get('followUpStatus')?.value
  344. }
  345. };
  346. this.projectService.createProject(projectData).subscribe(
  347. (response: any) => {
  348. this.snackBar.open('项目创建成功', '关闭', { duration: 3000 });
  349. },
  350. (error: any) => {
  351. this.snackBar.open('项目创建失败,请重试', '关闭', { duration: 3000 });
  352. }
  353. );
  354. }
  355. /**
  356. * 创建项目群
  357. */
  358. createProjectGroup() {
  359. // 先检查是否选择了客户
  360. if (!this.selectedCustomer()) {
  361. this.snackBar.open('请先选择客户', '确定', { duration: 2000 });
  362. return;
  363. }
  364. // 显示弹窗
  365. this.dialog.open(ProjectGroupDialog, {
  366. width: '500px',
  367. data: {
  368. selectedCustomer: this.selectedCustomer(),
  369. demandType: this.customerForm.get('demandType')?.value,
  370. preferenceTags: this.preferenceTags,
  371. followUpStatus: this.customerForm.get('followUpStatus')?.value
  372. }
  373. }).afterClosed().subscribe(result => {
  374. if (result && result.confirm) {
  375. const tags = {
  376. demandType: result.demandType,
  377. preferenceTags: result.preferenceTags,
  378. followUpStatus: result.followUpStatus
  379. };
  380. this.isSubmitting.set(true);
  381. this.projectService.createProjectGroup({
  382. customerId: result.customerId,
  383. customerName: result.customerName,
  384. tags
  385. }).subscribe(
  386. (response: any) => {
  387. if (response.success) {
  388. this.snackBar.open(`项目群创建成功,群ID: ${response.groupId}`, '确定', { duration: 3000 });
  389. } else {
  390. this.snackBar.open('项目群创建失败,请稍后重试', '确定', { duration: 2000 });
  391. }
  392. this.isSubmitting.set(false);
  393. },
  394. (error: any) => {
  395. this.snackBar.open('创建项目群时出错,请稍后重试', '确定', { duration: 2000 });
  396. this.isSubmitting.set(false);
  397. }
  398. );
  399. }
  400. });
  401. }
  402. }