dashboard.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. // 修复 OnDestroy 导入和使用
  2. import { Component, OnInit, OnDestroy, signal, computed } from '@angular/core';
  3. import { CommonModule } from '@angular/common';
  4. import { FormsModule } from '@angular/forms';
  5. import { RouterModule, Router, ActivatedRoute } from '@angular/router';
  6. import { ProjectService } from '../../../services/project.service';
  7. import { Project, Task, CustomerFeedback } from '../../../models/project.model';
  8. @Component({
  9. selector: 'app-dashboard',
  10. standalone: true,
  11. imports: [CommonModule, FormsModule, RouterModule],
  12. templateUrl: './dashboard.html',
  13. styleUrls: ['./dashboard.scss', '../customer-service-styles.scss'],
  14. providers: [ProjectService]
  15. })
  16. export class Dashboard implements OnInit, OnDestroy {
  17. // 数据看板统计
  18. stats = {
  19. newConsultations: signal(12),
  20. pendingAssignments: signal(5),
  21. exceptionProjects: signal(2),
  22. // 新增核心指标
  23. conversionRateToday: signal(36), // 当日成交率(%)
  24. pendingComplaints: signal(3), // 待处理投诉数
  25. unRepliedConsultations: signal(7), // 未回复咨询数
  26. // 新客户触达统计
  27. newCustomerReachCount: signal(8), // 新客户待触达数量
  28. newCustomerConversionRate: signal(24), // 新客户转化率(%)
  29. // 老客户回访统计
  30. oldCustomerFollowUpCount: signal(6), // 老客户待回访数量
  31. oldCustomerRetentionRate: signal(78) // 老客户留存率(%)
  32. };
  33. // 新增:新客户触达/老客户回访列表(增强版本,包含客户标签和策略)
  34. newReachOutCustomers = signal<Array<{
  35. name: string;
  36. demandType: string;
  37. lastContactAt: Date;
  38. customerTag: 'value-sensitive' | 'price-sensitive';
  39. recommendedPhrase: string;
  40. caseStrategy: string;
  41. }>>([]);
  42. oldCustomerFollowUps = signal<Array<{
  43. name: string;
  44. demandType: string;
  45. lastContactAt: Date;
  46. customerTag: 'value-sensitive' | 'price-sensitive';
  47. recommendedPhrase: string;
  48. caseStrategy: string;
  49. }>>([]);
  50. urgentTasks = signal<Task[]>([]);
  51. // 任务处理状态
  52. taskProcessingState = signal<Partial<Record<string, { inProgress: boolean; progress: number }>>>({});
  53. // 项目动态流
  54. projectUpdates = signal<(Project | CustomerFeedback)[]>([]);
  55. // 搜索关键词
  56. searchTerm = signal('');
  57. // 筛选后的项目更新
  58. filteredUpdates = computed(() => {
  59. if (!this.searchTerm()) return this.projectUpdates();
  60. return this.projectUpdates().filter(item => {
  61. if ('name' in item) {
  62. // 项目
  63. return item.name.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  64. item.customerName.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  65. item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  66. } else {
  67. // 反馈
  68. return 'content' in item && item.content.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  69. 'status' in item && item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  70. }
  71. });
  72. });
  73. currentDate = new Date();
  74. // 回到顶部按钮可见性信号
  75. showBackToTopSignal = signal(false);
  76. // 任务表单可见性
  77. isTaskFormVisible = signal(false);
  78. // 新任务数据
  79. newTask: Task = {
  80. id: '',
  81. projectId: '',
  82. projectName: '',
  83. title: '',
  84. stage: '需求沟通',
  85. deadline: new Date(),
  86. isOverdue: false,
  87. isCompleted: false,
  88. priority: 'high',
  89. assignee: '当前用户',
  90. description: ''
  91. };
  92. // 用于日期时间输入的属性
  93. deadlineInput = '';
  94. // 预设快捷时长选项
  95. timePresets = [
  96. { label: '1小时内', hours: 1 },
  97. { label: '3小时内', hours: 3 },
  98. { label: '6小时内', hours: 6 },
  99. { label: '12小时内', hours: 12 },
  100. { label: '24小时内', hours: 24 }
  101. ];
  102. // 选中的预设时长
  103. selectedPreset = '';
  104. // 自定义时间弹窗可见性
  105. isCustomTimeVisible = false;
  106. // 自定义选择的日期和时间
  107. customDate = new Date();
  108. customTime = '';
  109. // 错误提示信息
  110. deadlineError = '';
  111. // 提交按钮是否禁用
  112. isSubmitDisabled = false;
  113. // 下拉框可见性
  114. deadlineDropdownVisible = false;
  115. // 日期范围限制
  116. get todayDate(): string {
  117. return new Date().toISOString().split('T')[0];
  118. }
  119. get sevenDaysLaterDate(): string {
  120. const date = new Date();
  121. date.setDate(date.getDate() + 7);
  122. return date.toISOString().split('T')[0];
  123. }
  124. constructor(
  125. private projectService: ProjectService,
  126. private router: Router,
  127. private activatedRoute: ActivatedRoute
  128. ) {}
  129. ngOnInit(): void {
  130. this.loadUrgentTasks();
  131. this.loadProjectUpdates();
  132. this.loadCRMQueues(); // 新增:加载新客户触达与老客户回访队列
  133. // 添加滚动事件监听
  134. window.addEventListener('scroll', this.onScroll.bind(this));
  135. }
  136. // 添加滚动事件处理方法
  137. private onScroll(): void {
  138. this.showBackToTopSignal.set(window.scrollY > 300);
  139. }
  140. // 添加显示回到顶部按钮的计算属性
  141. showBackToTop = computed(() => this.showBackToTopSignal());
  142. // 清理事件监听器
  143. ngOnDestroy(): void {
  144. window.removeEventListener('scroll', this.onScroll.bind(this));
  145. }
  146. // 添加scrollToTop方法
  147. scrollToTop(): void {
  148. window.scrollTo({
  149. top: 0,
  150. behavior: 'smooth'
  151. });
  152. }
  153. // 查看人员考勤
  154. viewAttendance(): void {
  155. this.router.navigate(['/hr/attendance']);
  156. }
  157. // 修改loadUrgentTasks方法,添加status属性
  158. loadUrgentTasks(): void {
  159. // 从服务获取任务数据,筛选出紧急任务
  160. this.projectService.getTasks().subscribe(tasks => {
  161. const filteredTasks = tasks.map(task => ({...task, status: task.isOverdue ? '已逾期' : task.isCompleted ? '已完成' : '进行中'}))
  162. .filter(task => task.isOverdue || task.deadline.toDateString() === new Date().toDateString());
  163. this.urgentTasks.set(filteredTasks.sort((a, b) => {
  164. // 按紧急程度排序
  165. if (a.isOverdue && !b.isOverdue) return -1;
  166. if (!a.isOverdue && b.isOverdue) return 1;
  167. return a.deadline.getTime() - b.deadline.getTime();
  168. }));
  169. });
  170. }
  171. // 加载新客户触达与老客户回访数据(示例数据,后续可接入接口)
  172. private loadCRMQueues(): void {
  173. const now = new Date();
  174. this.newReachOutCustomers.set([
  175. {
  176. name: '陈女士',
  177. demandType: '全屋定制',
  178. lastContactAt: new Date(now.getTime() - 2 * 60 * 60 * 1000),
  179. customerTag: 'value-sensitive',
  180. recommendedPhrase: '我们的全屋定制方案注重品质与设计的完美结合,为您打造独一无二的家居空间',
  181. caseStrategy: '推荐高端别墅案例,强调设计理念和材料品质'
  182. },
  183. {
  184. name: '赵先生',
  185. demandType: '厨房改造',
  186. lastContactAt: new Date(now.getTime() - 26 * 60 * 60 * 1000),
  187. customerTag: 'price-sensitive',
  188. recommendedPhrase: '我们的厨房改造方案性价比极高,在预算范围内实现最大化的功能提升',
  189. caseStrategy: '推荐经济实用案例,突出成本控制和实用功能'
  190. },
  191. {
  192. name: '吴先生',
  193. demandType: '客厅软装',
  194. lastContactAt: new Date(now.getTime() - 5 * 60 * 60 * 1000),
  195. customerTag: 'value-sensitive',
  196. recommendedPhrase: '我们的软装设计师将为您量身定制,打造有品味的生活空间',
  197. caseStrategy: '推荐精品软装案例,强调设计师专业度和美学价值'
  198. }
  199. ]);
  200. this.oldCustomerFollowUps.set([
  201. {
  202. name: '王女士',
  203. demandType: '别墅整装',
  204. lastContactAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
  205. customerTag: 'value-sensitive',
  206. recommendedPhrase: '感谢您对我们的信任,我们将继续为您提供高品质的服务体验',
  207. caseStrategy: '展示同档次别墅案例,强调服务品质和后续保障'
  208. },
  209. {
  210. name: '李先生',
  211. demandType: '卧室升级',
  212. lastContactAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000),
  213. customerTag: 'price-sensitive',
  214. recommendedPhrase: '我们为老客户准备了特别优惠,让您以更实惠的价格享受升级服务',
  215. caseStrategy: '推荐性价比升级方案,提供老客户专属优惠'
  216. },
  217. {
  218. name: '孙女士',
  219. demandType: '卫生间翻新',
  220. lastContactAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000),
  221. customerTag: 'value-sensitive',
  222. recommendedPhrase: '基于您之前的项目经验,我们为您推荐更加精致的翻新方案',
  223. caseStrategy: '展示精品卫生间案例,强调细节工艺和材料升级'
  224. }
  225. ]);
  226. }
  227. // 查看全部咨询列表
  228. goToConsultationList(): void {
  229. this.router.navigate(['/customer-service/consultation-list']);
  230. }
  231. loadProjectUpdates(): void {
  232. // 模拟项目更新数据
  233. this.projectService.getProjects().subscribe(projects => {
  234. this.projectService.getCustomerFeedbacks().subscribe(feedbacks => {
  235. // 合并项目和反馈,按时间倒序排序
  236. const updates: (Project | CustomerFeedback)[] = [
  237. ...projects,
  238. ...feedbacks
  239. ].sort((a, b) => {
  240. const dateA = 'createdAt' in a ? a.createdAt : new Date(a['updatedAt'] || a['deadline']);
  241. const dateB = 'createdAt' in b ? b.createdAt : new Date(b['updatedAt'] || b['deadline']);
  242. return dateB.getTime() - dateA.getTime();
  243. }).slice(0, 20); // 限制显示20条
  244. this.projectUpdates.set(updates);
  245. });
  246. });
  247. }
  248. // 处理任务完成
  249. markTaskAsCompleted(taskId: string): void {
  250. this.urgentTasks.set(
  251. this.urgentTasks().map(task =>
  252. task.id === taskId ? { ...task, isCompleted: true, status: '已完成' } : task
  253. )
  254. );
  255. }
  256. // 处理派单操作
  257. handleAssignment(taskId: string): void {
  258. // 标记任务为处理中
  259. const task = this.urgentTasks().find(t => t.id === taskId);
  260. if (task) {
  261. // 初始化处理状态
  262. this.taskProcessingState.update(state => ({
  263. ...state,
  264. [task.id]: { inProgress: true, progress: 0 }
  265. }));
  266. // 模拟处理进度
  267. let progress = 0;
  268. const interval = setInterval(() => {
  269. progress += 10;
  270. this.taskProcessingState.update(state => ({
  271. ...state,
  272. [task.id]: { inProgress: progress < 100, progress }
  273. }));
  274. if (progress >= 100) {
  275. clearInterval(interval);
  276. // 处理完成后从列表中移除该任务
  277. this.urgentTasks.set(
  278. this.urgentTasks().filter(t => t.id !== task.id)
  279. );
  280. // 清除处理状态
  281. this.taskProcessingState.update(state => {
  282. const newState = { ...state };
  283. delete newState[task.id];
  284. return newState;
  285. });
  286. }
  287. }, 300);
  288. }
  289. // 更新统计数据
  290. this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1);
  291. }
  292. // 显示任务表单
  293. showTaskForm(): void {
  294. // 重置表单数据
  295. this.newTask = {
  296. id: '',
  297. projectId: '',
  298. projectName: '',
  299. title: '',
  300. stage: '需求沟通',
  301. deadline: new Date(),
  302. isOverdue: false,
  303. isCompleted: false,
  304. priority: 'high',
  305. assignee: '当前用户',
  306. description: ''
  307. };
  308. // 重置相关状态
  309. this.deadlineError = '';
  310. this.isSubmitDisabled = false;
  311. // 计算并设置默认预设时长
  312. this.setDefaultPreset();
  313. // 显示表单
  314. this.isTaskFormVisible.set(true);
  315. // 添加iOS风格的面板显示动画
  316. setTimeout(() => {
  317. document.querySelector('.ios-panel')?.classList.add('ios-panel-visible');
  318. }, 10);
  319. }
  320. // 设置默认预设时长
  321. private setDefaultPreset(): void {
  322. const now = new Date();
  323. const todayEnd = new Date(now);
  324. todayEnd.setHours(23, 59, 59, 999);
  325. // 检查3小时后是否超过当天24:00
  326. const threeHoursLater = new Date(now.getTime() + 3 * 60 * 60 * 1000);
  327. if (threeHoursLater <= todayEnd) {
  328. // 3小时后未超过当天24:00,默认选中3小时内
  329. this.selectedPreset = '3';
  330. this.updatePresetDeadline(3);
  331. } else {
  332. // 3小时后超过当天24:00,默认选中当天24:00前
  333. this.selectedPreset = 'today';
  334. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  335. this.newTask.deadline = todayEnd;
  336. }
  337. }
  338. // 处理预设时长选择
  339. handlePresetSelection(preset: string): void {
  340. this.selectedPreset = preset;
  341. this.deadlineError = '';
  342. if (preset === 'custom') {
  343. // 打开自定义时间选择器
  344. this.openCustomTimePicker();
  345. } else if (preset === 'today') {
  346. // 设置为当天24:00前
  347. const now = new Date();
  348. const todayEnd = new Date(now);
  349. todayEnd.setHours(23, 59, 59, 999);
  350. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  351. this.newTask.deadline = todayEnd;
  352. } else {
  353. // 计算预设时长的截止时间
  354. const hours = parseInt(preset);
  355. this.updatePresetDeadline(hours);
  356. }
  357. }
  358. // 更新预设时长的截止时间
  359. private updatePresetDeadline(hours: number): void {
  360. const now = new Date();
  361. const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000);
  362. this.deadlineInput = deadline.toISOString().slice(0, 16);
  363. this.newTask.deadline = deadline;
  364. }
  365. // 打开自定义时间选择器
  366. openCustomTimePicker(): void {
  367. // 重置自定义时间
  368. this.customDate = new Date();
  369. const now = new Date();
  370. this.customTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  371. // 显示自定义时间弹窗
  372. this.isCustomTimeVisible = true;
  373. // 添加iOS风格的弹窗动画
  374. setTimeout(() => {
  375. document.querySelector('.custom-time-modal')?.classList.add('modal-visible');
  376. }, 10);
  377. }
  378. // 关闭自定义时间选择器
  379. closeCustomTimePicker(): void {
  380. // 添加iOS风格的弹窗关闭动画
  381. const modal = document.querySelector('.custom-time-modal');
  382. if (modal) {
  383. modal.classList.remove('modal-visible');
  384. setTimeout(() => {
  385. this.isCustomTimeVisible = false;
  386. }, 300);
  387. } else {
  388. this.isCustomTimeVisible = false;
  389. }
  390. }
  391. // 处理自定义时间选择
  392. handleCustomTimeSelection(): void {
  393. const [hours, minutes] = this.customTime.split(':').map(Number);
  394. const selectedDateTime = new Date(this.customDate);
  395. selectedDateTime.setHours(hours, minutes, 0, 0);
  396. // 验证选择的时间是否有效
  397. if (this.validateDeadline(selectedDateTime)) {
  398. this.deadlineInput = selectedDateTime.toISOString().slice(0, 16);
  399. this.newTask.deadline = selectedDateTime;
  400. this.closeCustomTimePicker();
  401. }
  402. }
  403. // 验证截止时间是否有效
  404. validateDeadline(deadline: Date): boolean {
  405. const now = new Date();
  406. if (deadline < now) {
  407. this.deadlineError = '截止时间不能早于当前时间,请重新选择';
  408. this.isSubmitDisabled = true;
  409. return false;
  410. }
  411. this.deadlineError = '';
  412. this.isSubmitDisabled = false;
  413. return true;
  414. }
  415. // 获取显示的截止时间文本
  416. getDisplayDeadline(): string {
  417. if (!this.deadlineInput) return '';
  418. try {
  419. const date = new Date(this.deadlineInput);
  420. return date.toLocaleString('zh-CN', {
  421. year: 'numeric',
  422. month: '2-digit',
  423. day: '2-digit',
  424. hour: '2-digit',
  425. minute: '2-digit'
  426. });
  427. } catch (error) {
  428. return '';
  429. }
  430. }
  431. // 隐藏任务表单
  432. hideTaskForm(): void {
  433. // 添加iOS风格的面板隐藏动画
  434. const panel = document.querySelector('.ios-panel');
  435. if (panel) {
  436. panel.classList.remove('ios-panel-visible');
  437. setTimeout(() => {
  438. this.isTaskFormVisible.set(false);
  439. }, 300);
  440. } else {
  441. this.isTaskFormVisible.set(false);
  442. }
  443. }
  444. // 处理添加任务表单提交
  445. handleAddTaskSubmit(): void {
  446. // 验证表单数据
  447. if (!this.newTask.title.trim() || !this.newTask.projectName.trim() || !this.deadlineInput || this.isSubmitDisabled) {
  448. // 在实际应用中,这里应该显示错误提示
  449. alert('请填写必填字段(任务标题、项目名称、截止时间)');
  450. return;
  451. }
  452. // 创建新任务
  453. const taskToAdd: Task = {
  454. ...this.newTask,
  455. id: `task-${Date.now()}`,
  456. projectId: `project-${Math.floor(Math.random() * 1000)}`,
  457. deadline: new Date(this.deadlineInput),
  458. isOverdue: new Date(this.deadlineInput) < new Date()
  459. };
  460. // 添加到任务列表
  461. this.urgentTasks.set([taskToAdd, ...this.urgentTasks()]);
  462. // 更新统计数据
  463. this.stats.pendingAssignments.set(this.stats.pendingAssignments() + 1);
  464. // 隐藏表单
  465. this.hideTaskForm();
  466. }
  467. // 添加新的紧急事项
  468. addUrgentTask(): void {
  469. // 调用显示表单方法
  470. this.showTaskForm();
  471. }
  472. // 新咨询数图标点击处理
  473. handleNewConsultationsClick(): void {
  474. this.navigateToDetail('consultations');
  475. }
  476. // 待派单数图标点击处理
  477. handlePendingAssignmentsClick(): void {
  478. this.navigateToDetail('assignments');
  479. }
  480. // 异常项目图标点击处理
  481. handleExceptionProjectsClick(): void {
  482. this.navigateToDetail('exceptions');
  483. }
  484. // 导航到详情页
  485. private navigateToDetail(type: 'consultations' | 'assignments' | 'exceptions'): void {
  486. const routeMap = {
  487. consultations: '/customer-service/consultation-list',
  488. assignments: '/customer-service/assignment-list',
  489. exceptions: '/customer-service/exception-list'
  490. };
  491. console.log('导航到:', routeMap[type]);
  492. console.log('当前路由:', this.router.url);
  493. // 添加iOS风格页面过渡动画
  494. document.body.classList.add('ios-page-transition');
  495. setTimeout(() => {
  496. this.router.navigateByUrl(routeMap[type])
  497. .then(navResult => {
  498. console.log('导航结果:', navResult);
  499. if (!navResult) {
  500. console.error('导航失败,检查路由配置');
  501. }
  502. })
  503. .catch(err => {
  504. console.error('导航错误:', err);
  505. });
  506. setTimeout(() => {
  507. document.body.classList.remove('ios-page-transition');
  508. }, 300);
  509. }, 100);
  510. }
  511. // 格式化日期
  512. formatDate(date: Date | string): string {
  513. if (!date) return '';
  514. try {
  515. return new Date(date).toLocaleString('zh-CN', {
  516. month: '2-digit',
  517. day: '2-digit',
  518. hour: '2-digit',
  519. minute: '2-digit'
  520. });
  521. } catch (error) {
  522. console.error('日期格式化错误:', error);
  523. return '';
  524. }
  525. }
  526. // 添加安全获取客户名称的方法
  527. getCustomerName(update: Project | CustomerFeedback): string {
  528. if ('customerName' in update && update.customerName) {
  529. return update.customerName;
  530. } else if ('projectId' in update) {
  531. // 查找相关项目获取客户名称
  532. return '客户反馈';
  533. }
  534. return '未知客户';
  535. }
  536. // 优化的日期格式化方法
  537. getFormattedDate(update: Project | CustomerFeedback): string {
  538. if (!update) return '';
  539. if ('createdAt' in update && update.createdAt) {
  540. return this.formatDate(update.createdAt);
  541. } else if ('updatedAt' in update && update.updatedAt) {
  542. return this.formatDate(update.updatedAt);
  543. } else if ('deadline' in update && update.deadline) {
  544. return this.formatDate(update.deadline);
  545. }
  546. return '';
  547. }
  548. // 添加获取状态的安全方法
  549. getUpdateStatus(update: Project | CustomerFeedback): string {
  550. if ('status' in update && update.status) {
  551. return update.status;
  552. }
  553. return '已更新';
  554. }
  555. // 添加getTaskStatus方法的正确实现
  556. getTaskStatus(task: Task): string {
  557. if (!task) return '未知状态';
  558. if (task.isCompleted) return '已完成';
  559. if (task.isOverdue) return '已逾期';
  560. return '进行中';
  561. }
  562. // 添加getUpdateStatusClass方法的正确实现
  563. getUpdateStatusClass(update: Project | CustomerFeedback): string {
  564. if (!update || !('status' in update) || !update.status) return '';
  565. switch (update.status) {
  566. case '进行中':
  567. return 'status-active';
  568. case '已完成':
  569. return 'status-completed';
  570. case '已延期':
  571. case '已暂停':
  572. return 'status-warning';
  573. case '已解决':
  574. return 'status-success';
  575. case '待处理':
  576. case '处理中':
  577. return 'status-info';
  578. default:
  579. return '';
  580. }
  581. }
  582. }